|
|
@@ -0,0 +1,151 @@
|
|
|
+#!/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 = '<!-- CODEGRAPH_START -->';
|
|
|
+const CODEGRAPH_SECTION_END = '<!-- CODEGRAPH_END -->';
|
|
|
+
|
|
|
+function readJson(filePath: string): Record<string, any> | 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<string, any>): 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 */ }
|