index.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. /**
  2. * CodeGraph Interactive Installer
  3. *
  4. * Provides a beautiful interactive CLI experience for setting up CodeGraph
  5. * with Claude Code.
  6. */
  7. import { execSync } from 'child_process';
  8. import { showBanner, showNextSteps, success, error, info, chalk } from './banner';
  9. import { promptInstallLocation, promptAutoAllow, promptConfirm, InstallLocation } from './prompts';
  10. import { writeMcpConfig, writePermissions, writeClaudeMd, writeHooks, hasMcpConfig, hasPermissions, hasHooks } from './config-writer';
  11. /**
  12. * Format a number with commas
  13. */
  14. function formatNumber(n: number): string {
  15. return n.toLocaleString();
  16. }
  17. /**
  18. * Run the interactive installer
  19. */
  20. export async function runInstaller(): Promise<void> {
  21. // Show the banner
  22. showBanner();
  23. try {
  24. // Step 1: Install codegraph globally (with user consent).
  25. // The global install is needed because Claude Code hooks and the MCP server
  26. // invoke `codegraph` by name — the temporary npx binary vanishes when npx exits.
  27. console.log(chalk.bold(' Install codegraph globally?') + chalk.dim(' (Required for hooks & MCP server)'));
  28. console.log();
  29. const shouldInstallGlobally = await promptConfirm('Install globally via npm', true);
  30. if (shouldInstallGlobally) {
  31. console.log(chalk.dim(' Installing codegraph globally...'));
  32. try {
  33. execSync('npm install -g @colbymchenry/codegraph', { stdio: 'pipe' });
  34. success('Installed codegraph command globally');
  35. } catch {
  36. info('Could not install globally (permission denied)');
  37. info('Try: sudo npm install -g @colbymchenry/codegraph');
  38. }
  39. } else {
  40. info('Skipped global install — hooks and MCP server may not work without it');
  41. info('You can install later: npm install -g @colbymchenry/codegraph');
  42. }
  43. console.log();
  44. // Step 2: Ask for installation location
  45. const location = await promptInstallLocation();
  46. console.log();
  47. // Step 3: Write MCP configuration (always uses npx for reliability)
  48. const alreadyHasMcp = hasMcpConfig(location);
  49. writeMcpConfig(location);
  50. if (alreadyHasMcp) {
  51. success(`Updated MCP server in ${location === 'global' ? '~/.claude.json' : './.claude.json'}`);
  52. } else {
  53. success(`Added MCP server to ${location === 'global' ? '~/.claude.json' : './.claude.json'}`);
  54. }
  55. // Step 4: Ask about auto-allow permissions
  56. const autoAllow = await promptAutoAllow();
  57. console.log();
  58. if (autoAllow) {
  59. const alreadyHasPerms = hasPermissions(location);
  60. writePermissions(location);
  61. if (alreadyHasPerms) {
  62. success(`Updated permissions in ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
  63. } else {
  64. success(`Added permissions to ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
  65. }
  66. }
  67. // Step 5: Write auto-sync hooks
  68. const alreadyHasHooks = hasHooks(location);
  69. writeHooks(location);
  70. if (alreadyHasHooks) {
  71. success(`Updated auto-sync hooks in ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
  72. } else {
  73. success(`Added auto-sync hooks to ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
  74. }
  75. // Step 6: Write CLAUDE.md instructions
  76. const claudeMdResult = writeClaudeMd(location);
  77. const claudeMdPath = location === 'global' ? '~/.claude/CLAUDE.md' : './.claude/CLAUDE.md';
  78. if (claudeMdResult.created) {
  79. success(`Created ${claudeMdPath} with CodeGraph instructions`);
  80. } else if (claudeMdResult.updated) {
  81. success(`Updated CodeGraph section in ${claudeMdPath}`);
  82. } else {
  83. success(`Added CodeGraph instructions to ${claudeMdPath}`);
  84. }
  85. // Step 7: For local install, initialize the project
  86. if (location === 'local') {
  87. await initializeLocalProject();
  88. }
  89. // Show next steps
  90. showNextSteps(location);
  91. } catch (err) {
  92. console.log();
  93. if (err instanceof Error && err.message.includes('readline was closed')) {
  94. // User cancelled with Ctrl+C
  95. console.log(chalk.dim(' Installation cancelled.'));
  96. } else {
  97. error(`Installation failed: ${err instanceof Error ? err.message : String(err)}`);
  98. }
  99. process.exit(1);
  100. }
  101. }
  102. /**
  103. * Initialize CodeGraph in the current project (for local installs)
  104. */
  105. async function initializeLocalProject(): Promise<void> {
  106. const projectPath = process.cwd();
  107. // Lazy-load CodeGraph (requires native modules)
  108. let CodeGraph: typeof import('../index').default;
  109. try {
  110. CodeGraph = (await import('../index')).default;
  111. } catch (err) {
  112. const msg = err instanceof Error ? err.message : String(err);
  113. error(`Could not load native modules: ${msg}`);
  114. info('Skipping project initialization. You can run "codegraph init -i" later.');
  115. info('If this persists, try a Node.js LTS version (20 or 22).');
  116. return;
  117. }
  118. // Check if already initialized
  119. if (CodeGraph.isInitialized(projectPath)) {
  120. info('CodeGraph already initialized in this project');
  121. return;
  122. }
  123. console.log();
  124. console.log(chalk.dim(' Initializing CodeGraph in current project...'));
  125. // Initialize CodeGraph
  126. const cg = await CodeGraph.init(projectPath);
  127. success('Created .codegraph/ directory');
  128. // Index the project
  129. const result = await cg.indexAll({
  130. onProgress: (progress) => {
  131. // Simple progress indicator
  132. const phaseNames: Record<string, string> = {
  133. scanning: 'Scanning files',
  134. parsing: 'Parsing code',
  135. storing: 'Storing data',
  136. resolving: 'Resolving refs',
  137. };
  138. const phaseName = phaseNames[progress.phase] || progress.phase;
  139. const percent = progress.total > 0 ? Math.round((progress.current / progress.total) * 100) : 0;
  140. process.stdout.write(`\r ${chalk.dim(phaseName)}... ${percent}% `);
  141. },
  142. });
  143. // Clear progress line
  144. process.stdout.write('\r' + ' '.repeat(50) + '\r');
  145. if (result.success) {
  146. success(`Indexed ${formatNumber(result.filesIndexed)} files (${formatNumber(result.nodesCreated)} symbols)`);
  147. } else {
  148. success(`Indexed ${formatNumber(result.filesIndexed)} files with ${result.errors.length} warnings`);
  149. }
  150. cg.close();
  151. }
  152. // Export for use in CLI
  153. export { InstallLocation };