@optave/codegraph 3.1.3 → 3.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +17 -19
  2. package/package.json +10 -7
  3. package/src/analysis/context.js +408 -0
  4. package/src/analysis/dependencies.js +341 -0
  5. package/src/analysis/exports.js +130 -0
  6. package/src/analysis/impact.js +463 -0
  7. package/src/analysis/module-map.js +322 -0
  8. package/src/analysis/roles.js +45 -0
  9. package/src/analysis/symbol-lookup.js +232 -0
  10. package/src/ast-analysis/shared.js +5 -4
  11. package/src/batch.js +2 -1
  12. package/src/builder/context.js +85 -0
  13. package/src/builder/helpers.js +218 -0
  14. package/src/builder/incremental.js +178 -0
  15. package/src/builder/pipeline.js +130 -0
  16. package/src/builder/stages/build-edges.js +297 -0
  17. package/src/builder/stages/build-structure.js +113 -0
  18. package/src/builder/stages/collect-files.js +44 -0
  19. package/src/builder/stages/detect-changes.js +413 -0
  20. package/src/builder/stages/finalize.js +139 -0
  21. package/src/builder/stages/insert-nodes.js +195 -0
  22. package/src/builder/stages/parse-files.js +28 -0
  23. package/src/builder/stages/resolve-imports.js +143 -0
  24. package/src/builder/stages/run-analyses.js +44 -0
  25. package/src/builder.js +10 -1485
  26. package/src/cfg.js +1 -2
  27. package/src/cli/commands/ast.js +26 -0
  28. package/src/cli/commands/audit.js +46 -0
  29. package/src/cli/commands/batch.js +68 -0
  30. package/src/cli/commands/branch-compare.js +21 -0
  31. package/src/cli/commands/build.js +26 -0
  32. package/src/cli/commands/cfg.js +30 -0
  33. package/src/cli/commands/check.js +79 -0
  34. package/src/cli/commands/children.js +31 -0
  35. package/src/cli/commands/co-change.js +65 -0
  36. package/src/cli/commands/communities.js +23 -0
  37. package/src/cli/commands/complexity.js +45 -0
  38. package/src/cli/commands/context.js +34 -0
  39. package/src/cli/commands/cycles.js +28 -0
  40. package/src/cli/commands/dataflow.js +32 -0
  41. package/src/cli/commands/deps.js +16 -0
  42. package/src/cli/commands/diff-impact.js +30 -0
  43. package/src/cli/commands/embed.js +30 -0
  44. package/src/cli/commands/export.js +75 -0
  45. package/src/cli/commands/exports.js +18 -0
  46. package/src/cli/commands/flow.js +36 -0
  47. package/src/cli/commands/fn-impact.js +30 -0
  48. package/src/cli/commands/impact.js +16 -0
  49. package/src/cli/commands/info.js +76 -0
  50. package/src/cli/commands/map.js +19 -0
  51. package/src/cli/commands/mcp.js +18 -0
  52. package/src/cli/commands/models.js +19 -0
  53. package/src/cli/commands/owners.js +25 -0
  54. package/src/cli/commands/path.js +36 -0
  55. package/src/cli/commands/plot.js +80 -0
  56. package/src/cli/commands/query.js +49 -0
  57. package/src/cli/commands/registry.js +100 -0
  58. package/src/cli/commands/roles.js +34 -0
  59. package/src/cli/commands/search.js +42 -0
  60. package/src/cli/commands/sequence.js +32 -0
  61. package/src/cli/commands/snapshot.js +61 -0
  62. package/src/cli/commands/stats.js +15 -0
  63. package/src/cli/commands/structure.js +32 -0
  64. package/src/cli/commands/triage.js +78 -0
  65. package/src/cli/commands/watch.js +12 -0
  66. package/src/cli/commands/where.js +24 -0
  67. package/src/cli/index.js +118 -0
  68. package/src/cli/shared/options.js +39 -0
  69. package/src/cli/shared/output.js +1 -0
  70. package/src/cli.js +11 -1522
  71. package/src/commands/check.js +5 -5
  72. package/src/commands/manifesto.js +3 -3
  73. package/src/commands/structure.js +1 -1
  74. package/src/communities.js +15 -87
  75. package/src/cycles.js +30 -85
  76. package/src/dataflow.js +1 -2
  77. package/src/db/connection.js +4 -4
  78. package/src/db/migrations.js +41 -0
  79. package/src/db/query-builder.js +6 -5
  80. package/src/db/repository/base.js +201 -0
  81. package/src/db/repository/graph-read.js +5 -2
  82. package/src/db/repository/in-memory-repository.js +584 -0
  83. package/src/db/repository/index.js +5 -1
  84. package/src/db/repository/nodes.js +63 -4
  85. package/src/db/repository/sqlite-repository.js +219 -0
  86. package/src/db.js +5 -0
  87. package/src/embeddings/generator.js +163 -0
  88. package/src/embeddings/index.js +13 -0
  89. package/src/embeddings/models.js +218 -0
  90. package/src/embeddings/search/cli-formatter.js +151 -0
  91. package/src/embeddings/search/filters.js +46 -0
  92. package/src/embeddings/search/hybrid.js +121 -0
  93. package/src/embeddings/search/keyword.js +68 -0
  94. package/src/embeddings/search/prepare.js +66 -0
  95. package/src/embeddings/search/semantic.js +145 -0
  96. package/src/embeddings/stores/fts5.js +27 -0
  97. package/src/embeddings/stores/sqlite-blob.js +24 -0
  98. package/src/embeddings/strategies/source.js +14 -0
  99. package/src/embeddings/strategies/structured.js +43 -0
  100. package/src/embeddings/strategies/text-utils.js +43 -0
  101. package/src/errors.js +78 -0
  102. package/src/export.js +217 -520
  103. package/src/extractors/csharp.js +10 -2
  104. package/src/extractors/go.js +3 -1
  105. package/src/extractors/helpers.js +71 -0
  106. package/src/extractors/java.js +9 -2
  107. package/src/extractors/javascript.js +38 -1
  108. package/src/extractors/php.js +3 -1
  109. package/src/extractors/python.js +14 -3
  110. package/src/extractors/rust.js +3 -1
  111. package/src/graph/algorithms/bfs.js +49 -0
  112. package/src/graph/algorithms/centrality.js +16 -0
  113. package/src/graph/algorithms/index.js +5 -0
  114. package/src/graph/algorithms/louvain.js +26 -0
  115. package/src/graph/algorithms/shortest-path.js +41 -0
  116. package/src/graph/algorithms/tarjan.js +49 -0
  117. package/src/graph/builders/dependency.js +91 -0
  118. package/src/graph/builders/index.js +3 -0
  119. package/src/graph/builders/structure.js +40 -0
  120. package/src/graph/builders/temporal.js +33 -0
  121. package/src/graph/classifiers/index.js +2 -0
  122. package/src/graph/classifiers/risk.js +85 -0
  123. package/src/graph/classifiers/roles.js +64 -0
  124. package/src/graph/index.js +13 -0
  125. package/src/graph/model.js +230 -0
  126. package/src/index.js +33 -210
  127. package/src/infrastructure/result-formatter.js +2 -21
  128. package/src/mcp/index.js +2 -0
  129. package/src/mcp/middleware.js +26 -0
  130. package/src/mcp/server.js +128 -0
  131. package/src/mcp/tool-registry.js +801 -0
  132. package/src/mcp/tools/ast-query.js +14 -0
  133. package/src/mcp/tools/audit.js +21 -0
  134. package/src/mcp/tools/batch-query.js +11 -0
  135. package/src/mcp/tools/branch-compare.js +10 -0
  136. package/src/mcp/tools/cfg.js +21 -0
  137. package/src/mcp/tools/check.js +43 -0
  138. package/src/mcp/tools/co-changes.js +20 -0
  139. package/src/mcp/tools/code-owners.js +12 -0
  140. package/src/mcp/tools/communities.js +15 -0
  141. package/src/mcp/tools/complexity.js +18 -0
  142. package/src/mcp/tools/context.js +17 -0
  143. package/src/mcp/tools/dataflow.js +26 -0
  144. package/src/mcp/tools/diff-impact.js +24 -0
  145. package/src/mcp/tools/execution-flow.js +26 -0
  146. package/src/mcp/tools/export-graph.js +57 -0
  147. package/src/mcp/tools/file-deps.js +12 -0
  148. package/src/mcp/tools/file-exports.js +13 -0
  149. package/src/mcp/tools/find-cycles.js +15 -0
  150. package/src/mcp/tools/fn-impact.js +15 -0
  151. package/src/mcp/tools/impact-analysis.js +12 -0
  152. package/src/mcp/tools/index.js +71 -0
  153. package/src/mcp/tools/list-functions.js +14 -0
  154. package/src/mcp/tools/list-repos.js +11 -0
  155. package/src/mcp/tools/module-map.js +6 -0
  156. package/src/mcp/tools/node-roles.js +14 -0
  157. package/src/mcp/tools/path.js +12 -0
  158. package/src/mcp/tools/query.js +30 -0
  159. package/src/mcp/tools/semantic-search.js +65 -0
  160. package/src/mcp/tools/sequence.js +17 -0
  161. package/src/mcp/tools/structure.js +15 -0
  162. package/src/mcp/tools/symbol-children.js +14 -0
  163. package/src/mcp/tools/triage.js +35 -0
  164. package/src/mcp/tools/where.js +13 -0
  165. package/src/mcp.js +2 -1470
  166. package/src/native.js +3 -1
  167. package/src/presentation/colors.js +44 -0
  168. package/src/presentation/export.js +444 -0
  169. package/src/presentation/result-formatter.js +21 -0
  170. package/src/presentation/sequence-renderer.js +43 -0
  171. package/src/presentation/table.js +47 -0
  172. package/src/presentation/viewer.js +634 -0
  173. package/src/queries.js +35 -2276
  174. package/src/resolve.js +1 -1
  175. package/src/sequence.js +2 -38
  176. package/src/shared/file-utils.js +153 -0
  177. package/src/shared/generators.js +125 -0
  178. package/src/shared/hierarchy.js +27 -0
  179. package/src/shared/normalize.js +59 -0
  180. package/src/snapshot.js +6 -5
  181. package/src/structure.js +15 -40
  182. package/src/triage.js +20 -72
  183. package/src/viewer.js +35 -656
  184. package/src/watcher.js +8 -148
  185. package/src/embedder.js +0 -1097
