Sfoglia il codice sorgente

Add interactive CLI installer for Claude Code integration

- New `codegraph install` command and auto-run when invoked with no args
- Beautiful ASCII banner using figlet
- Interactive prompts for global (~/.claude) or local (./.claude) installation
- Writes MCP server config to claude.json
- Writes auto-allow permissions to settings.json
- For local installs: auto-initializes project, indexes, and installs git hooks

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Colby McHenry 5 mesi fa
parent
commit
9baf59e5f0

+ 28 - 4
package-lock.json

@@ -1,17 +1,18 @@
 {
-  "name": "codegraph",
-  "version": "0.1.5",
+  "name": "@colbymchenry/codegraph",
+  "version": "0.1.9",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
-      "name": "codegraph",
-      "version": "0.1.5",
+      "name": "@colbymchenry/codegraph",
+      "version": "0.1.9",
       "license": "MIT",
       "dependencies": {
         "@xenova/transformers": "^2.17.0",
         "better-sqlite3": "^11.0.0",
         "commander": "^14.0.2",
+        "figlet": "^1.8.0",
         "sqlite-vss": "^0.1.2",
         "tree-sitter": "^0.22.4",
         "tree-sitter-c": "^0.23.4",
@@ -33,6 +34,7 @@
       },
       "devDependencies": {
         "@types/better-sqlite3": "^7.6.0",
+        "@types/figlet": "^1.5.8",
         "@types/node": "^20.19.30",
         "typescript": "^5.0.0",
         "vitest": "^2.0.0"
@@ -879,6 +881,13 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/figlet": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/@types/figlet/-/figlet-1.7.0.tgz",
+      "integrity": "sha512-KwrT7p/8Eo3Op/HBSIwGXOsTZKYiM9NpWRBJ5sVjWP/SmlS+oxxRvJht/FNAtliJvja44N3ul1yATgohnVBV0Q==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/long": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
@@ -1464,6 +1473,21 @@
       "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
       "license": "MIT"
     },
+    "node_modules/figlet": {
+      "version": "1.9.4",
+      "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.9.4.tgz",
+      "integrity": "sha512-uN6QE+TrzTAHC1IWTyrc4FfGo2KH/82J8Jl1tyKB7+z5DBit/m3D++Iu5lg91qJMnQQ3vpJrj5gxcK/pk4R9tQ==",
+      "license": "MIT",
+      "dependencies": {
+        "commander": "^14.0.0"
+      },
+      "bin": {
+        "figlet": "bin/index.js"
+      },
+      "engines": {
+        "node": ">= 17.0.0"
+      }
+    },
     "node_modules/file-uri-to-path": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",

+ 3 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "@colbymchenry/codegraph",
-  "version": "0.1.9",
+  "version": "0.2.0",
   "description": "A local-first code intelligence system that builds a semantic knowledge graph from any codebase",
   "main": "dist/index.js",
   "types": "dist/index.d.ts",
@@ -31,6 +31,7 @@
   "license": "MIT",
   "dependencies": {
     "@xenova/transformers": "^2.17.0",
+    "figlet": "^1.8.0",
     "better-sqlite3": "^11.0.0",
     "commander": "^14.0.2",
     "sqlite-vss": "^0.1.2",
@@ -51,6 +52,7 @@
   },
   "devDependencies": {
     "@types/better-sqlite3": "^7.6.0",
+    "@types/figlet": "^1.5.8",
     "@types/node": "^20.19.30",
     "typescript": "^5.0.0",
     "vitest": "^2.0.0"

+ 28 - 0
src/bin/codegraph.ts

@@ -5,6 +5,8 @@
  * Command-line interface for CodeGraph code intelligence.
  *
  * Usage:
+ *   codegraph                    Run interactive installer (when no args)
+ *   codegraph install            Run interactive installer
  *   codegraph init [path]        Initialize CodeGraph in a project
  *   codegraph index [path]       Index all files in the project
  *   codegraph sync [path]        Sync changes since last index
@@ -20,6 +22,20 @@ import * as path from 'path';
 import * as fs from 'fs';
 import CodeGraph from '../index';
 import type { IndexProgress } from '../index';
+import { runInstaller } from '../installer';
+
+// Check if running with no arguments - run installer
+if (process.argv.length === 2) {
+  runInstaller().catch((err) => {
+    console.error('Installation failed:', err.message);
+    process.exit(1);
+  });
+} else {
+  // Normal CLI flow
+  main();
+}
+
+function main() {
 
 const program = new Command();
 
@@ -721,5 +737,17 @@ program
     }
   });
 
