diff --git a/bin/install.js b/bin/install.js index 5c7f82ea4..fef57a808 100755 --- a/bin/install.js +++ b/bin/install.js @@ -3650,9 +3650,26 @@ function uninstall(isGlobal, runtime = 'claude') { // Gemini: still uses commands/gsd/ const gsdCommandsDir = path.join(targetDir, 'commands', 'gsd'); if (fs.existsSync(gsdCommandsDir)) { + // Preserve user-generated files before wipe (#1423) + // Note: if more user files are added, consider a naming convention (e.g., USER-*.md) + // and preserve all matching files instead of listing each one individually. + const devPrefsPath = path.join(gsdCommandsDir, 'dev-preferences.md'); + const preservedDevPrefs = fs.existsSync(devPrefsPath) ? fs.readFileSync(devPrefsPath, 'utf-8') : null; + fs.rmSync(gsdCommandsDir, { recursive: true }); removedCount++; console.log(` ${green}✓${reset} Removed commands/gsd/`); + + // Restore user-generated files + if (preservedDevPrefs) { + try { + fs.mkdirSync(gsdCommandsDir, { recursive: true }); + fs.writeFileSync(devPrefsPath, preservedDevPrefs); + console.log(` ${green}✓${reset} Preserved commands/gsd/dev-preferences.md`); + } catch (err) { + console.error(` ${red}✗${reset} Failed to restore dev-preferences.md: ${err.message}`); + } + } } } else { // Claude Code: remove skills/gsd-*/ directories @@ -3675,18 +3692,47 @@ function uninstall(isGlobal, runtime = 'claude') { // Also clean up legacy commands/gsd/ from older installs const legacyCommandsDir = path.join(targetDir, 'commands', 'gsd'); if (fs.existsSync(legacyCommandsDir)) { + // Preserve user-generated files before legacy wipe (#1423) + const devPrefsPath = path.join(legacyCommandsDir, 'dev-preferences.md'); + const preservedDevPrefs = fs.existsSync(devPrefsPath) ? fs.readFileSync(devPrefsPath, 'utf-8') : null; + fs.rmSync(legacyCommandsDir, { recursive: true }); removedCount++; console.log(` ${green}✓${reset} Removed legacy commands/gsd/`); + + if (preservedDevPrefs) { + try { + fs.mkdirSync(legacyCommandsDir, { recursive: true }); + fs.writeFileSync(devPrefsPath, preservedDevPrefs); + console.log(` ${green}✓${reset} Preserved commands/gsd/dev-preferences.md`); + } catch (err) { + console.error(` ${red}✗${reset} Failed to restore dev-preferences.md: ${err.message}`); + } + } } } // 2. Remove get-shit-done directory const gsdDir = path.join(targetDir, 'get-shit-done'); if (fs.existsSync(gsdDir)) { + // Preserve user-generated files before wipe (#1423) + const userProfilePath = path.join(gsdDir, 'USER-PROFILE.md'); + const preservedProfile = fs.existsSync(userProfilePath) ? fs.readFileSync(userProfilePath, 'utf-8') : null; + fs.rmSync(gsdDir, { recursive: true }); removedCount++; console.log(` ${green}✓${reset} Removed get-shit-done/`); + + // Restore user-generated files + if (preservedProfile) { + try { + fs.mkdirSync(gsdDir, { recursive: true }); + fs.writeFileSync(userProfilePath, preservedProfile); + console.log(` ${green}✓${reset} Preserved get-shit-done/USER-PROFILE.md`); + } catch (err) { + console.error(` ${red}✗${reset} Failed to restore USER-PROFILE.md: ${err.message}`); + } + } } // 3. Remove GSD agents (gsd-*.md files only) diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index 8f123b42b..59766a3d8 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -1341,3 +1341,89 @@ describe('E2E: Copilot uninstall verification', () => { }); }); }); + +// ─── Claude uninstall: user file preservation (#1423) ───────────────────────── + +function runClaudeInstall(cwd) { + const env = { ...process.env }; + delete env.GSD_TEST_MODE; + return execFileSync(process.execPath, [INSTALL_PATH, '--claude', '--local'], { + cwd, + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + env, + }); +} + +function runClaudeUninstall(cwd) { + const env = { ...process.env }; + delete env.GSD_TEST_MODE; + return execFileSync(process.execPath, [INSTALL_PATH, '--claude', '--local', '--uninstall'], { + cwd, + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + env, + }); +} + +describe('Claude uninstall preserves user-generated files (#1423)', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-preserve-')); + runClaudeInstall(tmpDir); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + test('preserves USER-PROFILE.md across uninstall', () => { + const profilePath = path.join(tmpDir, '.claude', 'get-shit-done', 'USER-PROFILE.md'); + const content = '# Developer Profile\n\nAutonomy: High\nGenerated: 2026-03-29\n'; + fs.writeFileSync(profilePath, content); + + runClaudeUninstall(tmpDir); + + assert.ok(fs.existsSync(profilePath), 'USER-PROFILE.md should survive uninstall'); + assert.strictEqual(fs.readFileSync(profilePath, 'utf-8'), content, 'content should be identical'); + }); + + test('preserves dev-preferences.md across uninstall', () => { + const prefsDir = path.join(tmpDir, '.claude', 'commands', 'gsd'); + fs.mkdirSync(prefsDir, { recursive: true }); + const prefsPath = path.join(prefsDir, 'dev-preferences.md'); + const content = '---\nname: dev-preferences\n---\n# Preferences\nUse TypeScript strict.\n'; + fs.writeFileSync(prefsPath, content); + + runClaudeUninstall(tmpDir); + + assert.ok(fs.existsSync(prefsPath), 'dev-preferences.md should survive uninstall'); + assert.strictEqual(fs.readFileSync(prefsPath, 'utf-8'), content, 'content should be identical'); + }); + + test('still removes GSD engine files during uninstall', () => { + const profilePath = path.join(tmpDir, '.claude', 'get-shit-done', 'USER-PROFILE.md'); + fs.writeFileSync(profilePath, '# Profile\n'); + + // Verify engine files exist before uninstall + const binDir = path.join(tmpDir, '.claude', 'get-shit-done', 'bin'); + assert.ok(fs.existsSync(binDir), 'bin/ should exist before uninstall'); + + runClaudeUninstall(tmpDir); + + // Engine files gone, user file preserved + assert.ok(!fs.existsSync(binDir), 'bin/ should be removed after uninstall'); + assert.ok(fs.existsSync(profilePath), 'USER-PROFILE.md should survive'); + }); + + test('clean uninstall when no user files exist', () => { + runClaudeUninstall(tmpDir); + + const gsdDir = path.join(tmpDir, '.claude', 'get-shit-done'); + const cmdDir = path.join(tmpDir, '.claude', 'commands', 'gsd'); + // Directories should be fully removed when no user files to preserve + assert.ok(!fs.existsSync(gsdDir), 'get-shit-done/ should not exist after clean uninstall'); + assert.ok(!fs.existsSync(cmdDir), 'commands/gsd/ should not exist after clean uninstall'); + }); +});