From 9baf59e5f00f61280a755bca2a7ce7d69bb0d9f1 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Mon, 19 Jan 2026 14:31:29 -0600 Subject: [PATCH] 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 --- package-lock.json | 32 +++++- package.json | 4 +- src/bin/codegraph.ts | 28 ++++++ src/installer/banner.ts | 129 +++++++++++++++++++++++++ src/installer/config-writer.ts | 171 +++++++++++++++++++++++++++++++++ src/installer/index.ts | 132 +++++++++++++++++++++++++ src/installer/prompts.ts | 93 ++++++++++++++++++ 7 files changed, 584 insertions(+), 5 deletions(-) create mode 100644 src/installer/banner.ts create mode 100644 src/installer/config-writer.ts create mode 100644 src/installer/index.ts create mode 100644 src/installer/prompts.ts diff --git a/package-lock.json b/package-lock.json index 7f493b4..86e2427 100644 --- a/package-lock.json +++ b/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", diff --git a/package.json b/package.json index 55ce572..c9fee02 100644 --- a/package.json +++ b/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" diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index 1575df8..5a19461 100644 --- a/src/bin/codegraph.ts +++ b/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() diff --git a/src/installer/banner.ts b/src/installer/banner.ts new file mode 100644 index 0000000..952f6e0 --- /dev/null +++ b/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(); +} diff --git a/src/installer/config-writer.ts b/src/installer/config-writer.ts new file mode 100644 index 0000000..0506239 --- /dev/null +++ b/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 { + 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): 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 { + 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__')); +} diff --git a/src/installer/index.ts b/src/installer/index.ts new file mode 100644 index 0000000..6542aa4 --- /dev/null +++ b/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 { + // 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 { + 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 = { + 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 }; diff --git a/src/installer/prompts.ts b/src/installer/prompts.ts new file mode 100644 index 0000000..fa22daf --- /dev/null +++ b/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 { + return new Promise((resolve) => { + rl.question(question, (answer) => { + resolve(answer.trim()); + }); + }); +} + +/** + * Prompt for installation location (global or local) + */ +export async function promptInstallLocation(): Promise { + 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 { + 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 { + 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'); +}