+/**
+ * codegraph install
+ */
+program
+  .command('install')
+  .description('Run interactive installer for Claude Code integration')
+  .action(async () => {
+    await runInstaller();
+  });
+
 // Parse and run
 program.parse();
+
+} // end main()

+ 129 - 0
src/installer/banner.ts

@@ -0,0 +1,129 @@
+/**
+ * Banner and branding for the CodeGraph installer
+ */
+
+import * as figlet from 'figlet';
+import * as path from 'path';
+import * as fs from 'fs';
+
+// =============================================================================
+// ANSI Color Helpers (same pattern as CLI to avoid chalk ESM issues)
+// =============================================================================
+
+const colors = {
+  reset: '\x1b[0m',
+  bold: '\x1b[1m',
+  dim: '\x1b[2m',
+  red: '\x1b[31m',
+  green: '\x1b[32m',
+  yellow: '\x1b[33m',
+  blue: '\x1b[34m',
+  magenta: '\x1b[35m',
+  cyan: '\x1b[36m',
+  white: '\x1b[37m',
+  gray: '\x1b[90m',
+};
+
+export const chalk = {
+  bold: (s: string) => `${colors.bold}${s}${colors.reset}`,
+  dim: (s: string) => `${colors.dim}${s}${colors.reset}`,
+  red: (s: string) => `${colors.red}${s}${colors.reset}`,
+  green: (s: string) => `${colors.green}${s}${colors.reset}`,
+  yellow: (s: string) => `${colors.yellow}${s}${colors.reset}`,
+  blue: (s: string) => `${colors.blue}${s}${colors.reset}`,
+  magenta: (s: string) => `${colors.magenta}${s}${colors.reset}`,
+  cyan: (s: string) => `${colors.cyan}${s}${colors.reset}`,
+  white: (s: string) => `${colors.white}${s}${colors.reset}`,
+  gray: (s: string) => `${colors.gray}${s}${colors.reset}`,
+};
+
+/**
+ * Get the package version
+ */
+function getVersion(): string {
+  try {
+    const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
+    const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
+    return packageJson.version;
+  } catch {
+    return '0.0.0';
+  }
+}
+
+/**
+ * Display the CodeGraph banner
+ */
+export function showBanner(): void {
+  // Generate ASCII art using figlet
+  let banner: string;
+  try {
+    banner = figlet.textSync('CODEGRAPH', {
+      font: 'ANSI Shadow',
+      horizontalLayout: 'default',
+    });
+  } catch {
+    // Fallback if figlet fails
+    banner = `
+   ██████╗ ██████╗ ██████╗ ███████╗ ██████╗ ██████╗  █████╗ ██████╗ ██╗  ██╗
+  ██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔════╝ ██╔══██╗██╔══██╗██╔══██╗██║  ██║
+  ██║     ██║   ██║██║  ██║█████╗  ██║  ███╗██████╔╝███████║██████╔╝███████║
+  ██║     ██║   ██║██║  ██║██╔══╝  ██║   ██║██╔══██╗██╔══██║██╔═══╝ ██╔══██║
+  ╚██████╗╚██████╔╝██████╔╝███████╗╚██████╔╝██║  ██║██║  ██║██║     ██║  ██║
+   ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝╚═╝     ╚═╝  ╚═╝
+`;
+  }
+
+  console.log();
+  console.log(chalk.cyan(banner));
+  console.log();
+  console.log(`  ${chalk.bold('CodeGraph')} v${getVersion()}`);
+  console.log('  Semantic code intelligence for Claude Code');
+  console.log(chalk.dim('  Created by: Colby McHenry'));
+  console.log();
+}
+
+/**
+ * Show success checkmark
+ */
+export function success(message: string): void {
+  console.log(chalk.green('  ✓') + ' ' + message);
+}
+
+/**
+ * Show error message
+ */
+export function error(message: string): void {
+  console.log(chalk.red('  ✗') + ' ' + message);
+}
+
+/**
+ * Show info message
+ */
+export function info(message: string): void {
+  console.log(chalk.blue('  ℹ') + ' ' + message);
+}
+
+/**
+ * Show warning message
+ */
+export function warn(message: string): void {
+  console.log(chalk.yellow('  ⚠') + ' ' + message);
+}
+
+/**
+ * Show the "next steps" section after installation
+ */
+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') {
+    console.log(chalk.dim('  Quick start:'));
+    console.log(chalk.dim('    cd your-project'));
+    console.log(chalk.cyan('    codegraph init -i'));
+  } else {
+    console.log(chalk.dim('  CodeGraph is ready to use in this project!'));
+  }
+  console.log();
+}

