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>
This commit is contained in:
Colby McHenry
2026-01-19 14:31:29 -06:00
co-authored by Claude Opus 4.5
parent 0a1b78854a
commit 9baf59e5f0
7 changed files with 584 additions and 5 deletions
+28
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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');
}