Explorar o código

Merge pull request #45 from colbymchenry/fix/silent-failed-install

fix: Stop silent install failures — always run npm install -g, add preuninstall cleanup
Colby Mchenry hai 4 meses
pai
achega
7f516b9308

+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "@colbymchenry/codegraph",
-  "version": "0.5.5",
+  "version": "0.6.2",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@colbymchenry/codegraph",
-      "version": "0.5.5",
+      "version": "0.6.2",
       "hasInstallScript": true,
       "license": "MIT",
       "dependencies": {

+ 2 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "@colbymchenry/codegraph",
-  "version": "0.5.5",
+  "version": "0.6.2",
   "description": "Supercharge Claude Code with semantic code intelligence. 30% fewer tokens, 25% fewer tool calls, 100% local.",
   "main": "dist/index.js",
   "types": "dist/index.d.ts",
@@ -15,6 +15,7 @@
   "scripts": {
     "build": "tsc && npm run copy-assets",
     "postinstall": "node scripts/postinstall.js",
+    "preuninstall": "node dist/bin/uninstall.js",
     "copy-assets": "node -e \"const fs=require('fs');fs.mkdirSync('dist/db',{recursive:true});fs.copyFileSync('src/db/schema.sql','dist/db/schema.sql');fs.mkdirSync('dist/extraction/wasm',{recursive:true});fs.readdirSync('src/extraction/wasm').filter(f=>f.endsWith('.wasm')).forEach(f=>fs.copyFileSync('src/extraction/wasm/'+f,'dist/extraction/wasm/'+f))\"",
     "dev": "tsc --watch",
     "cli": "npm run build && node dist/bin/codegraph.js",

+ 6 - 6
src/bin/codegraph.ts

@@ -1015,17 +1015,17 @@ program
         process.exit(0);
       }
 
-      // Spawn `codegraph sync` as a detached background process
-      // so this hook exits immediately and doesn't block Claude Code
-      const isWindows = process.platform === 'win32';
+      // Spawn sync as a detached background process
+      // so this hook exits immediately and doesn't block Claude Code.
+      // Uses process.argv[0]/[1] (e.g. node /path/to/codegraph.js) so it
+      // works whether invoked via global install, npx, or directly.
       const child = spawn(
-        isWindows ? 'codegraph' : process.argv[0]!,
-        isWindows ? ['sync', '--quiet', projectRoot!] : [process.argv[1]!, 'sync', '--quiet', projectRoot!],
+        process.argv[0]!,
+        [process.argv[1]!, 'sync', '--quiet', projectRoot!],
         {
           detached: true,
           stdio: 'ignore',
           windowsHide: true,
-          shell: isWindows,
         }
       );
       child.unref();

+ 151 - 0
src/bin/uninstall.ts

@@ -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 */ }

+ 5 - 3
src/installer/banner.ts

