fix(installer): preserve USER-PROFILE.md and dev-preferences.md on re-install (#1945)
Running gsd-update (re-running the installer) silently deleted two
user-generated files:
- get-shit-done/USER-PROFILE.md (created by /gsd-profile-user)
- commands/gsd/dev-preferences.md (created by /gsd-profile-user)
Root causes:
1. copyWithPathReplacement() calls fs.rmSync(destDir, {recursive:true})
before copying, wiping USER-PROFILE.md with no preserve allowlist.
2. The legacy commands/gsd/ cleanup at ~line 5211 rmSync'd the entire
directory, wiping dev-preferences.md.
3. The backup path in profile-user.md pointed to the same directory
that gets wiped, so the backup was also lost.
Fix:
- Add preserveUserArtifacts(destDir, fileNames) and restoreUserArtifacts()
helpers that save/restore listed files around destructive wipes.
- Call them in install() before the get-shit-done/ copy (preserves
USER-PROFILE.md) and before the legacy commands/gsd/ cleanup
(preserves dev-preferences.md).
- Fix profile-user.md backup path from ~/.claude/get-shit-done/USER-PROFILE.backup.md
to ~/.claude/USER-PROFILE.backup.md (outside the wiped directory).
Closes #1924
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3809,6 +3809,42 @@ function copyCommandsAsAntigravitySkills(srcDir, skillsDir, prefix, isGlobal = f
|
||||
recurse(srcDir, prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save user-generated files from destDir to an in-memory map before a wipe.
|
||||
*
|
||||
* @param {string} destDir - Directory that is about to be wiped
|
||||
* @param {string[]} fileNames - Relative file names (e.g. ['USER-PROFILE.md']) to preserve
|
||||
* @returns {Map<string, string>} Map of fileName → file content (only entries that existed)
|
||||
*/
|
||||
function preserveUserArtifacts(destDir, fileNames) {
|
||||
const saved = new Map();
|
||||
for (const name of fileNames) {
|
||||
const fullPath = path.join(destDir, name);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
try {
|
||||
saved.set(name, fs.readFileSync(fullPath, 'utf8'));
|
||||
} catch { /* skip unreadable files */ }
|
||||
}
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore user-generated files saved by preserveUserArtifacts after a wipe.
|
||||
*
|
||||
* @param {string} destDir - Directory that was wiped and recreated
|
||||
* @param {Map<string, string>} saved - Map returned by preserveUserArtifacts
|
||||
*/
|
||||
function restoreUserArtifacts(destDir, saved) {
|
||||
for (const [name, content] of saved) {
|
||||
const fullPath = path.join(destDir, name);
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, content, 'utf8');
|
||||
} catch { /* skip unwritable paths */ }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively copy directory, replacing paths in .md files
|
||||
* Deletes existing destDir first to remove orphaned files from previous versions
|
||||
@@ -5217,10 +5253,13 @@ function install(isGlobal, runtime = 'claude') {
|
||||
}
|
||||
|
||||
// Clean up legacy commands/gsd/ from previous global installs
|
||||
// Preserve user-generated files (dev-preferences.md) before wiping the directory
|
||||
const legacyCommandsDir = path.join(targetDir, 'commands', 'gsd');
|
||||
if (fs.existsSync(legacyCommandsDir)) {
|
||||
const savedLegacyArtifacts = preserveUserArtifacts(legacyCommandsDir, ['dev-preferences.md']);
|
||||
fs.rmSync(legacyCommandsDir, { recursive: true });
|
||||
console.log(` ${green}✓${reset} Removed legacy commands/gsd/ directory`);
|
||||
restoreUserArtifacts(legacyCommandsDir, savedLegacyArtifacts);
|
||||
}
|
||||
} else {
|
||||
// Claude Code local: commands/gsd/ format — Claude Code reads local project
|
||||
@@ -5252,9 +5291,12 @@ function install(isGlobal, runtime = 'claude') {
|
||||
}
|
||||
|
||||
// Copy get-shit-done skill with path replacement
|
||||
// Preserve user-generated files before the wipe-and-copy so they survive re-install
|
||||
const skillSrc = path.join(src, 'get-shit-done');
|
||||
const skillDest = path.join(targetDir, 'get-shit-done');
|
||||
const savedGsdArtifacts = preserveUserArtifacts(skillDest, ['USER-PROFILE.md']);
|
||||
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal);
|
||||
restoreUserArtifacts(skillDest, savedGsdArtifacts);
|
||||
if (verifyInstalled(skillDest, 'get-shit-done')) {
|
||||
console.log(` ${green}✓${reset} Installed get-shit-done`);
|
||||
} else {
|
||||
@@ -6172,6 +6214,8 @@ if (process.env.GSD_TEST_MODE) {
|
||||
writeManifest,
|
||||
reportLocalPatches,
|
||||
validateHookFields,
|
||||
preserveUserArtifacts,
|
||||
restoreUserArtifacts,
|
||||
};
|
||||
} else {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user