#!/usr/bin/env node /** * CodeGraph preuninstall cleanup script * * Runs automatically when `npm uninstall -g @colbymchenry/codegraph` is called. * Removes all CodeGraph configuration from Claude Code: * - MCP server entry from ~/.claude.json * - Permissions from ~/.claude/settings.json * - Hooks from ~/.claude/settings.json * - CodeGraph section from ~/.claude/CLAUDE.md * * This script must never throw — a failed cleanup must not block uninstall. */ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; const CODEGRAPH_SECTION_START = ''; const CODEGRAPH_SECTION_END = ''; function readJson(filePath: string): Record | null { try { if (!fs.existsSync(filePath)) return null; return JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch { return null; } } function writeJson(filePath: string, data: Record): void { fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n'); } /** * Remove CodeGraph MCP server from ~/.claude.json */ function removeMcpConfig(): void { const filePath = path.join(os.homedir(), '.claude.json'); const config = readJson(filePath); if (!config?.mcpServers?.codegraph) return; delete config.mcpServers.codegraph; // Clean up empty mcpServers object if (Object.keys(config.mcpServers).length === 0) { delete config.mcpServers; } writeJson(filePath, config); } /** * Remove CodeGraph permissions and hooks from ~/.claude/settings.json */ function removeSettings(): void { const filePath = path.join(os.homedir(), '.claude', 'settings.json'); const settings = readJson(filePath); if (!settings) return; let changed = false; // Remove codegraph permissions if (Array.isArray(settings.permissions?.allow)) { const before = settings.permissions.allow.length; settings.permissions.allow = settings.permissions.allow.filter( (p: string) => !p.startsWith('mcp__codegraph__') ); if (settings.permissions.allow.length !== before) changed = true; // Clean up empty allow array if (settings.permissions.allow.length === 0) { delete settings.permissions.allow; } // Clean up empty permissions object if (Object.keys(settings.permissions).length === 0) { delete settings.permissions; } } // Remove codegraph hooks if (settings.hooks) { for (const event of Object.keys(settings.hooks)) { if (!Array.isArray(settings.hooks[event])) continue; const before = settings.hooks[event].length; settings.hooks[event] = settings.hooks[event].filter((entry: any) => { const json = JSON.stringify(entry); return !json.includes('codegraph mark-dirty') && !json.includes('codegraph sync-if-dirty'); }); if (settings.hooks[event].length !== before) changed = true; // Clean up empty event arrays if (settings.hooks[event].length === 0) { delete settings.hooks[event]; } } // Clean up empty hooks object if (Object.keys(settings.hooks).length === 0) { delete settings.hooks; } } if (changed) { writeJson(filePath, settings); } } /** * Remove CodeGraph section from ~/.claude/CLAUDE.md */ function removeClaudeMd(): void { const filePath = path.join(os.homedir(), '.claude', 'CLAUDE.md'); try { if (!fs.existsSync(filePath)) return; let content = fs.readFileSync(filePath, 'utf-8'); // Remove marked section const startIdx = content.indexOf(CODEGRAPH_SECTION_START); const endIdx = content.indexOf(CODEGRAPH_SECTION_END); if (startIdx !== -1 && endIdx > startIdx) { const before = content.substring(0, startIdx).trimEnd(); const after = content.substring(endIdx + CODEGRAPH_SECTION_END.length).trimStart(); content = before + (before && after ? '\n\n' : '') + after; if (content.trim() === '') { // File is empty after removing section — delete it fs.unlinkSync(filePath); } else { fs.writeFileSync(filePath, content.trim() + '\n'); } } } catch { // Never fail } } // Run cleanup — never throw try { removeMcpConfig(); } catch { /* ignore */ } try { removeSettings(); } catch { /* ignore */ } try { removeClaudeMd(); } catch { /* ignore */ }