codegraph.ts 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053
  1. #!/usr/bin/env node
  2. /**
  3. * CodeGraph CLI
  4. *
  5. * Command-line interface for CodeGraph code intelligence.
  6. *
  7. * Usage:
  8. * codegraph Run interactive installer (when no args)
  9. * codegraph install Run interactive installer
  10. * codegraph init [path] Initialize CodeGraph in a project
  11. * codegraph uninit [path] Remove CodeGraph from a project
  12. * codegraph index [path] Index all files in the project
  13. * codegraph sync [path] Sync changes since last index
  14. * codegraph status [path] Show index status
  15. * codegraph query <search> Search for symbols
  16. * codegraph files [options] Show project file structure
  17. * codegraph context <task> Build context for a task
  18. * codegraph mark-dirty [path] Mark project as needing sync (hooks)
  19. * codegraph sync-if-dirty [path] Sync if marked dirty (hooks)
  20. *
  21. * Note: Git hooks have been removed. CodeGraph sync is triggered automatically
  22. * through codegraph's Claude Code hooks integration.
  23. */
  24. import { Command } from 'commander';
  25. import * as path from 'path';
  26. import * as fs from 'fs';
  27. import { spawn } from 'child_process';
  28. import { getCodeGraphDir, findNearestCodeGraphRoot, isInitialized } from '../directory';
  29. import { initSentry, captureException } from '../sentry';
  30. // Lazy-load heavy modules (CodeGraph, runInstaller) to keep CLI startup fast.
  31. async function loadCodeGraph(): Promise<typeof import('../index')> {
  32. try {
  33. return await import('../index');
  34. } catch (err) {
  35. const msg = err instanceof Error ? err.message : String(err);
  36. console.error('\x1b[31m✗\x1b[0m Failed to load CodeGraph modules.');
  37. console.error(`\n Node: ${process.version} Platform: ${process.platform} ${process.arch}`);
  38. console.error(`\n Error: ${msg}`);
  39. console.error('\n Try reinstalling with: npm install -g @colbymchenry/codegraph\n');
  40. process.exit(1);
  41. }
  42. }
  43. type IndexProgress = import('../index').IndexProgress;
  44. // Check if running with no arguments - run installer
  45. // Read version for Sentry release tag
  46. const pkgVersion = (() => {
  47. try {
  48. return JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf-8')).version;
  49. } catch { return undefined; }
  50. })();
  51. initSentry({ processName: 'codegraph-cli', version: pkgVersion });
  52. if (process.argv.length === 2) {
  53. import('../installer').then(({ runInstaller }) =>
  54. runInstaller()
  55. ).catch((err) => {
  56. captureException(err);
  57. console.error('Installation failed:', err instanceof Error ? err.message : String(err));
  58. process.exit(1);
  59. });
  60. } else {
  61. // Normal CLI flow
  62. main();
  63. }
  64. process.on('uncaughtException', (error) => {
  65. captureException(error);
  66. console.error('[CodeGraph] Uncaught exception:', error);
  67. });
  68. process.on('unhandledRejection', (reason) => {
  69. captureException(reason);
  70. console.error('[CodeGraph] Unhandled rejection:', reason);
  71. });
  72. function main() {
  73. const program = new Command();
  74. // Version from package.json
  75. const packageJson = JSON.parse(
  76. fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf-8')
  77. );
  78. // =============================================================================
  79. // ANSI Color Helpers (avoid chalk ESM issues)
  80. // =============================================================================
  81. const colors = {
  82. reset: '\x1b[0m',
  83. bold: '\x1b[1m',
  84. dim: '\x1b[2m',
  85. red: '\x1b[31m',
  86. green: '\x1b[32m',
  87. yellow: '\x1b[33m',
  88. blue: '\x1b[34m',
  89. cyan: '\x1b[36m',
  90. white: '\x1b[37m',
  91. gray: '\x1b[90m',
  92. };
  93. const chalk = {
  94. bold: (s: string) => `${colors.bold}${s}${colors.reset}`,
  95. dim: (s: string) => `${colors.dim}${s}${colors.reset}`,
  96. red: (s: string) => `${colors.red}${s}${colors.reset}`,
  97. green: (s: string) => `${colors.green}${s}${colors.reset}`,
  98. yellow: (s: string) => `${colors.yellow}${s}${colors.reset}`,
  99. blue: (s: string) => `${colors.blue}${s}${colors.reset}`,
  100. cyan: (s: string) => `${colors.cyan}${s}${colors.reset}`,
  101. white: (s: string) => `${colors.white}${s}${colors.reset}`,
  102. gray: (s: string) => `${colors.gray}${s}${colors.reset}`,
  103. };
  104. program
  105. .name('codegraph')
  106. .description('Code intelligence and knowledge graph for any codebase')
  107. .version(packageJson.version);
  108. // =============================================================================
  109. // Helper Functions
  110. // =============================================================================
  111. /**
  112. * Resolve project path from argument or current directory
  113. * Walks up parent directories to find nearest initialized CodeGraph project
  114. * (must have .codegraph/codegraph.db, not just .codegraph/lessons.db)
  115. */
  116. function resolveProjectPath(pathArg?: string): string {
  117. const absolutePath = path.resolve(pathArg || process.cwd());
  118. // If exact path is initialized (has codegraph.db), use it
  119. if (isInitialized(absolutePath)) {
  120. return absolutePath;
  121. }
  122. // Walk up to find nearest parent with CodeGraph initialized
  123. // Note: findNearestCodeGraphRoot finds any .codegraph folder, but we need one with codegraph.db
  124. let current = absolutePath;
  125. const root = path.parse(current).root;
  126. while (current !== root) {
  127. const parent = path.dirname(current);
  128. if (parent === current) break;
  129. current = parent;
  130. if (isInitialized(current)) {
  131. return current;
  132. }
  133. }
  134. // Not found - return original path (will fail later with helpful error)
  135. return absolutePath;
  136. }
  137. /**
  138. * Format a number with commas
  139. */
  140. function formatNumber(n: number): string {
  141. return n.toLocaleString();
  142. }
  143. /**
  144. * Format duration in milliseconds to human readable
  145. */
  146. function formatDuration(ms: number): string {
  147. if (ms < 1000) {
  148. return `${ms}ms`;
  149. }
  150. const seconds = ms / 1000;
  151. if (seconds < 60) {
  152. return `${seconds.toFixed(1)}s`;
  153. }
  154. const minutes = Math.floor(seconds / 60);
  155. const remainingSeconds = seconds % 60;
  156. return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
  157. }
  158. /**
  159. * Create a progress bar string
  160. */
  161. function progressBar(current: number, total: number, width: number = 30): string {
  162. const percent = total > 0 ? current / total : 0;
  163. const filled = Math.round(width * percent);
  164. const empty = width - filled;
  165. const bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
  166. const percentStr = `${Math.round(percent * 100)}%`.padStart(4);
  167. return `${bar} ${percentStr}`;
  168. }
  169. /**
  170. * Print a progress update (overwrites current line)
  171. */
  172. function printProgress(progress: IndexProgress): void {
  173. const phaseNames: Record<string, string> = {
  174. scanning: 'Scanning files',
  175. parsing: 'Parsing code',
  176. storing: 'Storing data',
  177. resolving: 'Resolving refs',
  178. };
  179. const phaseName = phaseNames[progress.phase] || progress.phase;
  180. const bar = progressBar(progress.current, progress.total);
  181. const file = progress.currentFile ? chalk.dim(` ${progress.currentFile}`) : '';
  182. // Clear line and print progress
  183. process.stdout.write(`\r${chalk.cyan(phaseName)}: ${bar}${file}`.padEnd(100));
  184. }
  185. /**
  186. * Print success message
  187. */
  188. function success(message: string): void {
  189. console.log(chalk.green('✓') + ' ' + message);
  190. }
  191. /**
  192. * Print error message
  193. */
  194. function error(message: string): void {
  195. console.error(chalk.red('✗') + ' ' + message);
  196. }
  197. /**
  198. * Print info message
  199. */
  200. function info(message: string): void {
  201. console.log(chalk.blue('ℹ') + ' ' + message);
  202. }
  203. /**
  204. * Print warning message
  205. */
  206. function warn(message: string): void {
  207. console.log(chalk.yellow('⚠') + ' ' + message);
  208. }
  209. // =============================================================================
  210. // Commands
  211. // =============================================================================
  212. /**
  213. * codegraph init [path]
  214. */
  215. program
  216. .command('init [path]')
  217. .description('Initialize CodeGraph in a project directory')
  218. .option('-i, --index', 'Run initial indexing after initialization')
  219. .action(async (pathArg: string | undefined, options: { index?: boolean }) => {
  220. const projectPath = resolveProjectPath(pathArg);
  221. console.log(chalk.bold('\nInitializing CodeGraph...\n'));
  222. try {
  223. // Check if already initialized
  224. if (isInitialized(projectPath)) {
  225. warn(`CodeGraph already initialized in ${projectPath}`);
  226. info('Use "codegraph index" to re-index or "codegraph sync" to update');
  227. return;
  228. }
  229. // Initialize
  230. const { default: CodeGraph } = await loadCodeGraph();
  231. const cg = await CodeGraph.init(projectPath, {
  232. index: false, // We'll handle indexing ourselves for progress
  233. });
  234. success(`Initialized CodeGraph in ${projectPath}`);
  235. info(`Created .codegraph/ directory`);
  236. // Run initial index if requested
  237. if (options.index) {
  238. console.log('\nIndexing project...\n');
  239. const result = await cg.indexAll({
  240. onProgress: printProgress,
  241. });
  242. // Clear progress line
  243. process.stdout.write('\r' + ' '.repeat(100) + '\r');
  244. if (result.success) {
  245. success(`Indexed ${formatNumber(result.filesIndexed)} files`);
  246. info(`Created ${formatNumber(result.nodesCreated)} nodes and ${formatNumber(result.edgesCreated)} edges`);
  247. info(`Completed in ${formatDuration(result.durationMs)}`);
  248. } else {
  249. warn(`Indexing completed with ${result.errors.length} errors`);
  250. }
  251. } else {
  252. info('Run "codegraph index" to index the project');
  253. }
  254. cg.destroy();
  255. } catch (err) {
  256. captureException(err);
  257. error(`Failed to initialize: ${err instanceof Error ? err.message : String(err)}`);
  258. process.exit(1);
  259. }
  260. });
  261. /**
  262. * codegraph uninit [path]
  263. */
  264. program
  265. .command('uninit [path]')
  266. .description('Remove CodeGraph from a project (deletes .codegraph/ directory)')
  267. .option('-f, --force', 'Skip confirmation prompt')
  268. .action(async (pathArg: string | undefined, options: { force?: boolean }) => {
  269. const projectPath = resolveProjectPath(pathArg);
  270. try {
  271. if (!isInitialized(projectPath)) {
  272. warn(`CodeGraph is not initialized in ${projectPath}`);
  273. return;
  274. }
  275. if (!options.force) {
  276. // Confirm with user
  277. const readline = await import('readline');
  278. const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
  279. const answer = await new Promise<string>((resolve) => {
  280. rl.question(
  281. chalk.yellow('⚠ This will permanently delete all CodeGraph data. Continue? (y/N) '),
  282. resolve
  283. );
  284. });
  285. rl.close();
  286. if (answer.toLowerCase() !== 'y') {
  287. info('Cancelled');
  288. return;
  289. }
  290. }
  291. const { default: CodeGraph } = await loadCodeGraph();
  292. const cg = CodeGraph.openSync(projectPath);
  293. cg.uninitialize();
  294. success(`Removed CodeGraph from ${projectPath}`);
  295. } catch (err) {
  296. captureException(err);
  297. error(`Failed to uninitialize: ${err instanceof Error ? err.message : String(err)}`);
  298. process.exit(1);
  299. }
  300. });
  301. /**
  302. * codegraph index [path]
  303. */
  304. program
  305. .command('index [path]')
  306. .description('Index all files in the project')
  307. .option('-f, --force', 'Force full re-index even if already indexed')
  308. .option('-q, --quiet', 'Suppress progress output')
  309. .action(async (pathArg: string | undefined, options: { force?: boolean; quiet?: boolean }) => {
  310. const projectPath = resolveProjectPath(pathArg);
  311. try {
  312. if (!isInitialized(projectPath)) {
  313. error(`CodeGraph not initialized in ${projectPath}`);
  314. info('Run "codegraph init" first');
  315. process.exit(1);
  316. }
  317. const { default: CodeGraph } = await loadCodeGraph();
  318. const cg = await CodeGraph.open(projectPath);
  319. if (!options.quiet) {
  320. console.log(chalk.bold('\nIndexing project...\n'));
  321. }
  322. // Clear existing data if force
  323. if (options.force) {
  324. cg.clear();
  325. if (!options.quiet) {
  326. info('Cleared existing index');
  327. }
  328. }
  329. const result = await cg.indexAll({
  330. onProgress: options.quiet ? undefined : printProgress,
  331. });
  332. // Clear progress line
  333. if (!options.quiet) {
  334. process.stdout.write('\r' + ' '.repeat(100) + '\r');
  335. }
  336. if (result.success) {
  337. if (!options.quiet) {
  338. success(`Indexed ${formatNumber(result.filesIndexed)} files`);
  339. info(`Created ${formatNumber(result.nodesCreated)} nodes and ${formatNumber(result.edgesCreated)} edges`);
  340. info(`Completed in ${formatDuration(result.durationMs)}`);
  341. }
  342. } else {
  343. if (!options.quiet) {
  344. warn(`Indexing completed with ${result.errors.length} errors`);
  345. for (const err of result.errors.slice(0, 5)) {
  346. console.log(chalk.dim(` - ${err.message}`));
  347. }
  348. if (result.errors.length > 5) {
  349. console.log(chalk.dim(` ... and ${result.errors.length - 5} more`));
  350. }
  351. }
  352. process.exit(1);
  353. }
  354. cg.destroy();
  355. } catch (err) {
  356. captureException(err);
  357. error(`Failed to index: ${err instanceof Error ? err.message : String(err)}`);
  358. process.exit(1);
  359. }
  360. });
  361. /**
  362. * codegraph sync [path]
  363. */
  364. program
  365. .command('sync [path]')
  366. .description('Sync changes since last index')
  367. .option('-q, --quiet', 'Suppress output (for git hooks)')
  368. .action(async (pathArg: string | undefined, options: { quiet?: boolean }) => {
  369. const projectPath = resolveProjectPath(pathArg);
  370. try {
  371. if (!isInitialized(projectPath)) {
  372. if (!options.quiet) {
  373. error(`CodeGraph not initialized in ${projectPath}`);
  374. }
  375. process.exit(1);
  376. }
  377. const { default: CodeGraph } = await loadCodeGraph();
  378. const cg = await CodeGraph.open(projectPath);
  379. const result = await cg.sync({
  380. onProgress: options.quiet ? undefined : printProgress,
  381. });
  382. // Clear progress line
  383. if (!options.quiet) {
  384. process.stdout.write('\r' + ' '.repeat(100) + '\r');
  385. }
  386. const totalChanges = result.filesAdded + result.filesModified + result.filesRemoved;
  387. if (!options.quiet) {
  388. if (totalChanges === 0) {
  389. success('Already up to date');
  390. } else {
  391. success(`Synced ${formatNumber(totalChanges)} changed files`);
  392. if (result.filesAdded > 0) {
  393. info(` Added: ${result.filesAdded}`);
  394. }
  395. if (result.filesModified > 0) {
  396. info(` Modified: ${result.filesModified}`);
  397. }
  398. if (result.filesRemoved > 0) {
  399. info(` Removed: ${result.filesRemoved}`);
  400. }
  401. info(`Updated ${formatNumber(result.nodesUpdated)} nodes in ${formatDuration(result.durationMs)}`);
  402. }
  403. }
  404. cg.destroy();
  405. } catch (err) {
  406. captureException(err);
  407. if (!options.quiet) {
  408. error(`Failed to sync: ${err instanceof Error ? err.message : String(err)}`);
  409. }
  410. process.exit(1);
  411. }
  412. });
  413. /**
  414. * codegraph status [path]
  415. */
  416. program
  417. .command('status [path]')
  418. .description('Show index status and statistics')
  419. .option('-j, --json', 'Output as JSON')
  420. .action(async (pathArg: string | undefined, options: { json?: boolean }) => {
  421. const projectPath = resolveProjectPath(pathArg);
  422. try {
  423. if (!isInitialized(projectPath)) {
  424. if (options.json) {
  425. console.log(JSON.stringify({ initialized: false, projectPath }));
  426. return;
  427. }
  428. console.log(chalk.bold('\nCodeGraph Status\n'));
  429. info(`Project: ${projectPath}`);
  430. warn('Not initialized');
  431. info('Run "codegraph init" to initialize');
  432. return;
  433. }
  434. const { default: CodeGraph } = await loadCodeGraph();
  435. const cg = await CodeGraph.open(projectPath);
  436. const stats = cg.getStats();
  437. const changes = cg.getChangedFiles();
  438. // JSON output mode
  439. if (options.json) {
  440. console.log(JSON.stringify({
  441. initialized: true,
  442. projectPath,
  443. fileCount: stats.fileCount,
  444. nodeCount: stats.nodeCount,
  445. edgeCount: stats.edgeCount,
  446. dbSizeBytes: stats.dbSizeBytes,
  447. nodesByKind: stats.nodesByKind,
  448. languages: Object.entries(stats.filesByLanguage).filter(([, count]) => count > 0).map(([lang]) => lang),
  449. pendingChanges: {
  450. added: changes.added.length,
  451. modified: changes.modified.length,
  452. removed: changes.removed.length,
  453. },
  454. }));
  455. cg.destroy();
  456. return;
  457. }
  458. console.log(chalk.bold('\nCodeGraph Status\n'));
  459. // Project info
  460. console.log(chalk.cyan('Project:'), projectPath);
  461. console.log();
  462. // Index stats
  463. console.log(chalk.bold('Index Statistics:'));
  464. console.log(` Files: ${formatNumber(stats.fileCount)}`);
  465. console.log(` Nodes: ${formatNumber(stats.nodeCount)}`);
  466. console.log(` Edges: ${formatNumber(stats.edgeCount)}`);
  467. console.log(` DB Size: ${(stats.dbSizeBytes / 1024 / 1024).toFixed(2)} MB`);
  468. console.log();
  469. // Node breakdown
  470. console.log(chalk.bold('Nodes by Kind:'));
  471. const nodesByKind = Object.entries(stats.nodesByKind)
  472. .filter(([, count]) => count > 0)
  473. .sort((a, b) => b[1] - a[1]);
  474. for (const [kind, count] of nodesByKind) {
  475. console.log(` ${kind.padEnd(15)} ${formatNumber(count)}`);
  476. }
  477. console.log();
  478. // Language breakdown
  479. console.log(chalk.bold('Files by Language:'));
  480. const filesByLang = Object.entries(stats.filesByLanguage)
  481. .filter(([, count]) => count > 0)
  482. .sort((a, b) => b[1] - a[1]);
  483. for (const [lang, count] of filesByLang) {
  484. console.log(` ${lang.padEnd(15)} ${formatNumber(count)}`);
  485. }
  486. console.log();
  487. // Pending changes
  488. const totalChanges = changes.added.length + changes.modified.length + changes.removed.length;
  489. if (totalChanges > 0) {
  490. console.log(chalk.bold('Pending Changes:'));
  491. if (changes.added.length > 0) {
  492. console.log(` Added: ${changes.added.length} files`);
  493. }
  494. if (changes.modified.length > 0) {
  495. console.log(` Modified: ${changes.modified.length} files`);
  496. }
  497. if (changes.removed.length > 0) {
  498. console.log(` Removed: ${changes.removed.length} files`);
  499. }
  500. info('Run "codegraph sync" to update the index');
  501. } else {
  502. success('Index is up to date');
  503. }
  504. console.log();
  505. cg.destroy();
  506. } catch (err) {
  507. captureException(err);
  508. error(`Failed to get status: ${err instanceof Error ? err.message : String(err)}`);
  509. process.exit(1);
  510. }
  511. });
  512. /**
  513. * codegraph query <search>
  514. */
  515. program
  516. .command('query <search>')
  517. .description('Search for symbols in the codebase')
  518. .option('-p, --path <path>', 'Project path')
  519. .option('-l, --limit <number>', 'Maximum results', '10')
  520. .option('-k, --kind <kind>', 'Filter by node kind (function, class, etc.)')
  521. .option('-j, --json', 'Output as JSON')
  522. .action(async (search: string, options: { path?: string; limit?: string; kind?: string; json?: boolean }) => {
  523. const projectPath = resolveProjectPath(options.path);
  524. try {
  525. if (!isInitialized(projectPath)) {
  526. error(`CodeGraph not initialized in ${projectPath}`);
  527. process.exit(1);
  528. }
  529. const { default: CodeGraph } = await loadCodeGraph();
  530. const cg = await CodeGraph.open(projectPath);
  531. const limit = parseInt(options.limit || '10', 10);
  532. const results = cg.searchNodes(search, {
  533. limit,
  534. kinds: options.kind ? [options.kind as any] : undefined,
  535. });
  536. if (options.json) {
  537. console.log(JSON.stringify(results, null, 2));
  538. } else {
  539. if (results.length === 0) {
  540. info(`No results found for "${search}"`);
  541. } else {
  542. console.log(chalk.bold(`\nSearch Results for "${search}":\n`));
  543. for (const result of results) {
  544. const node = result.node;
  545. const location = `${node.filePath}:${node.startLine}`;
  546. const score = chalk.dim(`(${(result.score * 100).toFixed(0)}%)`);
  547. console.log(
  548. chalk.cyan(node.kind.padEnd(12)) +
  549. chalk.white(node.name) +
  550. ' ' + score
  551. );
  552. console.log(chalk.dim(` ${location}`));
  553. if (node.signature) {
  554. console.log(chalk.dim(` ${node.signature}`));
  555. }
  556. console.log();
  557. }
  558. }
  559. }
  560. cg.destroy();
  561. } catch (err) {
  562. captureException(err);
  563. error(`Search failed: ${err instanceof Error ? err.message : String(err)}`);
  564. process.exit(1);
  565. }
  566. });
  567. /**
  568. * codegraph files [path]
  569. */
  570. program
  571. .command('files')
  572. .description('Show project file structure from the index')
  573. .option('-p, --path <path>', 'Project path')
  574. .option('--filter <dir>', 'Filter to files under this directory')
  575. .option('--pattern <glob>', 'Filter files matching this glob pattern')
  576. .option('--format <format>', 'Output format (tree, flat, grouped)', 'tree')
  577. .option('--max-depth <number>', 'Maximum directory depth for tree format')
  578. .option('--no-metadata', 'Hide file metadata (language, symbol count)')
  579. .option('-j, --json', 'Output as JSON')
  580. .action(async (options: {
  581. path?: string;
  582. filter?: string;
  583. pattern?: string;
  584. format?: string;
  585. maxDepth?: string;
  586. metadata?: boolean;
  587. json?: boolean;
  588. }) => {
  589. const projectPath = resolveProjectPath(options.path);
  590. try {
  591. if (!isInitialized(projectPath)) {
  592. error(`CodeGraph not initialized in ${projectPath}`);
  593. process.exit(1);
  594. }
  595. const { default: CodeGraph } = await loadCodeGraph();
  596. const cg = await CodeGraph.open(projectPath);
  597. let files = cg.getFiles();
  598. if (files.length === 0) {
  599. info('No files indexed. Run "codegraph index" first.');
  600. cg.destroy();
  601. return;
  602. }
  603. // Filter by path prefix
  604. if (options.filter) {
  605. const filter = options.filter;
  606. files = files.filter(f => f.path.startsWith(filter) || f.path.startsWith('./' + filter));
  607. }
  608. // Filter by glob pattern
  609. if (options.pattern) {
  610. const regex = globToRegex(options.pattern);
  611. files = files.filter(f => regex.test(f.path));
  612. }
  613. if (files.length === 0) {
  614. info('No files found matching the criteria.');
  615. cg.destroy();
  616. return;
  617. }
  618. // JSON output
  619. if (options.json) {
  620. const output = files.map(f => ({
  621. path: f.path,
  622. language: f.language,
  623. nodeCount: f.nodeCount,
  624. size: f.size,
  625. }));
  626. console.log(JSON.stringify(output, null, 2));
  627. cg.destroy();
  628. return;
  629. }
  630. const includeMetadata = options.metadata !== false;
  631. const format = options.format || 'tree';
  632. const maxDepth = options.maxDepth ? parseInt(options.maxDepth, 10) : undefined;
  633. // Format output
  634. switch (format) {
  635. case 'flat':
  636. console.log(chalk.bold(`\nFiles (${files.length}):\n`));
  637. for (const file of files.sort((a, b) => a.path.localeCompare(b.path))) {
  638. if (includeMetadata) {
  639. console.log(` ${file.path} ${chalk.dim(`(${file.language}, ${file.nodeCount} symbols)`)}`);
  640. } else {
  641. console.log(` ${file.path}`);
  642. }
  643. }
  644. break;
  645. case 'grouped':
  646. console.log(chalk.bold(`\nFiles by Language (${files.length} total):\n`));
  647. const byLang = new Map<string, typeof files>();
  648. for (const file of files) {
  649. const existing = byLang.get(file.language) || [];
  650. existing.push(file);
  651. byLang.set(file.language, existing);
  652. }
  653. const sortedLangs = [...byLang.entries()].sort((a, b) => b[1].length - a[1].length);
  654. for (const [lang, langFiles] of sortedLangs) {
  655. console.log(chalk.cyan(`${lang} (${langFiles.length}):`));
  656. for (const file of langFiles.sort((a, b) => a.path.localeCompare(b.path))) {
  657. if (includeMetadata) {
  658. console.log(` ${file.path} ${chalk.dim(`(${file.nodeCount} symbols)`)}`);
  659. } else {
  660. console.log(` ${file.path}`);
  661. }
  662. }
  663. console.log();
  664. }
  665. break;
  666. case 'tree':
  667. default:
  668. console.log(chalk.bold(`\nProject Structure (${files.length} files):\n`));
  669. printFileTree(files, includeMetadata, maxDepth, chalk);
  670. break;
  671. }
  672. console.log();
  673. cg.destroy();
  674. } catch (err) {
  675. captureException(err);
  676. error(`Failed to list files: ${err instanceof Error ? err.message : String(err)}`);
  677. process.exit(1);
  678. }
  679. });
  680. /**
  681. * Convert glob pattern to regex
  682. */
  683. function globToRegex(pattern: string): RegExp {
  684. const escaped = pattern
  685. .replace(/[.+^${}()|[\]\\]/g, '\\$&')
  686. .replace(/\*\*/g, '{{GLOBSTAR}}')
  687. .replace(/\*/g, '[^/]*')
  688. .replace(/\?/g, '[^/]')
  689. .replace(/\{\{GLOBSTAR\}\}/g, '.*');
  690. return new RegExp(escaped);
  691. }
  692. /**
  693. * Print files as a tree
  694. */
  695. function printFileTree(
  696. files: { path: string; language: string; nodeCount: number }[],
  697. includeMetadata: boolean,
  698. maxDepth: number | undefined,
  699. chalk: { dim: (s: string) => string; cyan: (s: string) => string }
  700. ): void {
  701. interface TreeNode {
  702. name: string;
  703. children: Map<string, TreeNode>;
  704. file?: { language: string; nodeCount: number };
  705. }
  706. const root: TreeNode = { name: '', children: new Map() };
  707. for (const file of files) {
  708. const parts = file.path.split('/');
  709. let current = root;
  710. for (let i = 0; i < parts.length; i++) {
  711. const part = parts[i];
  712. if (!part) continue;
  713. if (!current.children.has(part)) {
  714. current.children.set(part, { name: part, children: new Map() });
  715. }
  716. current = current.children.get(part)!;
  717. if (i === parts.length - 1) {
  718. current.file = { language: file.language, nodeCount: file.nodeCount };
  719. }
  720. }
  721. }
  722. const renderNode = (node: TreeNode, prefix: string, isLast: boolean, depth: number): void => {
  723. if (maxDepth !== undefined && depth > maxDepth) return;
  724. const connector = isLast ? '└── ' : '├── ';
  725. const childPrefix = isLast ? ' ' : '│ ';
  726. if (node.name) {
  727. let line = prefix + connector + node.name;
  728. if (node.file && includeMetadata) {
  729. line += chalk.dim(` (${node.file.language}, ${node.file.nodeCount} symbols)`);
  730. }
  731. console.log(line);
  732. }
  733. const children = [...node.children.values()];
  734. children.sort((a, b) => {
  735. const aIsDir = a.children.size > 0 && !a.file;
  736. const bIsDir = b.children.size > 0 && !b.file;
  737. if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
  738. return a.name.localeCompare(b.name);
  739. });
  740. for (let i = 0; i < children.length; i++) {
  741. const child = children[i]!;
  742. const nextPrefix = node.name ? prefix + childPrefix : prefix;
  743. renderNode(child, nextPrefix, i === children.length - 1, depth + 1);
  744. }
  745. };
  746. renderNode(root, '', true, 0);
  747. }
  748. /**
  749. * codegraph context <task>
  750. */
  751. program
  752. .command('context <task>')
  753. .description('Build context for a task (outputs markdown)')
  754. .option('-p, --path <path>', 'Project path')
  755. .option('-n, --max-nodes <number>', 'Maximum nodes to include', '50')
  756. .option('-c, --max-code <number>', 'Maximum code blocks', '10')
  757. .option('--no-code', 'Exclude code blocks')
  758. .option('-f, --format <format>', 'Output format (markdown, json)', 'markdown')
  759. .action(async (task: string, options: {
  760. path?: string;
  761. maxNodes?: string;
  762. maxCode?: string;
  763. code?: boolean;
  764. format?: string;
  765. }) => {
  766. const projectPath = resolveProjectPath(options.path);
  767. try {
  768. if (!isInitialized(projectPath)) {
  769. error(`CodeGraph not initialized in ${projectPath}`);
  770. process.exit(1);
  771. }
  772. const { default: CodeGraph } = await loadCodeGraph();
  773. const cg = await CodeGraph.open(projectPath);
  774. const context = await cg.buildContext(task, {
  775. maxNodes: parseInt(options.maxNodes || '50', 10),
  776. maxCodeBlocks: parseInt(options.maxCode || '10', 10),
  777. includeCode: options.code !== false,
  778. format: options.format as 'markdown' | 'json',
  779. });
  780. // Output the context
  781. console.log(context);
  782. cg.destroy();
  783. } catch (err) {
  784. captureException(err);
  785. error(`Failed to build context: ${err instanceof Error ? err.message : String(err)}`);
  786. process.exit(1);
  787. }
  788. });
  789. /**
  790. * codegraph serve
  791. */
  792. program
  793. .command('serve')
  794. .description('Start CodeGraph as an MCP server for AI assistants')
  795. .option('-p, --path <path>', 'Project path (optional for MCP mode, uses rootUri from client)')
  796. .option('--mcp', 'Run as MCP server (stdio transport)')
  797. .action(async (options: { path?: string; mcp?: boolean }) => {
  798. const projectPath = options.path ? resolveProjectPath(options.path) : undefined;
  799. try {
  800. if (options.mcp) {
  801. // Start MCP server - it handles initialization lazily based on rootUri from client
  802. const { MCPServer } = await import('../mcp/index');
  803. const server = new MCPServer(projectPath);
  804. await server.start();
  805. // Server will run until terminated
  806. } else {
  807. // Default: show info about MCP mode.
  808. // Use stderr so stdout stays clean for any piped/stdio usage.
  809. console.error(chalk.bold('\nCodeGraph MCP Server\n'));
  810. console.error(chalk.blue('ℹ') + ' Use --mcp flag to start the MCP server');
  811. console.error('\nTo use with Claude Code, add to your MCP configuration:');
  812. console.error(chalk.dim(`
  813. {
  814. "mcpServers": {
  815. "codegraph": {
  816. "command": "codegraph",
  817. "args": ["serve", "--mcp"]
  818. }
  819. }
  820. }
  821. `));
  822. console.error('Available tools:');
  823. console.error(chalk.cyan(' codegraph_search') + ' - Search for code symbols');
  824. console.error(chalk.cyan(' codegraph_context') + ' - Build context for a task');
  825. console.error(chalk.cyan(' codegraph_callers') + ' - Find callers of a symbol');
  826. console.error(chalk.cyan(' codegraph_callees') + ' - Find what a symbol calls');
  827. console.error(chalk.cyan(' codegraph_impact') + ' - Analyze impact of changes');
  828. console.error(chalk.cyan(' codegraph_node') + ' - Get symbol details');
  829. console.error(chalk.cyan(' codegraph_files') + ' - Get project file structure');
  830. console.error(chalk.cyan(' codegraph_status') + ' - Get index status');
  831. }
  832. } catch (err) {
  833. captureException(err);
  834. error(`Failed to start server: ${err instanceof Error ? err.message : String(err)}`);
  835. process.exit(1);
  836. }
  837. });
  838. /**
  839. * codegraph mark-dirty [path]
  840. *
  841. * Touches .codegraph/.dirty to signal that files have changed.
  842. * Used by Claude Code PostToolUse hooks to batch syncs.
  843. * Runs silently and always exits 0.
  844. */
  845. program
  846. .command('mark-dirty [path]')
  847. .description('Mark project as needing sync (used by Claude Code hooks)')
  848. .action(async (pathArg: string | undefined) => {
  849. try {
  850. const startPath = path.resolve(pathArg || process.cwd());
  851. const projectRoot = findNearestCodeGraphRoot(startPath);
  852. if (!projectRoot) {
  853. // No .codegraph/ found — exit silently
  854. process.exit(0);
  855. }
  856. const dirtyPath = path.join(getCodeGraphDir(projectRoot), '.dirty');
  857. fs.writeFileSync(dirtyPath, Date.now().toString(), 'utf-8');
  858. } catch {
  859. // Never fail — this runs in the background during edits
  860. }
  861. process.exit(0);
  862. });
  863. /**
  864. * codegraph sync-if-dirty [path]
  865. *
  866. * Checks if .codegraph/.dirty exists and, if so, spawns a detached
  867. * background process to run `codegraph sync`. The hook process exits
  868. * immediately so Claude Code's Stop hook never blocks.
  869. *
  870. * Removes the marker BEFORE spawning so edits during sync
  871. * create a new marker for the next Stop event.
  872. * Runs silently and always exits 0.
  873. */
  874. program
  875. .command('sync-if-dirty [path]')
  876. .description('Sync if project was marked dirty (used by Claude Code hooks)')
  877. .action(async (pathArg: string | undefined) => {
  878. try {
  879. const startPath = path.resolve(pathArg || process.cwd());
  880. const projectRoot = findNearestCodeGraphRoot(startPath);
  881. if (!projectRoot) {
  882. process.exit(0);
  883. }
  884. const dirtyPath = path.join(getCodeGraphDir(projectRoot!), '.dirty');
  885. // No marker → nothing to do (sub-ms exit)
  886. if (!fs.existsSync(dirtyPath)) {
  887. process.exit(0);
  888. }
  889. // Remove marker FIRST so edits during sync create a new one
  890. try { fs.unlinkSync(dirtyPath); } catch { /* ignore */ }
  891. // If not fully initialized (no DB), exit
  892. if (!isInitialized(projectRoot!)) {
  893. process.exit(0);
  894. }
  895. // Spawn sync as a detached background process
  896. // so this hook exits immediately and doesn't block Claude Code.
  897. // Uses process.argv[0]/[1] (e.g. node /path/to/codegraph.js) so it
  898. // works whether invoked via global install, npx, or directly.
  899. const child = spawn(
  900. process.argv[0]!,
  901. [process.argv[1]!, 'sync', '--quiet', projectRoot!],
  902. {
  903. detached: true,
  904. stdio: 'ignore',
  905. windowsHide: true,
  906. }
  907. );
  908. child.unref();
  909. } catch {
  910. // Never fail — this runs at the end of Claude responses
  911. }
  912. process.exit(0);
  913. });
  914. /**
  915. * codegraph install
  916. */
  917. program
  918. .command('install')
  919. .description('Run interactive installer for Claude Code integration')
  920. .action(async () => {
  921. const { runInstaller } = await import('../installer');
  922. await runInstaller();
  923. });
  924. // Parse and run
  925. program.parse();
  926. } // end main()