@@ -113,16 +113,18 @@ export function warn(message: string): void {
 /**
  * Show the "next steps" section after installation
  */
-export function showNextSteps(location: 'global' | 'local', useNpx?: boolean): void {
+export function showNextSteps(location: 'global' | 'local'): void {
   console.log();
   console.log(chalk.bold('  Done!') + ' Restart Claude Code to use CodeGraph.');
   console.log();
 
   if (location === 'global') {
-    const cmd = useNpx ? 'npx @colbymchenry/codegraph' : 'codegraph';
     console.log(chalk.dim('  Quick start:'));
     console.log(chalk.dim('    cd your-project'));
-    console.log(chalk.cyan(`    ${cmd} init -i`));
+    console.log(chalk.cyan('    codegraph init -i'));
+    console.log();
+    console.log(chalk.dim('  To uninstall:'));
+    console.log(chalk.dim('    npm uninstall -g @colbymchenry/codegraph'));
   } else {
     console.log(chalk.dim('  CodeGraph is ready to use in this project!'));
   }

+ 8 - 27
src/installer/config-writer.ts

@@ -98,32 +98,13 @@ function writeJsonFile(filePath: string, data: Record<string, any>): void {
 }
 
 /**
- * When true, all configs use `npx @colbymchenry/codegraph` instead of the
- * bare `codegraph` command.  Set by the installer when global install fails.
+ * Get the MCP server configuration
  */
-let useNpxFallback = false;
-
-export function setUseNpxFallback(value: boolean): void {
-  useNpxFallback = value;
-}
-
-/**
- * Get the MCP server configuration for the given location
- */
-function getMcpServerConfig(location: InstallLocation): Record<string, any> {
-  if (location === 'global' && !useNpxFallback) {
-    // Global: use 'codegraph' command directly (globally installed and in PATH)
-    return {
-      type: 'stdio',
-      command: 'codegraph',
-      args: ['serve', '--mcp'],
-    };
-  }
-  // Local or npx fallback: use npx to run the package
+function getMcpServerConfig(): Record<string, any> {
   return {
     type: 'stdio',
-    command: 'npx',
-    args: ['@colbymchenry/codegraph', 'serve', '--mcp'],
+    command: 'codegraph',
+    args: ['serve', '--mcp'],
   };
 }
 
@@ -140,7 +121,7 @@ export function writeMcpConfig(location: InstallLocation): void {
   }
 
   // Add or update codegraph server
-  config.mcpServers.codegraph = getMcpServerConfig(location);
+  config.mcpServers.codegraph = getMcpServerConfig();
 
   writeJsonFile(claudeJsonPath, config);
 }
@@ -221,8 +202,8 @@ export function hasPermissions(location: InstallLocation): boolean {
  * PostToolUse(Edit|Write) → mark-dirty (async, non-blocking)
  * Stop → sync-if-dirty (sync, ensures fresh index before next user turn)
  */
-function getHooksConfig(location: InstallLocation): Record<string, any> {
-  const command = (location === 'global' && !useNpxFallback) ? 'codegraph' : 'npx @colbymchenry/codegraph';
+function getHooksConfig(): Record<string, any> {
+  const command = 'codegraph';
 
   return {
     PostToolUse: [
@@ -277,7 +258,7 @@ export function writeHooks(location: InstallLocation): void {
     settings.hooks = {};
   }
 
-  const newHooks = getHooksConfig(location);
+  const newHooks = getHooksConfig();
 
   // For each hook event (PostToolUse, Stop), merge with existing entries
   for (const [event, newEntries] of Object.entries(newHooks)) {

+ 12 - 28
src/installer/index.ts

@@ -8,7 +8,7 @@
 import { execSync } from 'child_process';
 import { showBanner, showNextSteps, success, error, info, chalk } from './banner';
 import { promptInstallLocation, promptAutoAllow, InstallLocation } from './prompts';
-import { writeMcpConfig, writePermissions, writeClaudeMd, writeHooks, hasMcpConfig, hasPermissions, hasHooks, setUseNpxFallback } from './config-writer';
+import { writeMcpConfig, writePermissions, writeClaudeMd, writeHooks, hasMcpConfig, hasPermissions, hasHooks } from './config-writer';
 
 /**
  * Format a number with commas
@@ -25,40 +25,24 @@ export async function runInstaller(): Promise<void> {
   showBanner();
 
   try {
-    // Step 1: Check if codegraph is available (skip install if already there)
-    let codegraphAvailable = false;
+    // Step 1: Install codegraph globally.
+    // Always run npm install -g — we can't use `command -v codegraph` to check
+    // because npx puts a temporary binary in PATH that vanishes when npx exits.
+    console.log(chalk.dim('  Installing codegraph globally...'));
     try {
-      const checkCmd = process.platform === 'win32' ? 'where codegraph' : 'command -v codegraph';
-      execSync(checkCmd, { stdio: 'pipe' });
-      codegraphAvailable = true;
+      execSync('npm install -g @colbymchenry/codegraph', { stdio: 'pipe' });
+      success('Installed codegraph command globally');
     } catch {
-      // Not installed globally yet
-    }
-
-    if (!codegraphAvailable) {
-      console.log(chalk.dim('  Installing codegraph globally...'));
-      try {
-        execSync('npm install -g @colbymchenry/codegraph', { stdio: 'pipe' });
-        success('Installed codegraph command globally');
-        codegraphAvailable = true;
-      } catch {
-        // May fail if no permissions, but that's ok - npx still works
-        info('Could not install globally — will use npx instead');
-        info('(MCP server and hooks will use npx @colbymchenry/codegraph)');
-      }
-      console.log();
-    }
-
-    // If codegraph binary isn't in PATH, tell config-writer to use npx for everything
-    if (!codegraphAvailable) {
-      setUseNpxFallback(true);
+      info('Could not install globally (permission denied)');
+      info('Try: sudo npm install -g @colbymchenry/codegraph');
     }
+    console.log();
 
     // Step 2: Ask for installation location
     const location = await promptInstallLocation();
     console.log();
 
-    // Step 3: Write MCP configuration
+    // Step 3: Write MCP configuration (always uses npx for reliability)
     const alreadyHasMcp = hasMcpConfig(location);
     writeMcpConfig(location);
 
@@ -111,7 +95,7 @@ export async function runInstaller(): Promise<void> {
     }
 
     // Show next steps
-    showNextSteps(location, !codegraphAvailable);
+    showNextSteps(location);
   } catch (err) {
     console.log();
     if (err instanceof Error && err.message.includes('readline was closed')) {