codegraph.ts 41 KB

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