+ 171 - 0
src/installer/config-writer.ts

@@ -0,0 +1,171 @@
+/**
+ * Config file writing for the CodeGraph installer
+ * Writes to claude.json and settings.json
+ */
+
+import * as fs from 'fs';
+import * as path from 'path';
+import * as os from 'os';
+import { InstallLocation } from './prompts';
+
+/**
+ * Get the path to the Claude config directory
+ */
+function getClaudeConfigDir(location: InstallLocation): string {
+  if (location === 'global') {
+    return path.join(os.homedir(), '.claude');
+  }
+  return path.join(process.cwd(), '.claude');
+}
+
+/**
+ * Get the path to the claude.json file
+ * - Global: ~/.claude.json (root level)
+ * - Local: ./.claude.json (project root)
+ */
+function getClaudeJsonPath(location: InstallLocation): string {
+  if (location === 'global') {
+    return path.join(os.homedir(), '.claude.json');
+  }
+  return path.join(process.cwd(), '.claude.json');
+}
+
+/**
+ * Get the path to the settings.json file
+ * - Global: ~/.claude/settings.json
+ * - Local: ./.claude/settings.json
+ */
+function getSettingsJsonPath(location: InstallLocation): string {
+  const configDir = getClaudeConfigDir(location);
+  return path.join(configDir, 'settings.json');
+}
+
+/**
+ * Read a JSON file, returning an empty object if it doesn't exist
+ */
+function readJsonFile(filePath: string): Record<string, any> {
+  try {
+    if (fs.existsSync(filePath)) {
+      const content = fs.readFileSync(filePath, 'utf-8');
+      return JSON.parse(content);
+    }
+  } catch {
+    // Ignore parse errors, return empty object
+  }
+  return {};
+}
+
+/**
+ * Write a JSON file, creating parent directories if needed
+ */
+function writeJsonFile(filePath: string, data: Record<string, any>): void {
+  const dir = path.dirname(filePath);
+  if (!fs.existsSync(dir)) {
+    fs.mkdirSync(dir, { recursive: true });
+  }
+  fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
+}
+
+/**
+ * Get the MCP server configuration for the given location
+ */
+function getMcpServerConfig(location: InstallLocation): Record<string, any> {
+  if (location === 'global') {
+    // Global: use 'codegraph' command directly (assumes globally installed)
+    return {
+      type: 'stdio',
+      command: 'codegraph',
+      args: ['serve', '--mcp'],
+    };
+  }
+  // Local: use npx to run the package
+  return {
+    type: 'stdio',
+    command: 'npx',
+    args: ['@colbymchenry/codegraph', 'serve', '--mcp'],
+  };
+}
+
+/**
+ * Write the MCP server configuration to claude.json
+ */
+export function writeMcpConfig(location: InstallLocation): void {
+  const claudeJsonPath = getClaudeJsonPath(location);
+  const config = readJsonFile(claudeJsonPath);
+
+  // Ensure mcpServers object exists
+  if (!config.mcpServers) {
+    config.mcpServers = {};
+  }
+
+  // Add or update codegraph server
+  config.mcpServers.codegraph = getMcpServerConfig(location);
+
+  writeJsonFile(claudeJsonPath, config);
+}
+
+/**
+ * Get the list of permissions for CodeGraph tools
+ */
+function getCodeGraphPermissions(): string[] {
+  return [
+    'mcp__codegraph__codegraph_search',
+    'mcp__codegraph__codegraph_context',
+    'mcp__codegraph__codegraph_callers',
+    'mcp__codegraph__codegraph_callees',
+    'mcp__codegraph__codegraph_impact',
+    'mcp__codegraph__codegraph_node',
+    'mcp__codegraph__codegraph_status',
+  ];
+}
+
+/**
+ * Write permissions to settings.json
+ */
+export function writePermissions(location: InstallLocation): void {
+  const settingsPath = getSettingsJsonPath(location);
+  const settings = readJsonFile(settingsPath);
+
+  // Ensure permissions object exists
+  if (!settings.permissions) {
+    settings.permissions = {};
+  }
+
+  // Ensure allow array exists
+  if (!Array.isArray(settings.permissions.allow)) {
+    settings.permissions.allow = [];
+  }
+
+  // Add CodeGraph permissions (avoiding duplicates)
+  const codegraphPermissions = getCodeGraphPermissions();
+  for (const permission of codegraphPermissions) {
+    if (!settings.permissions.allow.includes(permission)) {
+      settings.permissions.allow.push(permission);
+    }
+  }
+
+  writeJsonFile(settingsPath, settings);
+}
+
+/**
+ * Check if MCP config already exists for CodeGraph
+ */
+export function hasMcpConfig(location: InstallLocation): boolean {
+  const claudeJsonPath = getClaudeJsonPath(location);
+  const config = readJsonFile(claudeJsonPath);
+  return !!config.mcpServers?.codegraph;
+}
+
+/**
+ * Check if permissions already exist for CodeGraph
+ */
+export function hasPermissions(location: InstallLocation): boolean {
+  const settingsPath = getSettingsJsonPath(location);
+  const settings = readJsonFile(settingsPath);
+  const permissions = settings.permissions?.allow;
+  if (!Array.isArray(permissions)) {
+    return false;
+  }
+  // Check if at least one CodeGraph permission exists
+  return permissions.some((p: string) => p.startsWith('mcp__codegraph__'));
+}

