From 6f3a9d88c7a31b1cf6d25ca872696e29970f13d9 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Fri, 27 Mar 2026 11:22:04 -0700 Subject: [PATCH 1/5] fix(install): preserve USER-PROFILE.md and dev-preferences.md on update The uninstall step wipes get-shit-done/ and commands/gsd/ directories entirely, destroying user-generated files like USER-PROFILE.md (from /gsd:profile-user) and dev-preferences.md. These files are now read before rmSync and restored immediately after. Closes #1423 Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/install.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/bin/install.js b/bin/install.js index 9d5a58c4e..d21b98dfa 100755 --- a/bin/install.js +++ b/bin/install.js @@ -3650,9 +3650,20 @@ 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) + 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) { + fs.mkdirSync(gsdCommandsDir, { recursive: true }); + fs.writeFileSync(devPrefsPath, preservedDevPrefs); + console.log(` ${green}✓${reset} Preserved commands/gsd/dev-preferences.md`); + } } } else { // Claude Code: remove skills/gsd-*/ directories @@ -3684,9 +3695,20 @@ function uninstall(isGlobal, runtime = 'claude') { // 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) { + fs.mkdirSync(gsdDir, { recursive: true }); + fs.writeFileSync(userProfilePath, preservedProfile); + console.log(` ${green}✓${reset} Preserved get-shit-done/USER-PROFILE.md`); + } } // 3. Remove GSD agents (gsd-*.md files only) From 08bff6f8e9fa1ce93531fd46d59eacb5738beff9 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Sun, 29 Mar 2026 14:22:28 -0700 Subject: [PATCH 2/5] test(install): add automated tests for user file preservation (#1423) E2E tests verifying USER-PROFILE.md and dev-preferences.md survive uninstall. Covers: profile preservation, preferences preservation, engine files still removed, clean uninstall without user files. Addresses review feedback requesting automated coverage for the preserve-and-restore pattern in the rmSync uninstall path. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/copilot-install.test.cjs | 84 ++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index 25ec66c1e..c80ddff53 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -1341,3 +1341,87 @@ 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 prefsPath = path.join(tmpDir, '.claude', 'commands', 'gsd', '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'); + }); +}); From bdd41f961e64edb596c0d2b47998e2532b1d4ce6 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Thu, 2 Apr 2026 04:06:21 -0700 Subject: [PATCH 3/5] fix(install): add error handling on restore and scalability note Address review feedback: wrap writeFileSync in try/catch so restore failures surface a clear error instead of silently losing user files. Add comment noting the naming convention approach for future scaling. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/install.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/bin/install.js b/bin/install.js index d21b98dfa..ed0d61a7a 100755 --- a/bin/install.js +++ b/bin/install.js @@ -3651,6 +3651,8 @@ function uninstall(isGlobal, runtime = 'claude') { 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; @@ -3660,9 +3662,13 @@ function uninstall(isGlobal, runtime = 'claude') { // Restore user-generated files if (preservedDevPrefs) { - fs.mkdirSync(gsdCommandsDir, { recursive: true }); - fs.writeFileSync(devPrefsPath, preservedDevPrefs); - console.log(` ${green}✓${reset} Preserved commands/gsd/dev-preferences.md`); + 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 { @@ -3705,9 +3711,13 @@ function uninstall(isGlobal, runtime = 'claude') { // Restore user-generated files if (preservedProfile) { - fs.mkdirSync(gsdDir, { recursive: true }); - fs.writeFileSync(userProfilePath, preservedProfile); - console.log(` ${green}✓${reset} Preserved get-shit-done/USER-PROFILE.md`); + 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}`); + } } } From 11d5c0a4bdfd07767a4c257451149a0375c7cfe6 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Thu, 2 Apr 2026 05:49:24 -0700 Subject: [PATCH 4/5] fix(test): create commands/gsd/ dir before writing dev-preferences in test Since #1540 migrated Claude Code to skills/ format, the installer may not create commands/gsd/ anymore. The test needs to ensure the directory exists before writing the user file into it. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/copilot-install.test.cjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index c80ddff53..690d59a23 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -1390,7 +1390,9 @@ describe('Claude uninstall preserves user-generated files (#1423)', () => { }); test('preserves dev-preferences.md across uninstall', () => { - const prefsPath = path.join(tmpDir, '.claude', 'commands', 'gsd', 'dev-preferences.md'); + 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); From 6c5f89a4fd8759c7fa841b92629f7ead83247232 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Thu, 2 Apr 2026 05:55:42 -0700 Subject: [PATCH 5/5] fix(install): preserve dev-preferences.md in Claude Code legacy cleanup path After #1540 migrated Claude Code to skills/ format, the uninstall added a legacy commands/gsd/ cleanup that wiped the directory without checking for user files. Add preserve logic to the legacy cleanup path matching what Gemini's commands/gsd/ path already has. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/install.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/bin/install.js b/bin/install.js index ed0d61a7a..a19f3d27d 100755 --- a/bin/install.js +++ b/bin/install.js @@ -3692,9 +3692,23 @@ 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}`); + } + } } }