From 12e6acbf53eadadfbb2d41b4075e5a52166de3d8 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Thu, 22 Jan 2026 20:36:34 -0600 Subject: [PATCH] feat(installer): add --uninstall flag to remove GSD files Adds -u/--uninstall option that cleanly removes all GSD-installed files: - commands/gsd/ directory (Claude Code) or gsd-*.md files (OpenCode) - get-shit-done/ directory - gsd-*.md agent files - GSD hooks (gsd-statusline.js, gsd-check-update.js/sh) - GSD entries from settings.json (statusline, hooks) - GSD permissions from opencode.json (OpenCode only) Usage: npx get-shit-done-cc --claude --global --uninstall Closes #236 Co-Authored-By: Claude Opus 4.5 --- README.md | 16 ++++ bin/install.js | 216 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+) diff --git a/README.md b/README.md index 170cecbb9..b4d1c583f 100644 --- a/README.md +++ b/README.md @@ -553,6 +553,22 @@ CLAUDE_CONFIG_DIR=/home/youruser/.claude npx get-shit-done-cc --global ``` This ensures absolute paths are used instead of `~` which may not expand correctly in containers. +### Uninstalling + +To remove GSD completely: + +```bash +# Global installs +npx get-shit-done-cc --claude --global --uninstall +npx get-shit-done-cc --opencode --global --uninstall + +# Local installs (current project) +npx get-shit-done-cc --claude --local --uninstall +npx get-shit-done-cc --opencode --local --uninstall +``` + +This removes all GSD commands, agents, hooks, and settings while preserving your other configurations. + --- ## Community Ports diff --git a/bin/install.js b/bin/install.js index 84ac47cd5..48707151a 100755 --- a/bin/install.js +++ b/bin/install.js @@ -22,6 +22,7 @@ const hasLocal = args.includes('--local') || args.includes('-l'); const hasOpencode = args.includes('--opencode'); const hasClaude = args.includes('--claude'); const hasBoth = args.includes('--both'); +const hasUninstall = args.includes('--uninstall') || args.includes('-u'); // Runtime selection - can be set by flags or interactive prompt let selectedRuntimes = []; @@ -140,6 +141,7 @@ if (hasHelp) { ${cyan}--claude${reset} Install for Claude Code only ${cyan}--opencode${reset} Install for OpenCode only ${cyan}--both${reset} Install for both Claude Code and OpenCode + ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files) ${cyan}-c, --config-dir ${reset} Specify custom config directory ${cyan}-h, --help${reset} Show this help message ${cyan}--force-statusline${reset} Replace existing statusline config @@ -163,6 +165,12 @@ if (hasHelp) { ${dim}# Install to current project only${reset} npx get-shit-done-cc --claude --local + ${dim}# Uninstall GSD from Claude Code globally${reset} + npx get-shit-done-cc --claude --global --uninstall + + ${dim}# Uninstall GSD from current project${reset} + npx get-shit-done-cc --claude --local --uninstall + ${yellow}Notes:${reset} The --config-dir option is useful when you have multiple Claude Code configurations (e.g., for different subscriptions). It takes priority @@ -525,6 +533,203 @@ function cleanupOrphanedHooks(settings) { return settings; } +/** + * Uninstall GSD from the specified directory for a specific runtime + * Removes only GSD-specific files/directories, preserves user content + * @param {boolean} isGlobal - Whether to uninstall from global or local + * @param {string} runtime - Target runtime ('claude' or 'opencode') + */ +function uninstall(isGlobal, runtime = 'claude') { + const isOpencode = runtime === 'opencode'; + const dirName = getDirName(runtime); + + // Get the target directory based on runtime and install type + const targetDir = isGlobal + ? getGlobalDir(runtime, explicitConfigDir) + : path.join(process.cwd(), dirName); + + const locationLabel = isGlobal + ? targetDir.replace(os.homedir(), '~') + : targetDir.replace(process.cwd(), '.'); + + const runtimeLabel = isOpencode ? 'OpenCode' : 'Claude Code'; + console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`); + + // Check if target directory exists + if (!fs.existsSync(targetDir)) { + console.log(` ${yellow}⚠${reset} Directory does not exist: ${locationLabel}`); + console.log(` Nothing to uninstall.\n`); + return; + } + + let removedCount = 0; + + // 1. Remove GSD commands directory + if (isOpencode) { + // OpenCode: remove command/gsd-*.md files + const commandDir = path.join(targetDir, 'command'); + if (fs.existsSync(commandDir)) { + const files = fs.readdirSync(commandDir); + for (const file of files) { + if (file.startsWith('gsd-') && file.endsWith('.md')) { + fs.unlinkSync(path.join(commandDir, file)); + removedCount++; + } + } + console.log(` ${green}✓${reset} Removed GSD commands from command/`); + } + } else { + // Claude Code: remove commands/gsd/ directory + const gsdCommandsDir = path.join(targetDir, 'commands', 'gsd'); + if (fs.existsSync(gsdCommandsDir)) { + fs.rmSync(gsdCommandsDir, { recursive: true }); + removedCount++; + console.log(` ${green}✓${reset} Removed commands/gsd/`); + } + } + + // 2. Remove get-shit-done directory + const gsdDir = path.join(targetDir, 'get-shit-done'); + if (fs.existsSync(gsdDir)) { + fs.rmSync(gsdDir, { recursive: true }); + removedCount++; + console.log(` ${green}✓${reset} Removed get-shit-done/`); + } + + // 3. Remove GSD agents (gsd-*.md files only) + const agentsDir = path.join(targetDir, 'agents'); + if (fs.existsSync(agentsDir)) { + const files = fs.readdirSync(agentsDir); + let agentCount = 0; + for (const file of files) { + if (file.startsWith('gsd-') && file.endsWith('.md')) { + fs.unlinkSync(path.join(agentsDir, file)); + agentCount++; + } + } + if (agentCount > 0) { + removedCount++; + console.log(` ${green}✓${reset} Removed ${agentCount} GSD agents`); + } + } + + // 4. Remove GSD hooks + const hooksDir = path.join(targetDir, 'hooks'); + if (fs.existsSync(hooksDir)) { + const gsdHooks = ['gsd-statusline.js', 'gsd-check-update.js', 'gsd-check-update.sh']; + let hookCount = 0; + for (const hook of gsdHooks) { + const hookPath = path.join(hooksDir, hook); + if (fs.existsSync(hookPath)) { + fs.unlinkSync(hookPath); + hookCount++; + } + } + if (hookCount > 0) { + removedCount++; + console.log(` ${green}✓${reset} Removed ${hookCount} GSD hooks`); + } + } + + // 5. Clean up settings.json (remove GSD hooks and statusline) + const settingsPath = path.join(targetDir, 'settings.json'); + if (fs.existsSync(settingsPath)) { + let settings = readSettings(settingsPath); + let settingsModified = false; + + // Remove GSD statusline if it references our hook + if (settings.statusLine && settings.statusLine.command && + settings.statusLine.command.includes('gsd-statusline')) { + delete settings.statusLine; + settingsModified = true; + console.log(` ${green}✓${reset} Removed GSD statusline from settings`); + } + + // Remove GSD hooks from SessionStart + if (settings.hooks && settings.hooks.SessionStart) { + const before = settings.hooks.SessionStart.length; + settings.hooks.SessionStart = settings.hooks.SessionStart.filter(entry => { + if (entry.hooks && Array.isArray(entry.hooks)) { + // Filter out GSD hooks + const hasGsdHook = entry.hooks.some(h => + h.command && (h.command.includes('gsd-check-update') || h.command.includes('gsd-statusline')) + ); + return !hasGsdHook; + } + return true; + }); + if (settings.hooks.SessionStart.length < before) { + settingsModified = true; + console.log(` ${green}✓${reset} Removed GSD hooks from settings`); + } + // Clean up empty array + if (settings.hooks.SessionStart.length === 0) { + delete settings.hooks.SessionStart; + } + // Clean up empty hooks object + if (Object.keys(settings.hooks).length === 0) { + delete settings.hooks; + } + } + + if (settingsModified) { + writeSettings(settingsPath, settings); + removedCount++; + } + } + + // 6. For OpenCode, clean up permissions from opencode.json + if (isOpencode) { + const opencodeConfigDir = getOpencodeGlobalDir(); + const configPath = path.join(opencodeConfigDir, 'opencode.json'); + if (fs.existsSync(configPath)) { + try { + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + let modified = false; + + // Remove GSD permission entries + if (config.permission) { + for (const permType of ['read', 'external_directory']) { + if (config.permission[permType]) { + const keys = Object.keys(config.permission[permType]); + for (const key of keys) { + if (key.includes('get-shit-done')) { + delete config.permission[permType][key]; + modified = true; + } + } + // Clean up empty objects + if (Object.keys(config.permission[permType]).length === 0) { + delete config.permission[permType]; + } + } + } + if (Object.keys(config.permission).length === 0) { + delete config.permission; + } + } + + if (modified) { + fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n'); + removedCount++; + console.log(` ${green}✓${reset} Removed GSD permissions from opencode.json`); + } + } catch (e) { + // Ignore JSON parse errors + } + } + } + + if (removedCount === 0) { + console.log(` ${yellow}⚠${reset} No GSD files found to remove.`); + } + + console.log(` + ${green}Done!${reset} GSD has been uninstalled from ${runtimeLabel}. + Your other files and settings have been preserved. +`); +} + /** * Configure OpenCode permissions to allow reading GSD reference docs * This prevents permission prompts when GSD accesses the get-shit-done directory @@ -1049,6 +1254,17 @@ if (hasGlobal && hasLocal) { } else if (explicitConfigDir && hasLocal) { console.error(` ${yellow}Cannot use --config-dir with --local${reset}`); process.exit(1); +} else if (hasUninstall) { + // Uninstall mode + if (!hasGlobal && !hasLocal) { + console.error(` ${yellow}--uninstall requires --global or --local${reset}`); + console.error(` Example: npx get-shit-done-cc --claude --global --uninstall`); + process.exit(1); + } + const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude']; + for (const runtime of runtimes) { + uninstall(hasGlobal, runtime); + } } else if (selectedRuntimes.length > 0) { // Non-interactive: runtime specified via flags if (!hasGlobal && !hasLocal) {