package/src/cli.js CHANGED
@@ -1,1525 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import { Command } from 'commander';
6
- import { BATCH_COMMANDS, multiBatchData, splitTargets } from './batch.js';
7
- import { buildGraph } from './builder.js';
8
- import { audit } from './commands/audit.js';
9
- import { batch } from './commands/batch.js';
10
- import { loadConfig } from './config.js';
11
- import { findCycles, formatCycles } from './cycles.js';
12
- import { openReadonlyOrFail } from './db.js';
13
- import {
14
- buildEmbeddings,
15
- DEFAULT_MODEL,
16
- EMBEDDING_STRATEGIES,
17
- MODELS,
18
- search,
19
- } from './embedder.js';
20
- import {
21
- exportDOT,
22
- exportGraphML,
23
- exportGraphSON,
24
- exportJSON,
25
- exportMermaid,
26
- exportNeo4jCSV,
27
- } from './export.js';
28
- import { outputResult } from './infrastructure/result-formatter.js';
29
- import { setVerbose } from './logger.js';
30
- import { EVERY_SYMBOL_KIND, VALID_ROLES } from './queries.js';
31
- import {
32
- children,
33
- context,
34
- diffImpact,
35
- explain,
36
- fileDeps,
37
- fileExports,
38
- fnDeps,
39
- fnImpact,
40
- impactAnalysis,
41
- moduleMap,
42
- roles,
43
- stats,
44
- symbolPath,
45
- where,
46
- } from './queries-cli.js';
47
- import {
48
- listRepos,
49
- pruneRegistry,
50
- REGISTRY_PATH,
51
- registerRepo,
52
- unregisterRepo,
53
- } from './registry.js';
54
- import { snapshotDelete, snapshotList, snapshotRestore, snapshotSave } from './snapshot.js';
55
- import { checkForUpdates, printUpdateNotification } from './update-check.js';
56
- import { watchProject } from './watcher.js';
57
-
58
- const __cliDir = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/i, '$1'));
59
- const pkg = JSON.parse(fs.readFileSync(path.join(__cliDir, '..', 'package.json'), 'utf-8'));
60
-
61
- const config = loadConfig(process.cwd());
62
-
63
- const program = new Command();
64
- program
65
- .name('codegraph')
66
- .description('Local code dependency graph tool')
67
- .version(pkg.version)
68
- .option('-v, --verbose', 'Enable verbose/debug output')
69
- .option('--engine <engine>', 'Parser engine: native, wasm, or auto (default: auto)', 'auto')
70
- .hook('preAction', (thisCommand) => {
71
- const opts = thisCommand.opts();
72
- if (opts.verbose) setVerbose(true);
73
- })
74
- .hook('postAction', async (_thisCommand, actionCommand) => {
75
- const name = actionCommand.name();
76
- if (name === 'mcp' || name === 'watch') return;
77
- if (actionCommand.opts().json) return;
78
- try {
79
- const result = await checkForUpdates(pkg.version);
80
- if (result) printUpdateNotification(result.current, result.latest);
81
- } catch {
82
- /* never break CLI */
83
- }
84
- });
85
-
86
- /**
87
- * Resolve the effective noTests value: CLI flag > config > false.
88
- * Commander sets opts.tests to false when --no-tests is passed.
89
- * When --include-tests is passed, always return false (include tests).
90
- * Otherwise, fall back to config.query.excludeTests.
91
- */
92
- function resolveNoTests(opts) {
93
- if (opts.includeTests) return false;
94
- if (opts.tests === false) return true;
95
- return config.query?.excludeTests || false;
96
- }
97
-
98
- /** Attach the common query options shared by most analysis commands. */
99
- const QUERY_OPTS = (cmd) =>
100
- cmd
101
- .option('-d, --db <path>', 'Path to graph.db')
102
- .option('-T, --no-tests', 'Exclude test/spec files from results')
103
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
104
- .option('-j, --json', 'Output as JSON')
105
- .option('--limit <number>', 'Max results to return')
106
- .option('--offset <number>', 'Skip N results (default: 0)')
107
- .option('--ndjson', 'Newline-delimited JSON output');
108
-
109
- function formatSize(bytes) {
110
- if (bytes < 1024) return `${bytes} B`;
111
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
112
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
113
- }
114
-
115
- program
116
- .command('build [dir]')
117
- .description('Parse repo and build graph in .codegraph/graph.db')
118
- .option('--no-incremental', 'Force full rebuild (ignore file hashes)')
119
- .option('--no-ast', 'Skip AST node extraction (calls, new, string, regex, throw, await)')
120
- .option('--no-complexity', 'Skip complexity metrics computation')
121
- .option('--no-dataflow', 'Skip data flow edge extraction')
122
- .option('--no-cfg', 'Skip control flow graph building')
123
- .action(async (dir, opts) => {
124
- const root = path.resolve(dir || '.');
125
- const engine = program.opts().engine;
126
- await buildGraph(root, {
127
- incremental: opts.incremental,
128
- ast: opts.ast,
129
- complexity: opts.complexity,
130
- engine,
131
- dataflow: opts.dataflow,
132
- cfg: opts.cfg,
133
- });
134
- });
135
-
136
- QUERY_OPTS(
137
- program
138
- .command('query <name>')
139
- .description('Function-level dependency chain or shortest path between symbols'),
140
- )
141
- .option('--depth <n>', 'Transitive caller depth', '3')
142
- .option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
143
- .option('-k, --kind <kind>', 'Filter to a specific symbol kind')
144
- .option('--path <to>', 'Path mode: find shortest path to <to>')
145
- .option('--kinds <kinds>', 'Path mode: comma-separated edge kinds to follow (default: calls)')
146
- .option('--reverse', 'Path mode: follow edges backward')
147
- .option('--from-file <path>', 'Path mode: disambiguate source symbol by file')
148
- .option('--to-file <path>', 'Path mode: disambiguate target symbol by file')
149
- .action((name, opts) => {
150
- if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
151
- console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
152
- process.exit(1);
153
- }
154
- if (opts.path) {
155
- console.error('Note: "query --path" is deprecated, use "codegraph path <from> <to>" instead');
156
- symbolPath(name, opts.path, opts.db, {
157
- maxDepth: opts.depth ? parseInt(opts.depth, 10) : 10,
158
- edgeKinds: opts.kinds ? opts.kinds.split(',').map((s) => s.trim()) : undefined,
159
- reverse: opts.reverse,
160
- fromFile: opts.fromFile,
161
- toFile: opts.toFile,
162
- kind: opts.kind,
163
- noTests: resolveNoTests(opts),
164
- json: opts.json,
165
- });
166
- } else {
167
- fnDeps(name, opts.db, {
168
- depth: parseInt(opts.depth, 10),
169
- file: opts.file,
170
- kind: opts.kind,
171
- noTests: resolveNoTests(opts),
172
- json: opts.json,
173
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
174
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
175
- ndjson: opts.ndjson,
176
- });
177
- }
178
- });
179
-
180
- program
181
- .command('path <from> <to>')
182
- .description('Find shortest path between two symbols')
183
- .option('-d, --db <path>', 'Path to graph.db')
184
- .option('--reverse', 'Follow edges backward')
185
- .option('--kinds <kinds>', 'Comma-separated edge kinds to follow (default: calls)')
186
- .option('--from-file <path>', 'Disambiguate source symbol by file')
187
- .option('--to-file <path>', 'Disambiguate target symbol by file')
188
- .option('--depth <n>', 'Max traversal depth', '10')
189
- .option('-k, --kind <kind>', 'Filter to a specific symbol kind')
190
- .option('-T, --no-tests', 'Exclude test/spec files from results')
191
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
192
- .option('-j, --json', 'Output as JSON')
193
- .action((from, to, opts) => {
194
- if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
195
- console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
196
- process.exit(1);
197
- }
198
- symbolPath(from, to, opts.db, {
199
- maxDepth: opts.depth ? parseInt(opts.depth, 10) : 10,
200
- edgeKinds: opts.kinds ? opts.kinds.split(',').map((s) => s.trim()) : undefined,
201
- reverse: opts.reverse,
202
- fromFile: opts.fromFile,
203
- toFile: opts.toFile,
204
- kind: opts.kind,
205
- noTests: resolveNoTests(opts),
206
- json: opts.json,
207
- });
208
- });
209
-
210
- QUERY_OPTS(
211
- program.command('impact <file>').description('Show what depends on this file (transitive)'),
212
- ).action((file, opts) => {
213
- impactAnalysis(file, opts.db, {
214
- noTests: resolveNoTests(opts),
215
- json: opts.json,
216
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
217
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
218
- ndjson: opts.ndjson,
219
- });
220
- });
221
-
222
- program
223
- .command('map')
224
- .description('High-level module overview with most-connected nodes')
225
- .option('-d, --db <path>', 'Path to graph.db')
226
- .option('-n, --limit <number>', 'Number of top nodes', '20')
227
- .option('-T, --no-tests', 'Exclude test/spec files from results')
228
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
229
- .option('-j, --json', 'Output as JSON')
230
- .action((opts) => {
231
- moduleMap(opts.db, parseInt(opts.limit, 10), {
232
- noTests: resolveNoTests(opts),
233
- json: opts.json,
234
- });
235
- });
236
-
237
- program
238
- .command('stats')
239
- .description('Show graph health overview: nodes, edges, languages, cycles, hotspots, embeddings')
240
- .option('-d, --db <path>', 'Path to graph.db')
241
- .option('-T, --no-tests', 'Exclude test/spec files from results')
242
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
243
- .option('-j, --json', 'Output as JSON')
244
- .action(async (opts) => {
245
- await stats(opts.db, { noTests: resolveNoTests(opts), json: opts.json });
246
- });
247
-
248
- QUERY_OPTS(
249
- program.command('deps <file>').description('Show what this file imports and what imports it'),
250
- ).action((file, opts) => {
251
- fileDeps(file, opts.db, {
252
- noTests: resolveNoTests(opts),
253
- json: opts.json,
254
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
255
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
256
- ndjson: opts.ndjson,
257
- });
3
+ import { run } from './cli/index.js';
4
+ import { CodegraphError } from './errors.js';
5
+
6
+ run().catch((err) => {
7
+ if (err instanceof CodegraphError) {
8
+ console.error(`codegraph [${err.code}]: ${err.message}`);
9
+ if (err.file) console.error(` file: ${err.file}`);
10
+ } else {
11
+ console.error(`codegraph: fatal error ${err.message || err}`);
12
+ }
13
+ process.exit(1);
258
14
  });
259
-
260
- QUERY_OPTS(
261
- program
262
- .command('exports <file>')
263
- .description('Show exported symbols with per-symbol consumers (who calls each export)'),
264
- )
265
- .option('--unused', 'Show only exports with zero consumers (dead exports)')
266
- .action((file, opts) => {
267
- fileExports(file, opts.db, {
268
- noTests: resolveNoTests(opts),
269
- json: opts.json,
270
- unused: opts.unused || false,
271
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
272
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
273
- ndjson: opts.ndjson,
274
- });
275
- });
276
-
277
- QUERY_OPTS(
278
- program
279
- .command('fn-impact <name>')
280
- .description('Function-level impact: what functions break if this one changes'),
281
- )
282
- .option('--depth <n>', 'Max transitive depth', '5')
283
- .option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
284
- .option('-k, --kind <kind>', 'Filter to a specific symbol kind')
285
- .action((name, opts) => {
286
- if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
287
- console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
288
- process.exit(1);
289
- }
290
- fnImpact(name, opts.db, {
291
- depth: parseInt(opts.depth, 10),
292
- file: opts.file,
293
- kind: opts.kind,
294
- noTests: resolveNoTests(opts),
295
- json: opts.json,
296
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
297
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
298
- ndjson: opts.ndjson,
299
- });
300
- });
301
-
302
- QUERY_OPTS(
303
- program
304
- .command('context <name>')
305
- .description('Full context for a function: source, deps, callers, tests, signature'),
306
- )
307
- .option('--depth <n>', 'Include callee source up to N levels deep', '0')
308
- .option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
309
- .option('-k, --kind <kind>', 'Filter to a specific symbol kind')
310
- .option('--no-source', 'Metadata only (skip source extraction)')
311
- .option('--with-test-source', 'Include test source code')
312
- .action((name, opts) => {
313
- if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
314
- console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
315
- process.exit(1);
316
- }
317
- context(name, opts.db, {
318
- depth: parseInt(opts.depth, 10),
319
- file: opts.file,
320
- kind: opts.kind,
321
- noSource: !opts.source,
322
- noTests: resolveNoTests(opts),
323
- includeTests: opts.withTestSource,
324
- json: opts.json,
325
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
326
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
327
- ndjson: opts.ndjson,
328
- });
329
- });
330
-
331
- program
332
- .command('children <name>')
333
- .description('List parameters, properties, and constants of a symbol')
334
- .option('-d, --db <path>', 'Path to graph.db')
335
- .option('-f, --file <path>', 'Scope search to symbols in this file (partial match)')
336
- .option('-k, --kind <kind>', 'Filter to a specific symbol kind')
337
- .option('-T, --no-tests', 'Exclude test/spec files from results')
338
- .option('-j, --json', 'Output as JSON')
339
- .option('--limit <number>', 'Max results to return')
340
- .option('--offset <number>', 'Skip N results (default: 0)')
341
- .action((name, opts) => {
342
- if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
343
- console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
344
- process.exit(1);
345
- }
346
- children(name, opts.db, {
347
- file: opts.file,
348
- kind: opts.kind,
349
- noTests: resolveNoTests(opts),
350
- json: opts.json,
351
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
352
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
353
- });
354
- });
355
-
356
- program
357
- .command('audit <target>')
358
- .description('Composite report: explain + impact + health metrics per function')
359
- .option('-d, --db <path>', 'Path to graph.db')
360
- .option('--quick', 'Structural summary only (skip impact analysis and health metrics)')
361
- .option('--depth <n>', 'Impact/explain depth', '3')
362
- .option('-f, --file <path>', 'Scope to file (partial match)')
363
- .option('-k, --kind <kind>', 'Filter by symbol kind')
364
- .option('-T, --no-tests', 'Exclude test/spec files from results')
365
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
366
- .option('-j, --json', 'Output as JSON')
367
- .option('--limit <number>', 'Max results to return (quick mode)')
368
- .option('--offset <number>', 'Skip N results (quick mode)')
369
- .option('--ndjson', 'Newline-delimited JSON output (quick mode)')
370
- .action((target, opts) => {
371
- if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
372
- console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
373
- process.exit(1);
374
- }
375
- if (opts.quick) {
376
- explain(target, opts.db, {
377
- depth: parseInt(opts.depth, 10),
378
- noTests: resolveNoTests(opts),
379
- json: opts.json,
380
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
381
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
382
- ndjson: opts.ndjson,
383
- });
384
- return;
385
- }
386
- audit(target, opts.db, {
387
- depth: parseInt(opts.depth, 10),
388
- file: opts.file,
389
- kind: opts.kind,
390
- noTests: resolveNoTests(opts),
391
- json: opts.json,
392
- });
393
- });
394
-
395
- QUERY_OPTS(
396
- program
397
- .command('where [name]')
398
- .description('Find where a symbol is defined and used (minimal, fast lookup)'),
399
- )
400
- .option('-f, --file <path>', 'File overview: list symbols, imports, exports')
401
- .action((name, opts) => {
402
- if (!name && !opts.file) {
403
- console.error('Provide a symbol name or use --file <path>');
404
- process.exit(1);
405
- }
406
- const target = opts.file || name;
407
- where(target, opts.db, {
408
- file: !!opts.file,
409
- noTests: resolveNoTests(opts),
410
- json: opts.json,
411
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
412
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
413
- ndjson: opts.ndjson,
414
- });
415
- });
416
-
417
- program
418
- .command('diff-impact [ref]')
419
- .description('Show impact of git changes (unstaged, staged, or vs a ref)')
420
- .option('-d, --db <path>', 'Path to graph.db')
421
- .option('-T, --no-tests', 'Exclude test/spec files from results')
422
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
423
- .option('--limit <number>', 'Max results to return')
424
- .option('--offset <number>', 'Skip N results (default: 0)')
425
- .option('--ndjson', 'Newline-delimited JSON output')
426
- .option('--staged', 'Analyze staged changes instead of unstaged')
427
- .option('--depth <n>', 'Max transitive caller depth', '3')
428
- .option('-f, --format <format>', 'Output format: text, mermaid, json', 'text')
429
- .action((ref, opts) => {
430
- diffImpact(opts.db, {
431
- ref,
432
- staged: opts.staged,
433
- depth: parseInt(opts.depth, 10),
434
- noTests: resolveNoTests(opts),
435
- json: opts.json,
436
- format: opts.format,
437
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
438
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
439
- ndjson: opts.ndjson,
440
- });
441
- });
442
-
443
- program
444
- .command('check [ref]')
445
- .description(
446
- 'CI gate: run manifesto rules (no args), diff predicates (with ref/--staged), or both (--rules)',
447
- )
448
- .option('-d, --db <path>', 'Path to graph.db')
449
- .option('--staged', 'Analyze staged changes')
450
- .option('--rules', 'Also run manifesto rules alongside diff predicates')
451
- .option('--cycles', 'Assert no dependency cycles involve changed files')
452
- .option('--blast-radius <n>', 'Assert no function exceeds N transitive callers')
453
- .option('--signatures', 'Assert no function declaration lines were modified')
454
- .option('--boundaries', 'Assert no cross-owner boundary violations')
455
- .option('--depth <n>', 'Max BFS depth for blast radius (default: 3)')
456
- .option('-f, --file <path>', 'Scope to file (partial match, manifesto mode)')
457
- .option('-k, --kind <kind>', 'Filter by symbol kind (manifesto mode)')
458
- .option('-T, --no-tests', 'Exclude test/spec files from results')
459
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
460
- .option('-j, --json', 'Output as JSON')
461
- .option('--limit <number>', 'Max results to return (manifesto mode)')
462
- .option('--offset <number>', 'Skip N results (manifesto mode)')
463
- .option('--ndjson', 'Newline-delimited JSON output (manifesto mode)')
464
- .action(async (ref, opts) => {
465
- const isDiffMode = ref || opts.staged;
466
-
467
- if (!isDiffMode && !opts.rules) {
468
- // No ref, no --staged → run manifesto rules on whole codebase
469
- if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
470
- console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
471
- process.exit(1);
472
- }
473
- const { manifesto } = await import('./commands/manifesto.js');
474
- manifesto(opts.db, {
475
- file: opts.file,
476
- kind: opts.kind,
477
- noTests: resolveNoTests(opts),
478
- json: opts.json,
479
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
480
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
481
- ndjson: opts.ndjson,
482
- });
483
- return;
484
- }
485
-
486
- // Diff predicates mode
487
- const { check } = await import('./commands/check.js');
488
- check(opts.db, {
489
- ref,
490
- staged: opts.staged,
491
- cycles: opts.cycles || undefined,
492
- blastRadius: opts.blastRadius ? parseInt(opts.blastRadius, 10) : undefined,
493
- signatures: opts.signatures || undefined,
494
- boundaries: opts.boundaries || undefined,
495
- depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
496
- noTests: resolveNoTests(opts),
497
- json: opts.json,
498
- });
499
-
500
- // If --rules, also run manifesto after diff predicates
501
- if (opts.rules) {
502
- if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
503
- console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
504
- process.exit(1);
505
- }
506
- const { manifesto } = await import('./commands/manifesto.js');
507
- manifesto(opts.db, {
508
- file: opts.file,
509
- kind: opts.kind,
510
- noTests: resolveNoTests(opts),
511
- json: opts.json,
512
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
513
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
514
- ndjson: opts.ndjson,
515
- });
516
- }
517
- });
518
-
519
- // ─── New commands ────────────────────────────────────────────────────────
520
-
521
- program
522
- .command('export')
523
- .description('Export dependency graph as DOT, Mermaid, JSON, GraphML, GraphSON, or Neo4j CSV')
524
- .option('-d, --db <path>', 'Path to graph.db')
525
- .option(
526
- '-f, --format <format>',
527
- 'Output format: dot, mermaid, json, graphml, graphson, neo4j',
528
- 'dot',
529
- )
530
- .option('--functions', 'Function-level graph instead of file-level')
531
- .option('-T, --no-tests', 'Exclude test/spec files')
532
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
533
- .option('--min-confidence <score>', 'Minimum edge confidence threshold (default: 0.5)', '0.5')
534
- .option('--direction <dir>', 'Flowchart direction for Mermaid: TB, LR, RL, BT', 'LR')
535
- .option('-o, --output <file>', 'Write to file instead of stdout')
536
- .action((opts) => {
537
- const db = openReadonlyOrFail(opts.db);
538
- const exportOpts = {
539
- fileLevel: !opts.functions,
540
- noTests: resolveNoTests(opts),
541
- minConfidence: parseFloat(opts.minConfidence),
542
- direction: opts.direction,
543
- };
544
-
545
- let output;
546
- switch (opts.format) {
547
- case 'mermaid':
548
- output = exportMermaid(db, exportOpts);
549
- break;
550
- case 'json':
551
- output = JSON.stringify(exportJSON(db, exportOpts), null, 2);
552
- break;
553
- case 'graphml':
554
- output = exportGraphML(db, exportOpts);
555
- break;
556
- case 'graphson':
557
- output = JSON.stringify(exportGraphSON(db, exportOpts), null, 2);
558
- break;
559
- case 'neo4j': {
560
- const csv = exportNeo4jCSV(db, exportOpts);
561
- if (opts.output) {
562
- const base = opts.output.replace(/\.[^.]+$/, '') || opts.output;
563
- fs.writeFileSync(`${base}-nodes.csv`, csv.nodes, 'utf-8');
564
- fs.writeFileSync(`${base}-relationships.csv`, csv.relationships, 'utf-8');
565
- db.close();
566
- console.log(`Exported to ${base}-nodes.csv and ${base}-relationships.csv`);
567
- return;
568
- }
569
- output = `--- nodes.csv ---\n${csv.nodes}\n\n--- relationships.csv ---\n${csv.relationships}`;
570
- break;
571
- }
572
- default:
573
- output = exportDOT(db, exportOpts);
574
- break;
575
- }
576
-
577
- db.close();
578
-
579
- if (opts.output) {
580
- fs.writeFileSync(opts.output, output, 'utf-8');
581
- console.log(`Exported ${opts.format} to ${opts.output}`);
582
- } else {
583
- console.log(output);
584
- }
585
- });
586
-
587
- program
588
- .command('plot')
589
- .description('Generate an interactive HTML dependency graph viewer')
590
- .option('-d, --db <path>', 'Path to graph.db')
591
- .option('--functions', 'Function-level graph instead of file-level')
592
- .option('-T, --no-tests', 'Exclude test/spec files')
593
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
594
- .option('--min-confidence <score>', 'Minimum edge confidence threshold (default: 0.5)', '0.5')
595
- .option('-o, --output <file>', 'Write HTML to file')
596
- .option('-c, --config <path>', 'Path to .plotDotCfg config file')
597
- .option('--no-open', 'Do not open in browser')
598
- .option('--cluster <mode>', 'Cluster nodes: none | community | directory')
599
- .option('--overlay <list>', 'Comma-separated overlays: complexity,risk')
600
- .option('--seed <strategy>', 'Seed strategy: all | top-fanin | entry')
601
- .option('--seed-count <n>', 'Number of seed nodes (default: 30)')
602
- .option('--size-by <metric>', 'Size nodes by: uniform | fan-in | fan-out | complexity')
603
- .option('--color-by <mode>', 'Color nodes by: kind | role | community | complexity')
604
- .action(async (opts) => {
605
- const { generatePlotHTML, loadPlotConfig } = await import('./viewer.js');
606
- const os = await import('node:os');
607
- const db = openReadonlyOrFail(opts.db);
608
-
609
- let plotCfg;
610
- if (opts.config) {
611
- try {
612
- plotCfg = JSON.parse(fs.readFileSync(opts.config, 'utf-8'));
613
- } catch (e) {
614
- console.error(`Failed to load config: ${e.message}`);
615
- db.close();
616
- process.exitCode = 1;
617
- return;
618
- }
619
- } else {
620
- plotCfg = loadPlotConfig(process.cwd());
621
- }
622
-
623
- // Merge CLI flags into config
624
- if (opts.cluster) plotCfg.clusterBy = opts.cluster;
625
- if (opts.colorBy) plotCfg.colorBy = opts.colorBy;
626
- if (opts.sizeBy) plotCfg.sizeBy = opts.sizeBy;
627
- if (opts.seed) plotCfg.seedStrategy = opts.seed;
628
- if (opts.seedCount) plotCfg.seedCount = parseInt(opts.seedCount, 10);
629
- if (opts.overlay) {
630
- const parts = opts.overlay.split(',').map((s) => s.trim());
631
- if (!plotCfg.overlays) plotCfg.overlays = {};
632
- if (parts.includes('complexity')) plotCfg.overlays.complexity = true;
633
- if (parts.includes('risk')) plotCfg.overlays.risk = true;
634
- }
635
-
636
- const html = generatePlotHTML(db, {
637
- fileLevel: !opts.functions,
638
- noTests: resolveNoTests(opts),
639
- minConfidence: parseFloat(opts.minConfidence),
640
- config: plotCfg,
641
- });
642
- db.close();
643
-
644
- const outPath = opts.output || path.join(os.tmpdir(), `codegraph-plot-${Date.now()}.html`);
645
- fs.writeFileSync(outPath, html, 'utf-8');
646
- console.log(`Plot written to ${outPath}`);
647
-
648
- if (opts.open !== false) {
649
- const { execFile } = await import('node:child_process');
650
- const args =
651
- process.platform === 'win32'
652
- ? ['cmd', ['/c', 'start', '', outPath]]
653
- : process.platform === 'darwin'
654
- ? ['open', [outPath]]
655
- : ['xdg-open', [outPath]];
656
- execFile(args[0], args[1], (err) => {
657
- if (err) console.error('Could not open browser:', err.message);
658
- });
659
- }
660
- });
661
-
662
- program
663
- .command('cycles')
664
- .description('Detect circular dependencies in the codebase')
665
- .option('-d, --db <path>', 'Path to graph.db')
666
- .option('--functions', 'Function-level cycle detection')
667
- .option('-T, --no-tests', 'Exclude test/spec files')
668
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
669
- .option('-j, --json', 'Output as JSON')
670
- .action((opts) => {
671
- const db = openReadonlyOrFail(opts.db);
672
- const cycles = findCycles(db, { fileLevel: !opts.functions, noTests: resolveNoTests(opts) });
673
- db.close();
674
-
675
- if (opts.json) {
676
- console.log(JSON.stringify({ cycles, count: cycles.length }, null, 2));
677
- } else {
678
- console.log(formatCycles(cycles));
679
- }
680
- });
681
-
682
- program
683
- .command('mcp')
684
- .description('Start MCP (Model Context Protocol) server for AI assistant integration')
685
- .option('-d, --db <path>', 'Path to graph.db')
686
- .option('--multi-repo', 'Enable access to all registered repositories')
687
- .option('--repos <names>', 'Comma-separated list of allowed repo names (restricts access)')
688
- .action(async (opts) => {
689
- const { startMCPServer } = await import('./mcp.js');
690
- const mcpOpts = {};
691
- mcpOpts.multiRepo = opts.multiRepo || !!opts.repos;
692
- if (opts.repos) {
693
- mcpOpts.allowedRepos = opts.repos.split(',').map((s) => s.trim());
694
- }
695
- await startMCPServer(opts.db, mcpOpts);
696
- });
697
-
698
- // ─── Registry commands ──────────────────────────────────────────────────
699
-
700
- const registry = program.command('registry').description('Manage the multi-repo project registry');
701
-
702
- registry
703
- .command('list')
704
- .description('List all registered repositories')
705
- .option('-j, --json', 'Output as JSON')
706
- .action((opts) => {
707
- pruneRegistry();
708
- const repos = listRepos();
709
- if (opts.json) {
710
- console.log(JSON.stringify(repos, null, 2));
711
- } else if (repos.length === 0) {
712
- console.log(`No repositories registered.\nRegistry: ${REGISTRY_PATH}`);
713
- } else {
714
- console.log(`Registered repositories (${REGISTRY_PATH}):\n`);
715
- for (const r of repos) {
716
- const dbExists = fs.existsSync(r.dbPath);
717
- const status = dbExists ? '' : ' [DB missing]';
718
- console.log(` ${r.name}${status}`);
719
- console.log(` Path: ${r.path}`);
720
- console.log(` DB: ${r.dbPath}`);
721
- console.log();
722
- }
723
- }
724
- });
725
-
726
- registry
727
- .command('add <dir>')
728
- .description('Register a project directory')
729
- .option('-n, --name <name>', 'Custom name (defaults to directory basename)')
730
- .action((dir, opts) => {
731
- const absDir = path.resolve(dir);
732
- const { name, entry } = registerRepo(absDir, opts.name);
733
- console.log(`Registered "${name}" → ${entry.path}`);
734
- });
735
-
736
- registry
737
- .command('remove <name>')
738
- .description('Unregister a repository by name')
739
- .action((name) => {
740
- const removed = unregisterRepo(name);
741
- if (removed) {
742
- console.log(`Removed "${name}" from registry.`);
743
- } else {
744
- console.error(`Repository "${name}" not found in registry.`);
745
- process.exit(1);
746
- }
747
- });
748
-
749
- registry
750
- .command('prune')
751
- .description('Remove stale registry entries (missing directories or idle beyond TTL)')
752
- .option('--ttl <days>', 'Days of inactivity before pruning (default: 30)', '30')
753
- .option('--exclude <names>', 'Comma-separated repo names to preserve from pruning')
754
- .option('--dry-run', 'Show what would be pruned without removing anything')
755
- .action((opts) => {
756
- const excludeNames = opts.exclude
757
- ? opts.exclude
758
- .split(',')
759
- .map((s) => s.trim())
760
- .filter((s) => s.length > 0)
761
- : [];
762
- const dryRun = !!opts.dryRun;
763
- const pruned = pruneRegistry(undefined, parseInt(opts.ttl, 10), excludeNames, dryRun);
764
- if (pruned.length === 0) {
765
- console.log('No stale entries found.');
766
- } else {
767
- const prefix = dryRun ? 'Would prune' : 'Pruned';
768
- for (const entry of pruned) {
769
- const tag = entry.reason === 'expired' ? 'expired' : 'missing';
770
- console.log(`${prefix} "${entry.name}" (${entry.path}) [${tag}]`);
771
- }
772
- if (dryRun) {
773
- console.log(
774
- `\nDry run: ${pruned.length} ${pruned.length === 1 ? 'entry' : 'entries'} would be removed.`,
775
- );
776
- } else {
777
- console.log(
778
- `\nRemoved ${pruned.length} stale ${pruned.length === 1 ? 'entry' : 'entries'}.`,
779
- );
780
- }
781
- }
782
- });
783
-
784
- // ─── Snapshot commands ──────────────────────────────────────────────────
785
-
786
- const snapshot = program
787
- .command('snapshot')
788
- .description('Save and restore graph database snapshots');
789
-
790
- snapshot
791
- .command('save <name>')
792
- .description('Save a snapshot of the current graph database')
793
- .option('-d, --db <path>', 'Path to graph.db')
794
- .option('--force', 'Overwrite existing snapshot')
795
- .action((name, opts) => {
796
- try {
797
- const result = snapshotSave(name, { dbPath: opts.db, force: opts.force });
798
- console.log(`Snapshot saved: ${result.name} (${formatSize(result.size)})`);
799
- } catch (err) {
800
- console.error(err.message);
801
- process.exit(1);
802
- }
803
- });
804
-
805
- snapshot
806
- .command('restore <name>')
807
- .description('Restore a snapshot over the current graph database')
808
- .option('-d, --db <path>', 'Path to graph.db')
809
- .action((name, opts) => {
810
- try {
811
- snapshotRestore(name, { dbPath: opts.db });
812
- console.log(`Snapshot "${name}" restored.`);
813
- } catch (err) {
814
- console.error(err.message);
815
- process.exit(1);
816
- }
817
- });
818
-
819
- snapshot
820
- .command('list')
821
- .description('List all saved snapshots')
822
- .option('-d, --db <path>', 'Path to graph.db')
823
- .option('-j, --json', 'Output as JSON')
824
- .action((opts) => {
825
- try {
826
- const snapshots = snapshotList({ dbPath: opts.db });
827
- if (opts.json) {
828
- console.log(JSON.stringify(snapshots, null, 2));
829
- } else if (snapshots.length === 0) {
830
- console.log('No snapshots found.');
831
- } else {
832
- console.log(`Snapshots (${snapshots.length}):\n`);
833
- for (const s of snapshots) {
834
- console.log(
835
- ` ${s.name.padEnd(30)} ${formatSize(s.size).padStart(10)} ${s.createdAt.toISOString()}`,
836
- );
837
- }
838
- }
839
- } catch (err) {
840
- console.error(err.message);
841
- process.exit(1);
842
- }
843
- });
844
-
845
- snapshot
846
- .command('delete <name>')
847
- .description('Delete a saved snapshot')
848
- .option('-d, --db <path>', 'Path to graph.db')
849
- .action((name, opts) => {
850
- try {
851
- snapshotDelete(name, { dbPath: opts.db });
852
- console.log(`Snapshot "${name}" deleted.`);
853
- } catch (err) {
854
- console.error(err.message);
855
- process.exit(1);
856
- }
857
- });
858
-
859
- // ─── Embedding commands ─────────────────────────────────────────────────
860
-
861
- program
862
- .command('models')
863
- .description('List available embedding models')
864
- .action(() => {
865
- const defaultModel = config.embeddings?.model || DEFAULT_MODEL;
866
- console.log('\nAvailable embedding models:\n');
867
- for (const [key, cfg] of Object.entries(MODELS)) {
868
- const def = key === defaultModel ? ' (default)' : '';
869
- const ctx = cfg.contextWindow ? `${cfg.contextWindow} ctx` : '';
870
- console.log(
871
- ` ${key.padEnd(12)} ${String(cfg.dim).padStart(4)}d ${ctx.padEnd(9)} ${cfg.desc}${def}`,
872
- );
873
- }
874
- console.log('\nUsage: codegraph embed --model <name> --strategy <structured|source>');
875
- console.log(' codegraph search "query" --model <name>\n');
876
- });
877
-
878
- program
879
- .command('embed [dir]')
880
- .description(
881
- 'Build semantic embeddings for all functions/methods/classes (requires prior `build`)',
882
- )
883
- .option(
884
- '-m, --model <name>',
885
- 'Embedding model (default from config or minilm). Run `codegraph models` for details',
886
- )
887
- .option(
888
- '-s, --strategy <name>',
889
- `Embedding strategy: ${EMBEDDING_STRATEGIES.join(', ')}. "structured" uses graph context (callers/callees), "source" embeds raw code`,
890
- 'structured',
891
- )
892
- .option('-d, --db <path>', 'Path to graph.db')
893
- .action(async (dir, opts) => {
894
- if (!EMBEDDING_STRATEGIES.includes(opts.strategy)) {
895
- console.error(
896
- `Unknown strategy: ${opts.strategy}. Available: ${EMBEDDING_STRATEGIES.join(', ')}`,
897
- );
898
- process.exit(1);
899
- }
900
- const root = path.resolve(dir || '.');
901
- const model = opts.model || config.embeddings?.model || DEFAULT_MODEL;
902
- await buildEmbeddings(root, model, opts.db, { strategy: opts.strategy });
903
- });
904
-
905
- program
906
- .command('search <query>')
907
- .description('Semantic search: find functions by natural language description')
908
- .option('-d, --db <path>', 'Path to graph.db')
909
- .option('-m, --model <name>', 'Override embedding model (auto-detects from DB)')
910
- .option('-n, --limit <number>', 'Max results', '15')
911
- .option('-T, --no-tests', 'Exclude test/spec files from results')
912
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
913
- .option('--min-score <score>', 'Minimum similarity threshold', '0.2')
914
- .option('-k, --kind <kind>', 'Filter by kind: function, method, class')
915
- .option('--file <pattern>', 'Filter by file path pattern')
916
- .option('--rrf-k <number>', 'RRF k parameter for multi-query ranking', '60')
917
- .option('--mode <mode>', 'Search mode: hybrid, semantic, keyword (default: hybrid)')
918
- .option('-j, --json', 'Output as JSON')
919
- .option('--offset <number>', 'Skip N results (default: 0)')
920
- .option('--ndjson', 'Newline-delimited JSON output')
921
- .action(async (query, opts) => {
922
- const validModes = ['hybrid', 'semantic', 'keyword'];
923
- if (opts.mode && !validModes.includes(opts.mode)) {
924
- console.error(`Invalid mode "${opts.mode}". Valid: ${validModes.join(', ')}`);
925
- process.exit(1);
926
- }
927
- await search(query, opts.db, {
928
- limit: parseInt(opts.limit, 10),
929
- noTests: resolveNoTests(opts),
930
- minScore: parseFloat(opts.minScore),
931
- model: opts.model,
932
- kind: opts.kind,
933
- filePattern: opts.file,
934
- rrfK: parseInt(opts.rrfK, 10),
935
- mode: opts.mode,
936
- json: opts.json,
937
- });
938
- });
939
-
940
- program
941
- .command('structure [dir]')
942
- .description(
943
- 'Show project directory structure with hierarchy, cohesion scores, and per-file metrics',
944
- )
945
- .option('-d, --db <path>', 'Path to graph.db')
946
- .option('--depth <n>', 'Max directory depth')
947
- .option('--sort <metric>', 'Sort by: cohesion | fan-in | fan-out | density | files', 'files')
948
- .option('--full', 'Show all files without limit')
949
- .option('-T, --no-tests', 'Exclude test/spec files')
950
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
951
- .option('-j, --json', 'Output as JSON')
952
- .option('--limit <number>', 'Max results to return')
953
- .option('--offset <number>', 'Skip N results (default: 0)')
954
- .option('--ndjson', 'Newline-delimited JSON output')
955
- .action(async (dir, opts) => {
956
- const { structureData, formatStructure } = await import('./commands/structure.js');
957
- const data = structureData(opts.db, {
958
- directory: dir,
959
- depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
960
- sort: opts.sort,
961
- full: opts.full,
962
- noTests: resolveNoTests(opts),
963
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
964
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
965
- });
966
- if (!outputResult(data, 'directories', opts)) {
967
- console.log(formatStructure(data));
968
- }
969
- });
970
-
971
- program
972
- .command('roles')
973
- .description('Show node role classification: entry, core, utility, adapter, dead, leaf')
974
- .option('-d, --db <path>', 'Path to graph.db')
975
- .option('--role <role>', `Filter by role (${VALID_ROLES.join(', ')})`)
976
- .option('-f, --file <path>', 'Scope to a specific file (partial match)')
977
- .option('-T, --no-tests', 'Exclude test/spec files')
978
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
979
- .option('-j, --json', 'Output as JSON')
980
- .option('--limit <number>', 'Max results to return')
981
- .option('--offset <number>', 'Skip N results (default: 0)')
982
- .option('--ndjson', 'Newline-delimited JSON output')
983
- .action((opts) => {
984
- if (opts.role && !VALID_ROLES.includes(opts.role)) {
985
- console.error(`Invalid role "${opts.role}". Valid roles: ${VALID_ROLES.join(', ')}`);
986
- process.exit(1);
987
- }
988
- roles(opts.db, {
989
- role: opts.role,
990
- file: opts.file,
991
- noTests: resolveNoTests(opts),
992
- json: opts.json,
993
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
994
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
995
- ndjson: opts.ndjson,
996
- });
997
- });
998
-
999
- program
1000
- .command('co-change [file]')
1001
- .description(
1002
- 'Analyze git history for files that change together. Use --analyze to scan, or query existing data.',
1003
- )
1004
- .option('--analyze', 'Scan git history and populate co-change data')
1005
- .option('--since <date>', 'Git date for history window (default: "1 year ago")')
1006
- .option('--min-support <n>', 'Minimum co-occurrence count (default: 3)')
1007
- .option('--min-jaccard <n>', 'Minimum Jaccard similarity 0-1 (default: 0.3)')
1008
- .option('--full', 'Force full re-scan (ignore incremental state)')
1009
- .option('-n, --limit <n>', 'Max results', '20')
1010
- .option('-d, --db <path>', 'Path to graph.db')
1011
- .option('-T, --no-tests', 'Exclude test/spec files')
1012
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1013
- .option('-j, --json', 'Output as JSON')
1014
- .option('--offset <number>', 'Skip N results (default: 0)')
1015
- .option('--ndjson', 'Newline-delimited JSON output')
1016
- .action(async (file, opts) => {
1017
- const { analyzeCoChanges, coChangeData, coChangeTopData } = await import('./cochange.js');
1018
- const { formatCoChange, formatCoChangeTop } = await import('./commands/cochange.js');
1019
-
1020
- if (opts.analyze) {
1021
- const result = analyzeCoChanges(opts.db, {
1022
- since: opts.since || config.coChange?.since,
1023
- minSupport: opts.minSupport ? parseInt(opts.minSupport, 10) : config.coChange?.minSupport,
1024
- maxFilesPerCommit: config.coChange?.maxFilesPerCommit,
1025
- full: opts.full,
1026
- });
1027
- if (opts.json) {
1028
- console.log(JSON.stringify(result, null, 2));
1029
- } else if (result.error) {
1030
- console.error(result.error);
1031
- process.exit(1);
1032
- } else {
1033
- console.log(
1034
- `\nCo-change analysis complete: ${result.pairsFound} pairs from ${result.commitsScanned} commits (since: ${result.since})\n`,
1035
- );
1036
- }
1037
- return;
1038
- }
1039
-
1040
- const queryOpts = {
1041
- limit: parseInt(opts.limit, 10),
1042
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
1043
- minJaccard: opts.minJaccard ? parseFloat(opts.minJaccard) : config.coChange?.minJaccard,
1044
- noTests: resolveNoTests(opts),
1045
- };
1046
-
1047
- if (file) {
1048
- const data = coChangeData(file, opts.db, queryOpts);
1049
- if (!outputResult(data, 'partners', opts)) {
1050
- console.log(formatCoChange(data));
1051
- }
1052
- } else {
1053
- const data = coChangeTopData(opts.db, queryOpts);
1054
- if (!outputResult(data, 'pairs', opts)) {
1055
- console.log(formatCoChangeTop(data));
1056
- }
1057
- }
1058
- });
1059
-
1060
- QUERY_OPTS(
1061
- program
1062
- .command('flow [name]')
1063
- .description(
1064
- 'Trace execution flow forward from an entry point (route, command, event) through callees to leaves',
1065
- ),
1066
- )
1067
- .option('--list', 'List all entry points grouped by type')
1068
- .option('--depth <n>', 'Max forward traversal depth', '10')
1069
- .option('-f, --file <path>', 'Scope to a specific file (partial match)')
1070
- .option('-k, --kind <kind>', 'Filter by symbol kind')
1071
- .action(async (name, opts) => {
1072
- if (!name && !opts.list) {
1073
- console.error('Provide a function/entry point name or use --list to see all entry points.');
1074
- process.exit(1);
1075
- }
1076
- if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
1077
- console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1078
- process.exit(1);
1079
- }
1080
- const { flow } = await import('./commands/flow.js');
1081
- flow(name, opts.db, {
1082
- list: opts.list,
1083
- depth: parseInt(opts.depth, 10),
1084
- file: opts.file,
1085
- kind: opts.kind,
1086
- noTests: resolveNoTests(opts),
1087
- json: opts.json,
1088
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
1089
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
1090
- ndjson: opts.ndjson,
1091
- });
1092
- });
1093
-
1094
- QUERY_OPTS(
1095
- program
1096
- .command('sequence <name>')
1097
- .description(
1098
- 'Generate a Mermaid sequence diagram from call graph edges (participants = files)',
1099
- ),
1100
- )
1101
- .option('--depth <n>', 'Max forward traversal depth', '10')
1102
- .option('--dataflow', 'Annotate with parameter names and return arrows from dataflow table')
1103
- .option('-f, --file <path>', 'Scope to a specific file (partial match)')
1104
- .option('-k, --kind <kind>', 'Filter by symbol kind')
1105
- .action(async (name, opts) => {
1106
- if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
1107
- console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1108
- process.exit(1);
1109
- }
1110
- const { sequence } = await import('./commands/sequence.js');
1111
- sequence(name, opts.db, {
1112
- depth: parseInt(opts.depth, 10),
1113
- file: opts.file,
1114
- kind: opts.kind,
1115
- noTests: resolveNoTests(opts),
1116
- json: opts.json,
1117
- dataflow: opts.dataflow,
1118
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
1119
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
1120
- ndjson: opts.ndjson,
1121
- });
1122
- });
1123
-
1124
- QUERY_OPTS(
1125
- program
1126
- .command('dataflow <name>')
1127
- .description('Show data flow for a function: parameters, return consumers, mutations'),
1128
- )
1129
- .option('-f, --file <path>', 'Scope to file (partial match)')
1130
- .option('-k, --kind <kind>', 'Filter by symbol kind')
1131
- .option('--impact', 'Show data-dependent blast radius')
1132
- .option('--depth <n>', 'Max traversal depth', '5')
1133
- .action(async (name, opts) => {
1134
- if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
1135
- console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1136
- process.exit(1);
1137
- }
1138
- const { dataflow } = await import('./commands/dataflow.js');
1139
- dataflow(name, opts.db, {
1140
- file: opts.file,
1141
- kind: opts.kind,
1142
- noTests: resolveNoTests(opts),
1143
- json: opts.json,
1144
- ndjson: opts.ndjson,
1145
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
1146
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
1147
- impact: opts.impact,
1148
- depth: opts.depth,
1149
- });
1150
- });
1151
-
1152
- QUERY_OPTS(program.command('cfg <name>').description('Show control flow graph for a function'))
1153
- .option('--format <fmt>', 'Output format: text, dot, mermaid', 'text')
1154
- .option('-f, --file <path>', 'Scope to file (partial match)')
1155
- .option('-k, --kind <kind>', 'Filter by symbol kind')
1156
- .action(async (name, opts) => {
1157
- if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
1158
- console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1159
- process.exit(1);
1160
- }
1161
- const { cfg } = await import('./commands/cfg.js');
1162
- cfg(name, opts.db, {
1163
- format: opts.format,
1164
- file: opts.file,
1165
- kind: opts.kind,
1166
- noTests: resolveNoTests(opts),
1167
- json: opts.json,
1168
- ndjson: opts.ndjson,
1169
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
1170
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
1171
- });
1172
- });
1173
-
1174
- program
1175
- .command('complexity [target]')
1176
- .description('Show per-function complexity metrics (cognitive, cyclomatic, nesting depth, MI)')
1177
- .option('-d, --db <path>', 'Path to graph.db')
1178
- .option('-n, --limit <number>', 'Max results', '20')
1179
- .option(
1180
- '--sort <metric>',
1181
- 'Sort by: cognitive | cyclomatic | nesting | mi | volume | effort | bugs | loc',
1182
- 'cognitive',
1183
- )
1184
- .option('--above-threshold', 'Only functions exceeding warn thresholds')
1185
- .option('--health', 'Show health metrics (Halstead, MI) columns')
1186
- .option('-f, --file <path>', 'Scope to file (partial match)')
1187
- .option('-k, --kind <kind>', 'Filter by symbol kind')
1188
- .option('-T, --no-tests', 'Exclude test/spec files from results')
1189
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1190
- .option('-j, --json', 'Output as JSON')
1191
- .option('--offset <number>', 'Skip N results (default: 0)')
1192
- .option('--ndjson', 'Newline-delimited JSON output')
1193
- .action(async (target, opts) => {
1194
- if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
1195
- console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1196
- process.exit(1);
1197
- }
1198
- const { complexity } = await import('./commands/complexity.js');
1199
- complexity(opts.db, {
1200
- target,
1201
- limit: parseInt(opts.limit, 10),
1202
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
1203
- sort: opts.sort,
1204
- aboveThreshold: opts.aboveThreshold,
1205
- health: opts.health,
1206
- file: opts.file,
1207
- kind: opts.kind,
1208
- noTests: resolveNoTests(opts),
1209
- json: opts.json,
1210
- ndjson: opts.ndjson,
1211
- });
1212
- });
1213
-
1214
- QUERY_OPTS(
1215
- program
1216
- .command('ast [pattern]')
1217
- .description('Search stored AST nodes (calls, new, string, regex, throw, await) by pattern'),
1218
- )
1219
- .option('-k, --kind <kind>', 'Filter by AST node kind (call, new, string, regex, throw, await)')
1220
- .option('-f, --file <path>', 'Scope to file (partial match)')
1221
- .action(async (pattern, opts) => {
1222
- const { AST_NODE_KINDS, astQuery } = await import('./ast.js');
1223
- if (opts.kind && !AST_NODE_KINDS.includes(opts.kind)) {
1224
- console.error(`Invalid AST kind "${opts.kind}". Valid: ${AST_NODE_KINDS.join(', ')}`);
1225
- process.exit(1);
1226
- }
1227
- astQuery(pattern, opts.db, {
1228
- kind: opts.kind,
1229
- file: opts.file,
1230
- noTests: resolveNoTests(opts),
1231
- json: opts.json,
1232
- ndjson: opts.ndjson,
1233
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
1234
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
1235
- });
1236
- });
1237
-
1238
- QUERY_OPTS(
1239
- program
1240
- .command('communities')
1241
- .description('Detect natural module boundaries using Louvain community detection'),
1242
- )
1243
- .option('--functions', 'Function-level instead of file-level')
1244
- .option('--resolution <n>', 'Louvain resolution parameter (default 1.0)', '1.0')
1245
- .option('--drift', 'Show only drift analysis')
1246
- .action(async (opts) => {
1247
- const { communities } = await import('./commands/communities.js');
1248
- communities(opts.db, {
1249
- functions: opts.functions,
1250
- resolution: parseFloat(opts.resolution),
1251
- drift: opts.drift,
1252
- noTests: resolveNoTests(opts),
1253
- json: opts.json,
1254
- limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
1255
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
1256
- ndjson: opts.ndjson,
1257
- });
1258
- });
1259
-
1260
- program
1261
- .command('triage')
1262
- .description(
1263
- 'Ranked audit queue by composite risk score (connectivity + complexity + churn + role)',
1264
- )
1265
- .option('-d, --db <path>', 'Path to graph.db')
1266
- .option('-n, --limit <number>', 'Max results to return', '20')
1267
- .option(
1268
- '--level <level>',
1269
- 'Granularity: function (default) | file | directory. File/directory level shows hotspots',
1270
- 'function',
1271
- )
1272
- .option(
1273
- '--sort <metric>',
1274
- 'Sort metric: risk | complexity | churn | fan-in | mi (function level); fan-in | fan-out | density | coupling (file/directory level)',
1275
- 'risk',
1276
- )
1277
- .option('--min-score <score>', 'Only show symbols with risk score >= threshold')
1278
- .option('--role <role>', 'Filter by role (entry, core, utility, adapter, leaf, dead)')
1279
- .option('-f, --file <path>', 'Scope to a specific file (partial match)')
1280
- .option('-k, --kind <kind>', 'Filter by symbol kind (function, method, class)')
1281
- .option('-T, --no-tests', 'Exclude test/spec files from results')
1282
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1283
- .option('-j, --json', 'Output as JSON')
1284
- .option('--offset <number>', 'Skip N results (default: 0)')
1285
- .option('--ndjson', 'Newline-delimited JSON output')
1286
- .option('--weights <json>', 'Custom weights JSON (e.g. \'{"fanIn":1,"complexity":0}\')')
1287
- .action(async (opts) => {
1288
- if (opts.level === 'file' || opts.level === 'directory') {
1289
- // Delegate to hotspots for file/directory level
1290
- const { hotspotsData, formatHotspots } = await import('./commands/structure.js');
1291
- const metric = opts.sort === 'risk' ? 'fan-in' : opts.sort;
1292
- const data = hotspotsData(opts.db, {
1293
- metric,
1294
- level: opts.level,
1295
- limit: parseInt(opts.limit, 10),
1296
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
1297
- noTests: resolveNoTests(opts),
1298
- });
1299
- if (!outputResult(data, 'hotspots', opts)) {
1300
- console.log(formatHotspots(data));
1301
- }
1302
- return;
1303
- }
1304
-
1305
- if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
1306
- console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1307
- process.exit(1);
1308
- }
1309
- if (opts.role && !VALID_ROLES.includes(opts.role)) {
1310
- console.error(`Invalid role "${opts.role}". Valid: ${VALID_ROLES.join(', ')}`);
1311
- process.exit(1);
1312
- }
1313
- let weights;
1314
- if (opts.weights) {
1315
- try {
1316
- weights = JSON.parse(opts.weights);
1317
- } catch {
1318
- console.error('Invalid --weights JSON');
1319
- process.exit(1);
1320
- }
1321
- }
1322
- const { triage } = await import('./commands/triage.js');
1323
- triage(opts.db, {
1324
- limit: parseInt(opts.limit, 10),
1325
- offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
1326
- sort: opts.sort,
1327
- minScore: opts.minScore,
1328
- role: opts.role,
1329
- file: opts.file,
1330
- kind: opts.kind,
1331
- noTests: resolveNoTests(opts),
1332
- json: opts.json,
1333
- ndjson: opts.ndjson,
1334
- weights,
1335
- });
1336
- });
1337
-
1338
- program
1339
- .command('owners [target]')
1340
- .description('Show CODEOWNERS mapping for files and functions')
1341
- .option('-d, --db <path>', 'Path to graph.db')
1342
- .option('--owner <owner>', 'Filter to a specific owner')
1343
- .option('--boundary', 'Show cross-owner boundary edges')
1344
- .option('-f, --file <path>', 'Scope to a specific file')
1345
- .option('-k, --kind <kind>', 'Filter by symbol kind')
1346
- .option('-T, --no-tests', 'Exclude test/spec files')
1347
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1348
- .option('-j, --json', 'Output as JSON')
1349
- .action(async (target, opts) => {
1350
- const { owners } = await import('./commands/owners.js');
1351
- owners(opts.db, {
1352
- owner: opts.owner,
1353
- boundary: opts.boundary,
1354
- file: opts.file || target,
1355
- kind: opts.kind,
1356
- noTests: resolveNoTests(opts),
1357
- json: opts.json,
1358
- });
1359
- });
1360
-
1361
- program
1362
- .command('branch-compare <base> <target>')
1363
- .description('Compare code structure between two branches/refs')
1364
- .option('--depth <n>', 'Max transitive caller depth', '3')
1365
- .option('-T, --no-tests', 'Exclude test/spec files')
1366
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1367
- .option('-j, --json', 'Output as JSON')
1368
- .option('-f, --format <format>', 'Output format: text, mermaid, json', 'text')
1369
- .action(async (base, target, opts) => {
1370
- const { branchCompare } = await import('./commands/branch-compare.js');
1371
- await branchCompare(base, target, {
1372
- engine: program.opts().engine,
1373
- depth: parseInt(opts.depth, 10),
1374
- noTests: resolveNoTests(opts),
1375
- json: opts.json,
1376
- format: opts.format,
1377
- });
1378
- });
1379
-
1380
- program
1381
- .command('watch [dir]')
1382
- .description('Watch project for file changes and incrementally update the graph')
1383
- .action(async (dir) => {
1384
- const root = path.resolve(dir || '.');
1385
- const engine = program.opts().engine;
1386
- await watchProject(root, { engine });
1387
- });
1388
-
1389
- program
1390
- .command('info')
1391
- .description('Show codegraph engine info and diagnostics')
1392
- .action(async () => {
1393
- const { getNativePackageVersion, isNativeAvailable, loadNative } = await import('./native.js');
1394
- const { getActiveEngine } = await import('./parser.js');
1395
-
1396
- const engine = program.opts().engine;
1397
- const { name: activeName, version: activeVersion } = getActiveEngine({ engine });
1398
- const nativeAvailable = isNativeAvailable();
1399
-
1400
- console.log('\nCodegraph Diagnostics');
1401
- console.log('====================');
1402
- console.log(` Version : ${program.version()}`);
1403
- console.log(` Node.js : ${process.version}`);
1404
- console.log(` Platform : ${process.platform}-${process.arch}`);
1405
- console.log(` Native engine : ${nativeAvailable ? 'available' : 'unavailable'}`);
1406
- if (nativeAvailable) {
1407
- const native = loadNative();
1408
- const binaryVersion =
1409
- typeof native.engineVersion === 'function' ? native.engineVersion() : 'unknown';
1410
- const pkgVersion = getNativePackageVersion();
1411
- const knownBinaryVersion = binaryVersion !== 'unknown' ? binaryVersion : null;
1412
- if (pkgVersion && knownBinaryVersion && pkgVersion !== knownBinaryVersion) {
1413
- console.log(
1414
- ` Native version: ${pkgVersion} (binary reports ${knownBinaryVersion} — stale)`,
1415
- );
1416
- } else {
1417
- console.log(` Native version: ${pkgVersion ?? binaryVersion}`);
1418
- }
1419
- }
1420
- console.log(` Engine flag : --engine ${engine}`);
1421
- console.log(` Active engine : ${activeName}${activeVersion ? ` (v${activeVersion})` : ''}`);
1422
- console.log();
1423
-
1424
- // Build metadata from DB
1425
- try {
1426
- const { findDbPath, getBuildMeta } = await import('./db.js');
1427
- const Database = (await import('better-sqlite3')).default;
1428
- const dbPath = findDbPath();
1429
- const fs = await import('node:fs');
1430
- if (fs.existsSync(dbPath)) {
1431
- const db = new Database(dbPath, { readonly: true });
1432
- const buildEngine = getBuildMeta(db, 'engine');
1433
- const buildVersion = getBuildMeta(db, 'codegraph_version');
1434
- const builtAt = getBuildMeta(db, 'built_at');
1435
- db.close();
1436
-
1437
- if (buildEngine || buildVersion || builtAt) {
1438
- console.log('Build metadata');
1439
- console.log('──────────────');
1440
- if (buildEngine) console.log(` Engine : ${buildEngine}`);
1441
- if (buildVersion) console.log(` Version : ${buildVersion}`);
1442
- if (builtAt) console.log(` Built at : ${builtAt}`);
1443
-
1444
- if (buildVersion && buildVersion !== program.version()) {
1445
- console.log(
1446
- ` ⚠ DB was built with v${buildVersion}, current is v${program.version()}. Consider: codegraph build --no-incremental`,
1447
- );
1448
- }
1449
- if (buildEngine && buildEngine !== activeName) {
1450
- console.log(
1451
- ` ⚠ DB was built with ${buildEngine} engine, active is ${activeName}. Consider: codegraph build --no-incremental`,
1452
- );
1453
- }
1454
- console.log();
1455
- }
1456
- }
1457
- } catch {
1458
- /* diagnostics must never crash */
1459
- }
1460
- });
1461
-
1462
- program
1463
- .command('batch <command> [targets...]')
1464
- .description(
1465
- `Run a query against multiple targets in one call. Output is always JSON.\nValid commands: ${Object.keys(BATCH_COMMANDS).join(', ')}`,
1466
- )
1467
- .option('-d, --db <path>', 'Path to graph.db')
1468
- .option('--from-file <path>', 'Read targets from file (JSON array or newline-delimited)')
1469
- .option('--stdin', 'Read targets from stdin (JSON array)')
1470
- .option('--depth <n>', 'Traversal depth passed to underlying command')
1471
- .option('-f, --file <path>', 'Scope to file (partial match)')
1472
- .option('-k, --kind <kind>', 'Filter by symbol kind')
1473
- .option('-T, --no-tests', 'Exclude test/spec files from results')
1474
- .option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
1475
- .action(async (command, positionalTargets, opts) => {
1476
- if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
1477
- console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
1478
- process.exit(1);
1479
- }
1480
-
1481
- let targets;
1482
- try {
1483
- if (opts.fromFile) {
1484
- const raw = fs.readFileSync(opts.fromFile, 'utf-8').trim();
1485
- if (raw.startsWith('[')) {
1486
- targets = JSON.parse(raw);
1487
- } else {
1488
- targets = raw.split(/\r?\n/).filter(Boolean);
1489
- }
1490
- } else if (opts.stdin) {
1491
- const chunks = [];
1492
- for await (const chunk of process.stdin) chunks.push(chunk);
1493
- const raw = Buffer.concat(chunks).toString('utf-8').trim();
1494
- targets = raw.startsWith('[') ? JSON.parse(raw) : raw.split(/\r?\n/).filter(Boolean);
1495
- } else {
1496
- targets = splitTargets(positionalTargets);
1497
- }
1498
- } catch (err) {
1499
- console.error(`Failed to parse targets: ${err.message}`);
1500
- process.exit(1);
1501
- }
1502
-
1503
- if (!targets || targets.length === 0) {
1504
- console.error('No targets provided. Pass targets as arguments, --from-file, or --stdin.');
1505
- process.exit(1);
1506
- }
1507
-
1508
- const batchOpts = {
1509
- depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
1510
- file: opts.file,
1511
- kind: opts.kind,
1512
- noTests: resolveNoTests(opts),
1513
- };
1514
-
1515
- // Multi-command mode: items from --from-file / --stdin may be objects with { command, target }
1516
- const isMulti = targets.length > 0 && typeof targets[0] === 'object' && targets[0].command;
1517
- if (isMulti) {
1518
- const data = multiBatchData(targets, opts.db, batchOpts);
1519
- console.log(JSON.stringify(data, null, 2));
1520
- } else {
1521
- batch(command, targets, opts.db, batchOpts);
1522
- }
1523
- });
1524
-
1525
- program.parse();