* fix(install): skip Gemini local commands/gsd when global GSD present (#3037) Reporter showed that running `npx get-shit-done-cc --gemini --global` followed by `--gemini --local` in a project creates the same 65 GSD command files in both Gemini scopes: - ~/.gemini/commands/gsd/ (user scope) - <project>/.gemini/commands/gsd/ (workspace scope) Gemini conflict-detects by command name across scopes and renames every overlapping /gsd:* command to /workspace.gsd:* and /user.gsd:*, breaking the documented /gsd:* namespace. Fix: in bin/install.js, when handling --gemini --local, detect whether ~/.gemini/commands/gsd/ already exists with managed-shape content. If so, skip the local copy and print a clear three-line warning explaining the conflict avoidance. The user-scope install already provides the same /gsd:* commands in this project; the local copy adds zero value. Sibling fixes (test isolation): - tests/install-minimal-all-runtimes.test.cjs: pass HOME/USERPROFILE through the spawned installer's env so the developer's real ~/.gemini/commands/gsd/ doesn't trigger the new skip path during test runs that want to assert the local-install populates commands/gsd/. - tests/gemini-namespacing.test.cjs: the "Gemini Install (Behavioral)" describe block now creates an isolated tmpHome and points process.env.HOME at it before calling install(false, 'gemini'), with proper restore in afterEach. Test: - tests/bug-3037-gemini-duplicate-commands.test.cjs — 4 structural tests: 1. global install populates HOME/.gemini/commands/gsd 2. local install AFTER global skips the local copy 3. local install with NO existing global still populates locally (no-regression) 4. local install when HOME has .gemini/ but no GSD-managed commands/gsd/ still populates locally (non-GSD-Gemini-user no-regression) 6909/6909 full suite pass. Lints clean. Closes #3037 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: address CR feedback on PR #3041 — narrower detection + USERPROFILE restore CR findings: 1. **bin/install.js (Major)** — userScopeHasGsd used `fs.readdirSync(homeGeminiGsd).length > 0` which would skip the local install for any non-empty directory, including a user who hand-dropped a single override at ~/.gemini/commands/gsd/<thing> .toml without ever running --gemini --global. Narrowed the detection to require at least 3 canonical GSD command files (help.toml, progress.toml, new-project.toml) — a marker that ships in every GSD Gemini install (minimal mode included) and is structurally impossible to produce by accident. 2. **tests/bug-3037-...:59 (Minor)** — beforeEach overwrites process.env.USERPROFILE but afterEach only restores HOME, leaking the temp home into later tests on Windows or any code path that reads USERPROFILE. Added save/restore symmetric with HOME. Plus added a 5th regression test covering the narrowed detection: "local install when HOME has hand-dropped overrides UNDER commands/gsd/ (but no full GSD) still populates locally" — directly exercises the edge case CR identified. 5/5 targeted tests pass. 6910/6910 full suite pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -7556,15 +7556,55 @@ function install(isGlobal, runtime = 'claude') {
|
||||
// No skills/commands directory needed. Engine is installed via copyWithPathReplacement.
|
||||
console.log(` ${green}✓${reset} Cline: commands will be available via .clinerules`);
|
||||
} else if (isGemini) {
|
||||
const commandsDir = path.join(targetDir, 'commands');
|
||||
fs.mkdirSync(commandsDir, { recursive: true });
|
||||
const gsdSrc = stageSkillsForMode(path.join(src, 'commands', 'gsd'), installMode);
|
||||
const gsdDest = path.join(commandsDir, 'gsd');
|
||||
copyWithPathReplacement(gsdSrc, gsdDest, pathPrefix, runtime, true, isGlobal);
|
||||
if (verifyInstalled(gsdDest, 'commands/gsd')) {
|
||||
console.log(` ${green}✓${reset} Installed commands/gsd`);
|
||||
// #3037: when running --local --gemini and a GSD-managed user-scope
|
||||
// command directory already exists at ~/.gemini/commands/gsd/, skip
|
||||
// the local copy. Gemini conflict-detects by command name across
|
||||
// scopes and renames every overlapping /gsd:* command to
|
||||
// /workspace.gsd:* and /user.gsd:*, breaking the documented namespace.
|
||||
// The user-scope install already provides the same commands, so the
|
||||
// local copy adds zero value at the cost of namespace conflicts.
|
||||
//
|
||||
// CR #3041 (Major): the detection must be specific to PACKAGE-MANAGED
|
||||
// GSD content, not just "directory is non-empty". A user who hand-
|
||||
// dropped a single override (e.g. ~/.gemini/commands/gsd/my-override
|
||||
// .toml) would otherwise be unable to run a local install at all.
|
||||
// Detection rule: at least 3 of the canonical GSD command files
|
||||
// ('help.toml', 'progress.toml', 'new-project.toml') must be present.
|
||||
// These three ship in every GSD Gemini install (minimal mode included
|
||||
// — they're in the core skill set per #2790's consolidation), and 3-of-
|
||||
// 3 with that specific basename set is structurally impossible to
|
||||
// produce by accident.
|
||||
const homeGeminiGsd = path.join(os.homedir(), '.gemini', 'commands', 'gsd');
|
||||
const GSD_MANAGED_CANARIES = ['help.toml', 'progress.toml', 'new-project.toml'];
|
||||
const userScopeHasGsd =
|
||||
!isGlobal &&
|
||||
path.resolve(targetDir) !== path.resolve(path.join(os.homedir(), '.gemini')) &&
|
||||
fs.existsSync(homeGeminiGsd) &&
|
||||
GSD_MANAGED_CANARIES.every((f) =>
|
||||
fs.existsSync(path.join(homeGeminiGsd, f))
|
||||
);
|
||||
|
||||
if (userScopeHasGsd) {
|
||||
console.log(
|
||||
` ${yellow}⚠${reset} Skipping commands/gsd/ for local install — GSD is already installed at user scope (${homeGeminiGsd}).`
|
||||
);
|
||||
console.log(
|
||||
` Gemini conflict-detects across scopes and would rename every /gsd:* command to /workspace.gsd:* and /user.gsd:*.`
|
||||
);
|
||||
console.log(
|
||||
` The user-scope install already provides /gsd:* commands in this project; no local copy is needed.`
|
||||
);
|
||||
} else {
|
||||
failures.push('commands/gsd');
|
||||
const commandsDir = path.join(targetDir, 'commands');
|
||||
fs.mkdirSync(commandsDir, { recursive: true });
|
||||
const gsdSrc = stageSkillsForMode(path.join(src, 'commands', 'gsd'), installMode);
|
||||
const gsdDest = path.join(commandsDir, 'gsd');
|
||||
copyWithPathReplacement(gsdSrc, gsdDest, pathPrefix, runtime, true, isGlobal);
|
||||
if (verifyInstalled(gsdDest, 'commands/gsd')) {
|
||||
console.log(` ${green}✓${reset} Installed commands/gsd`);
|
||||
} else {
|
||||
failures.push('commands/gsd');
|
||||
}
|
||||
}
|
||||
} else if (isGlobal) {
|
||||
// Claude Code global: skills/ format (2.1.88+ compatibility)
|
||||
|
||||
Reference in New Issue
Block a user