Merge pull request #1428 from Tibsfox/fix/preserve-profile-on-update-1423
fix(install): preserve USER-PROFILE.md and dev-preferences.md on update
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user