Files
codegraph/src/installer/index.ts
T
Colby McHenryandClaude Opus 4.6 5a185eb736 fix: Add telemetry opt-out via installer prompt and env var
Sentry error reporting can now be disabled by:
1. Declining during the interactive installer (sets CODEGRAPH_TELEMETRY=off
   in the MCP server config env)
2. Setting CODEGRAPH_TELEMETRY=off in your shell environment

README updated with a Telemetry section documenting what is collected
and how to opt out.

Closes #68

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:14:33 -05:00

193 lines
6.6 KiB
TypeScript

/**
* CodeGraph Interactive Installer
*
* Provides a beautiful interactive CLI experience for setting up CodeGraph
* with Claude Code.
*/
import { execSync } from 'child_process';
import { showBanner, showNextSteps, success, error, info, chalk } from './banner';
import { promptInstallLocation, promptAutoAllow, promptConfirm, InstallLocation } from './prompts';
import { writeMcpConfig, writePermissions, writeClaudeMd, writeHooks, hasMcpConfig, hasPermissions, hasHooks } from './config-writer';
/**
* 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: Install codegraph globally (with user consent).
// The global install is needed because Claude Code hooks and the MCP server
// invoke `codegraph` by name — the temporary npx binary vanishes when npx exits.
console.log(chalk.bold(' Install codegraph globally?') + chalk.dim(' (Required for hooks & MCP server)'));
console.log();
const shouldInstallGlobally = await promptConfirm('Install globally via npm', true);
if (shouldInstallGlobally) {
console.log(chalk.dim(' Installing codegraph globally...'));
try {
execSync('npm install -g @colbymchenry/codegraph', { stdio: 'pipe' });
success('Installed codegraph command globally');
} catch {
info('Could not install globally (permission denied)');
info('Try: sudo npm install -g @colbymchenry/codegraph');
}
} else {
info('Skipped global install — hooks and MCP server may not work without it');
info('You can install later: npm install -g @colbymchenry/codegraph');
}
console.log();
// Step 2: Ask for installation location
const location = await promptInstallLocation();
console.log();
// Step 3: Ask about auto-allow permissions
const autoAllow = await promptAutoAllow();
console.log();
// Step 4: Ask about anonymous error reporting
console.log(chalk.bold(' Send anonymous error reports?') + chalk.dim(' (Helps fix bugs — no source code collected)'));
console.log();
const enableTelemetry = await promptConfirm('Enable anonymous error reporting', true);
if (!enableTelemetry) {
info('Telemetry disabled');
} else {
success('Anonymous error reporting enabled');
}
console.log();
// Step 5: Write MCP configuration (includes telemetry env if opted out)
const alreadyHasMcp = hasMcpConfig(location);
writeMcpConfig(location, { telemetry: enableTelemetry });
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'}`);
}
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 6: Write auto-sync hooks
const alreadyHasHooks = hasHooks(location);
writeHooks(location);
if (alreadyHasHooks) {
success(`Updated auto-sync hooks in ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
} else {
success(`Added auto-sync hooks to ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
}
// Step 7: Write CLAUDE.md instructions
const claudeMdResult = writeClaudeMd(location);
const claudeMdPath = location === 'global' ? '~/.claude/CLAUDE.md' : './.claude/CLAUDE.md';
if (claudeMdResult.created) {
success(`Created ${claudeMdPath} with CodeGraph instructions`);
} else if (claudeMdResult.updated) {
success(`Updated CodeGraph section in ${claudeMdPath}`);
} else {
success(`Added CodeGraph instructions to ${claudeMdPath}`);
}
// Step 7: 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();
// Lazy-load CodeGraph (requires native modules)
let CodeGraph: typeof import('../index').default;
try {
CodeGraph = (await import('../index')).default;
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
error(`Could not load native modules: ${msg}`);
info('Skipping project initialization. You can run "codegraph init -i" later.');
info('If this persists, try a Node.js LTS version (20 or 22).');
return;
}
// 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`);
}
cg.close();
}
// Export for use in CLI
export { InstallLocation };