+ 132 - 0
src/installer/index.ts

@@ -0,0 +1,132 @@
+/**
+ * CodeGraph Interactive Installer
+ *
+ * Provides a beautiful interactive CLI experience for setting up CodeGraph
+ * with Claude Code.
+ */
+
+import { showBanner, showNextSteps, success, error, info, chalk } from './banner';
+import { promptInstallLocation, promptAutoAllow, InstallLocation } from './prompts';
+import { writeMcpConfig, writePermissions, hasMcpConfig, hasPermissions } from './config-writer';
+import CodeGraph from '../index';
+
+/**
+ * Format a number with commas
+ */
+function formatNumber(n: number): string {
+  return n.toLocaleString();
+}
+
+/**
+ * Run the interactive installer
+ */
+export async function runInstaller(): Promise<void> {
+  // Show the banner
+  showBanner();
+
+  try {
+    // Step 1: Ask for installation location
+    const location = await promptInstallLocation();
+    console.log();
+
+    // Step 2: Write MCP configuration
+    const alreadyHasMcp = hasMcpConfig(location);
+    writeMcpConfig(location);
+
+    if (alreadyHasMcp) {
+      success(`Updated MCP server in ${location === 'global' ? '~/.claude.json' : './.claude.json'}`);
+    } else {
+      success(`Added MCP server to ${location === 'global' ? '~/.claude.json' : './.claude.json'}`);
+    }
+
+    // Step 3: Ask about auto-allow permissions
+    const autoAllow = await promptAutoAllow();
+    console.log();
+
+    if (autoAllow) {
+      const alreadyHasPerms = hasPermissions(location);
+      writePermissions(location);
+
+      if (alreadyHasPerms) {
+        success(`Updated permissions in ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
+      } else {
+        success(`Added permissions to ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
+      }
+    }
+
+    // Step 4: For local install, initialize the project
+    if (location === 'local') {
+      await initializeLocalProject();
+    }
+
+    // Show next steps
+    showNextSteps(location);
+  } catch (err) {
+    console.log();
+    if (err instanceof Error && err.message.includes('readline was closed')) {
+      // User cancelled with Ctrl+C
+      console.log(chalk.dim('  Installation cancelled.'));
+    } else {
+      error(`Installation failed: ${err instanceof Error ? err.message : String(err)}`);
+    }
+    process.exit(1);
+  }
+}
+
+/**
+ * Initialize CodeGraph in the current project (for local installs)
+ */
+async function initializeLocalProject(): Promise<void> {
+  const projectPath = process.cwd();
+
+  // Check if already initialized
+  if (CodeGraph.isInitialized(projectPath)) {
+    info('CodeGraph already initialized in this project');
+    return;
+  }
+
+  console.log();
+  console.log(chalk.dim('  Initializing CodeGraph in current project...'));
+
+  // Initialize CodeGraph
+  const cg = await CodeGraph.init(projectPath);
+  success('Created .codegraph/ directory');
+
+  // Index the project
+  const result = await cg.indexAll({
+    onProgress: (progress) => {
+      // Simple progress indicator
+      const phaseNames: Record<string, string> = {
+        scanning: 'Scanning files',
+        parsing: 'Parsing code',
+        storing: 'Storing data',
+        resolving: 'Resolving refs',
+      };
+      const phaseName = phaseNames[progress.phase] || progress.phase;
+      const percent = progress.total > 0 ? Math.round((progress.current / progress.total) * 100) : 0;
+      process.stdout.write(`\r  ${chalk.dim(phaseName)}... ${percent}%   `);
+    },
+  });
+
+  // Clear progress line
+  process.stdout.write('\r' + ' '.repeat(50) + '\r');
+
+  if (result.success) {
+    success(`Indexed ${formatNumber(result.filesIndexed)} files (${formatNumber(result.nodesCreated)} symbols)`);
+  } else {
+    success(`Indexed ${formatNumber(result.filesIndexed)} files with ${result.errors.length} warnings`);
+  }
+
+  // Install git hooks if this is a git repository
+  if (cg.isGitRepository()) {
+    const hookResult = cg.installGitHooks();
+    if (hookResult.success) {
+      success('Installed git post-commit hook');
+    }
+  }
+
+  cg.close();
+}
+
+// Export for use in CLI
+export { InstallLocation };

+ 93 - 0
src/installer/prompts.ts

@@ -0,0 +1,93 @@
+/**
+ * User prompts for the CodeGraph installer
+ * Uses built-in readline to avoid ESM issues with inquirer
+ */
+
+import * as readline from 'readline';
+import { chalk } from './banner';
+
+export type InstallLocation = 'global' | 'local';
+
+/**
+ * Create a readline interface for prompts
+ */
+function createInterface(): readline.Interface {
+  return readline.createInterface({
+    input: process.stdin,
+    output: process.stdout,
+  });
+}
+
+/**
+ * Prompt the user with a question and return their answer
+ */
+function prompt(rl: readline.Interface, question: string): Promise<string> {
+  return new Promise((resolve) => {
+    rl.question(question, (answer) => {
+      resolve(answer.trim());
+    });
+  });
+}
+
+/**
+ * Prompt for installation location (global or local)
+ */
+export async function promptInstallLocation(): Promise<InstallLocation> {
+  const rl = createInterface();
+
+  console.log(chalk.bold('  Where would you like to install?'));
+  console.log();
+  console.log('  1) Global (~/.claude) - available in all projects');
+  console.log('  2) Local (./.claude) - this project only');
+  console.log();
+
+  const answer = await prompt(rl, '  Choice [1]: ');
+  rl.close();
+
+  // Default to '1' if empty, parse the answer
+  const choice = answer === '' ? '1' : answer;
+
+  if (choice === '2') {
+    return 'local';
+  }
+  return 'global';
+}
+
+/**
+ * Prompt for auto-allow permissions
+ */
+export async function promptAutoAllow(): Promise<boolean> {
+  const rl = createInterface();
+
+  console.log();
+  console.log(chalk.bold('  Auto-allow CodeGraph commands?') + chalk.dim(' (Skips permission prompts)'));
+  console.log();
+  console.log('  1) Yes - auto-approve all codegraph_* tools');
+  console.log('  2) No - ask for permission each time');
+  console.log();
+
+  const answer = await prompt(rl, '  Choice [1]: ');
+  rl.close();
+
+  // Default to '1' if empty
+  const choice = answer === '' ? '1' : answer;
+
+  return choice !== '2';
+}
+
+/**
+ * Prompt for confirmation (yes/no)
+ */
+export async function promptConfirm(message: string, defaultYes: boolean = true): Promise<boolean> {
+  const rl = createInterface();
+
+  const defaultStr = defaultYes ? 'Y/n' : 'y/N';
+  const answer = await prompt(rl, `  ${message} [${defaultStr}]: `);
+  rl.close();
+
+  if (answer === '') {
+    return defaultYes;
+  }
+
+  return answer.toLowerCase().startsWith('y');
+}