fix(#1477): scope changeset to marker writer; lock guard with negative tests

Address maintainer re-review nits:
- Changeset body no longer claims install-exports resolution (Failure 2 is
  now handled upstream by #1511 / ADR-1508 Phase 2). This PR delivers the
  .gsd-source marker writer, which fixes list/status (Failure 1).
- Add negative tests locking the `runtime === 'claude' && isGlobal` write
  guard: a non-claude global install and a claude *local* install both
  provision no marker. Verified must-fail-first against a relaxed guard.
This commit is contained in:
Joe Slitzker
2026-06-25 09:46:09 -05:00
parent 4e473c8847
commit ddfc6b0815
2 changed files with 44 additions and 1 deletions

View File

@@ -2,4 +2,4 @@
type: Fixed
pr: 1487
---
**`/gsd-surface` works on Claude Code global installs** — the installer now writes a `.gsd-source` marker and install-exports resolve in the deployed layout, so `list`/`status` and `profile`/`enable`/`disable`/`reset` no longer throw `could not locate commands/gsd` or `MODULE_NOT_FOUND`. (#1487)
**`/gsd-surface` (`list`/`status`) works on Claude Code global installs** — the installer now writes a `.gsd-source` marker pointing at its `commands/gsd` source, so `findInstallSourceRoot` resolves on the global skills layout (which ships no `commands/gsd` tree) instead of throwing `could not locate commands/gsd`. (#1487)

View File

@@ -726,6 +726,49 @@ describe('#1477 .gsd-source marker provisioning', () => {
assert.equal(path.basename(path.dirname(markerSrc)), 'commands');
});
// ── Guard: marker is scoped to claude-global ONLY (#1477 PR-scope) ───────────
// Locks the `runtime === 'claude' && isGlobal` write guard. Every other layout
// (non-claude runtimes, and claude *local*) ships a commands/gsd source tree, so
// findInstallSourceRoot's walk-up already resolves and the marker must not appear.
function findGsdSourceMarkers(root) {
const found = [];
const walk = (dir) => {
let entries;
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
for (const e of entries) {
const full = path.join(dir, e.name);
if (e.isDirectory()) walk(full);
else if (e.name === '.gsd-source') found.push(full);
}
};
walk(root);
return found;
}
test('non-claude global install writes no .gsd-source marker (locks runtime === claude)', () => {
runInstall(true /* isGlobal */, 'cursor');
assert.deepEqual(
findGsdSourceMarkers(tmpRoot), [],
'a non-claude runtime must not provision the claude-global marker',
);
});
test('claude local install writes no .gsd-source marker (locks isGlobal)', () => {
// Local installs target process.cwd()/.claude — chdir into the fixture so the
// install is contained within tmpRoot rather than polluting the repo.
const savedCwd = process.cwd();
process.chdir(tmpRoot);
try {
runInstall(false /* isGlobal */, 'claude');
} finally {
process.chdir(savedCwd);
}
assert.deepEqual(
findGsdSourceMarkers(tmpRoot), [],
'a claude local install must not provision the marker — local ships commands/gsd',
);
});
// ── Failure 1 end-to-end: resolution succeeds FROM the deployed tree ─────────
// The deployed module's __dirname is <claudeDir>/gsd-core/bin/lib, which has no
// commands/gsd ancestor (global skills layout). Only the marker rescues it.