@optave/codegraph 3.1.2 → 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.
- package/README.md +19 -21
- package/package.json +10 -7
- package/src/analysis/context.js +408 -0
- package/src/analysis/dependencies.js +341 -0
- package/src/analysis/exports.js +130 -0
- package/src/analysis/impact.js +463 -0
- package/src/analysis/module-map.js +322 -0
- package/src/analysis/roles.js +45 -0
- package/src/analysis/symbol-lookup.js +232 -0
- package/src/ast-analysis/shared.js +5 -4
- package/src/batch.js +2 -1
- package/src/builder/context.js +85 -0
- package/src/builder/helpers.js +218 -0
- package/src/builder/incremental.js +178 -0
- package/src/builder/pipeline.js +130 -0
- package/src/builder/stages/build-edges.js +297 -0
- package/src/builder/stages/build-structure.js +113 -0
- package/src/builder/stages/collect-files.js +44 -0
- package/src/builder/stages/detect-changes.js +413 -0
- package/src/builder/stages/finalize.js +139 -0
- package/src/builder/stages/insert-nodes.js +195 -0
- package/src/builder/stages/parse-files.js +28 -0
- package/src/builder/stages/resolve-imports.js +143 -0
- package/src/builder/stages/run-analyses.js +44 -0
- package/src/builder.js +10 -1472
- package/src/cfg.js +1 -2
- package/src/cli/commands/ast.js +26 -0
- package/src/cli/commands/audit.js +46 -0
- package/src/cli/commands/batch.js +68 -0
- package/src/cli/commands/branch-compare.js +21 -0
- package/src/cli/commands/build.js +26 -0
- package/src/cli/commands/cfg.js +30 -0
- package/src/cli/commands/check.js +79 -0
- package/src/cli/commands/children.js +31 -0
- package/src/cli/commands/co-change.js +65 -0
- package/src/cli/commands/communities.js +23 -0
- package/src/cli/commands/complexity.js +45 -0
- package/src/cli/commands/context.js +34 -0
- package/src/cli/commands/cycles.js +28 -0
- package/src/cli/commands/dataflow.js +32 -0
- package/src/cli/commands/deps.js +16 -0
- package/src/cli/commands/diff-impact.js +30 -0
- package/src/cli/commands/embed.js +30 -0
- package/src/cli/commands/export.js +75 -0
- package/src/cli/commands/exports.js +18 -0
- package/src/cli/commands/flow.js +36 -0
- package/src/cli/commands/fn-impact.js +30 -0
- package/src/cli/commands/impact.js +16 -0
- package/src/cli/commands/info.js +76 -0
- package/src/cli/commands/map.js +19 -0
- package/src/cli/commands/mcp.js +18 -0
- package/src/cli/commands/models.js +19 -0
- package/src/cli/commands/owners.js +25 -0
- package/src/cli/commands/path.js +36 -0
- package/src/cli/commands/plot.js +80 -0
- package/src/cli/commands/query.js +49 -0
- package/src/cli/commands/registry.js +100 -0
- package/src/cli/commands/roles.js +34 -0
- package/src/cli/commands/search.js +42 -0
- package/src/cli/commands/sequence.js +32 -0
- package/src/cli/commands/snapshot.js +61 -0
- package/src/cli/commands/stats.js +15 -0
- package/src/cli/commands/structure.js +32 -0
- package/src/cli/commands/triage.js +78 -0
- package/src/cli/commands/watch.js +12 -0
- package/src/cli/commands/where.js +24 -0
- package/src/cli/index.js +118 -0
- package/src/cli/shared/options.js +39 -0
- package/src/cli/shared/output.js +1 -0
- package/src/cli.js +11 -1514
- package/src/commands/check.js +5 -5
- package/src/commands/manifesto.js +3 -3
- package/src/commands/structure.js +1 -1
- package/src/communities.js +15 -87
- package/src/complexity.js +1 -1
- package/src/cycles.js +30 -85
- package/src/dataflow.js +1 -2
- package/src/db/connection.js +4 -4
- package/src/db/migrations.js +41 -0
- package/src/db/query-builder.js +6 -5
- package/src/db/repository/base.js +201 -0
- package/src/db/repository/cached-stmt.js +19 -0
- package/src/db/repository/cfg.js +27 -38
- package/src/db/repository/cochange.js +16 -3
- package/src/db/repository/complexity.js +11 -6
- package/src/db/repository/dataflow.js +6 -1
- package/src/db/repository/edges.js +120 -98
- package/src/db/repository/embeddings.js +14 -3
- package/src/db/repository/graph-read.js +32 -9
- package/src/db/repository/in-memory-repository.js +584 -0
- package/src/db/repository/index.js +6 -1
- package/src/db/repository/nodes.js +110 -40
- package/src/db/repository/sqlite-repository.js +219 -0
- package/src/db.js +5 -0
- package/src/embeddings/generator.js +163 -0
- package/src/embeddings/index.js +13 -0
- package/src/embeddings/models.js +218 -0
- package/src/embeddings/search/cli-formatter.js +151 -0
- package/src/embeddings/search/filters.js +46 -0
- package/src/embeddings/search/hybrid.js +121 -0
- package/src/embeddings/search/keyword.js +68 -0
- package/src/embeddings/search/prepare.js +66 -0
- package/src/embeddings/search/semantic.js +145 -0
- package/src/embeddings/stores/fts5.js +27 -0
- package/src/embeddings/stores/sqlite-blob.js +24 -0
- package/src/embeddings/strategies/source.js +14 -0
- package/src/embeddings/strategies/structured.js +43 -0
- package/src/embeddings/strategies/text-utils.js +43 -0
- package/src/errors.js +78 -0
- package/src/export.js +217 -520
- package/src/extractors/csharp.js +10 -2
- package/src/extractors/go.js +3 -1
- package/src/extractors/helpers.js +71 -0
- package/src/extractors/java.js +9 -2
- package/src/extractors/javascript.js +38 -1
- package/src/extractors/php.js +3 -1
- package/src/extractors/python.js +14 -3
- package/src/extractors/rust.js +3 -1
- package/src/graph/algorithms/bfs.js +49 -0
- package/src/graph/algorithms/centrality.js +16 -0
- package/src/graph/algorithms/index.js +5 -0
- package/src/graph/algorithms/louvain.js +26 -0
- package/src/graph/algorithms/shortest-path.js +41 -0
- package/src/graph/algorithms/tarjan.js +49 -0
- package/src/graph/builders/dependency.js +91 -0
- package/src/graph/builders/index.js +3 -0
- package/src/graph/builders/structure.js +40 -0
- package/src/graph/builders/temporal.js +33 -0
- package/src/graph/classifiers/index.js +2 -0
- package/src/graph/classifiers/risk.js +85 -0
- package/src/graph/classifiers/roles.js +64 -0
- package/src/graph/index.js +13 -0
- package/src/graph/model.js +230 -0
- package/src/index.js +33 -204
- package/src/infrastructure/result-formatter.js +2 -21
- package/src/mcp/index.js +2 -0
- package/src/mcp/middleware.js +26 -0
- package/src/mcp/server.js +128 -0
- package/src/mcp/tool-registry.js +801 -0
- package/src/mcp/tools/ast-query.js +14 -0
- package/src/mcp/tools/audit.js +21 -0
- package/src/mcp/tools/batch-query.js +11 -0
- package/src/mcp/tools/branch-compare.js +10 -0
- package/src/mcp/tools/cfg.js +21 -0
- package/src/mcp/tools/check.js +43 -0
- package/src/mcp/tools/co-changes.js +20 -0
- package/src/mcp/tools/code-owners.js +12 -0
- package/src/mcp/tools/communities.js +15 -0
- package/src/mcp/tools/complexity.js +18 -0
- package/src/mcp/tools/context.js +17 -0
- package/src/mcp/tools/dataflow.js +26 -0
- package/src/mcp/tools/diff-impact.js +24 -0
- package/src/mcp/tools/execution-flow.js +26 -0
- package/src/mcp/tools/export-graph.js +57 -0
- package/src/mcp/tools/file-deps.js +12 -0
- package/src/mcp/tools/file-exports.js +13 -0
- package/src/mcp/tools/find-cycles.js +15 -0
- package/src/mcp/tools/fn-impact.js +15 -0
- package/src/mcp/tools/impact-analysis.js +12 -0
- package/src/mcp/tools/index.js +71 -0
- package/src/mcp/tools/list-functions.js +14 -0
- package/src/mcp/tools/list-repos.js +11 -0
- package/src/mcp/tools/module-map.js +6 -0
- package/src/mcp/tools/node-roles.js +14 -0
- package/src/mcp/tools/path.js +12 -0
- package/src/mcp/tools/query.js +30 -0
- package/src/mcp/tools/semantic-search.js +65 -0
- package/src/mcp/tools/sequence.js +17 -0
- package/src/mcp/tools/structure.js +15 -0
- package/src/mcp/tools/symbol-children.js +14 -0
- package/src/mcp/tools/triage.js +35 -0
- package/src/mcp/tools/where.js +13 -0
- package/src/mcp.js +2 -1470
- package/src/native.js +34 -10
- package/src/parser.js +53 -2
- package/src/presentation/colors.js +44 -0
- package/src/presentation/export.js +444 -0
- package/src/presentation/result-formatter.js +21 -0
- package/src/presentation/sequence-renderer.js +43 -0
- package/src/presentation/table.js +47 -0
- package/src/presentation/viewer.js +634 -0
- package/src/queries.js +35 -2276
- package/src/resolve.js +1 -1
- package/src/sequence.js +2 -38
- package/src/shared/file-utils.js +153 -0
- package/src/shared/generators.js +125 -0
- package/src/shared/hierarchy.js +27 -0
- package/src/shared/normalize.js +59 -0
- package/src/snapshot.js +6 -5
- package/src/structure.js +15 -40
- package/src/triage.js +20 -72
- package/src/viewer.js +35 -656
- package/src/watcher.js +8 -148
- package/src/embedder.js +0 -1097
package/src/cli.js
CHANGED
|
@@ -1,1517 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 { 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 nativeVersion =
|
|
1409
|
-
typeof native.engineVersion === 'function' ? native.engineVersion() : 'unknown';
|
|
1410
|
-
console.log(` Native version: ${nativeVersion}`);
|
|
1411
|
-
}
|
|
1412
|
-
console.log(` Engine flag : --engine ${engine}`);
|
|
1413
|
-
console.log(` Active engine : ${activeName}${activeVersion ? ` (v${activeVersion})` : ''}`);
|
|
1414
|
-
console.log();
|
|
1415
|
-
|
|
1416
|
-
// Build metadata from DB
|
|
1417
|
-
try {
|
|
1418
|
-
const { findDbPath, getBuildMeta } = await import('./db.js');
|
|
1419
|
-
const Database = (await import('better-sqlite3')).default;
|
|
1420
|
-
const dbPath = findDbPath();
|
|
1421
|
-
const fs = await import('node:fs');
|
|
1422
|
-
if (fs.existsSync(dbPath)) {
|
|
1423
|
-
const db = new Database(dbPath, { readonly: true });
|
|
1424
|
-
const buildEngine = getBuildMeta(db, 'engine');
|
|
1425
|
-
const buildVersion = getBuildMeta(db, 'codegraph_version');
|
|
1426
|
-
const builtAt = getBuildMeta(db, 'built_at');
|
|
1427
|
-
db.close();
|
|
1428
|
-
|
|
1429
|
-
if (buildEngine || buildVersion || builtAt) {
|
|
1430
|
-
console.log('Build metadata');
|
|
1431
|
-
console.log('──────────────');
|
|
1432
|
-
if (buildEngine) console.log(` Engine : ${buildEngine}`);
|
|
1433
|
-
if (buildVersion) console.log(` Version : ${buildVersion}`);
|
|
1434
|
-
if (builtAt) console.log(` Built at : ${builtAt}`);
|
|
1435
|
-
|
|
1436
|
-
if (buildVersion && buildVersion !== program.version()) {
|
|
1437
|
-
console.log(
|
|
1438
|
-
` ⚠ DB was built with v${buildVersion}, current is v${program.version()}. Consider: codegraph build --no-incremental`,
|
|
1439
|
-
);
|
|
1440
|
-
}
|
|
1441
|
-
if (buildEngine && buildEngine !== activeName) {
|
|
1442
|
-
console.log(
|
|
1443
|
-
` ⚠ DB was built with ${buildEngine} engine, active is ${activeName}. Consider: codegraph build --no-incremental`,
|
|
1444
|
-
);
|
|
1445
|
-
}
|
|
1446
|
-
console.log();
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
} catch {
|
|
1450
|
-
/* diagnostics must never crash */
|
|
1451
|
-
}
|
|
1452
|
-
});
|
|
1453
|
-
|
|
1454
|
-
program
|
|
1455
|
-
.command('batch <command> [targets...]')
|
|
1456
|
-
.description(
|
|
1457
|
-
`Run a query against multiple targets in one call. Output is always JSON.\nValid commands: ${Object.keys(BATCH_COMMANDS).join(', ')}`,
|
|
1458
|
-
)
|
|
1459
|
-
.option('-d, --db <path>', 'Path to graph.db')
|
|
1460
|
-
.option('--from-file <path>', 'Read targets from file (JSON array or newline-delimited)')
|
|
1461
|
-
.option('--stdin', 'Read targets from stdin (JSON array)')
|
|
1462
|
-
.option('--depth <n>', 'Traversal depth passed to underlying command')
|
|
1463
|
-
.option('-f, --file <path>', 'Scope to file (partial match)')
|
|
1464
|
-
.option('-k, --kind <kind>', 'Filter by symbol kind')
|
|
1465
|
-
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
1466
|
-
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
1467
|
-
.action(async (command, positionalTargets, opts) => {
|
|
1468
|
-
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
1469
|
-
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
1470
|
-
process.exit(1);
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
let targets;
|
|
1474
|
-
try {
|
|
1475
|
-
if (opts.fromFile) {
|
|
1476
|
-
const raw = fs.readFileSync(opts.fromFile, 'utf-8').trim();
|
|
1477
|
-
if (raw.startsWith('[')) {
|
|
1478
|
-
targets = JSON.parse(raw);
|
|
1479
|
-
} else {
|
|
1480
|
-
targets = raw.split(/\r?\n/).filter(Boolean);
|
|
1481
|
-
}
|
|
1482
|
-
} else if (opts.stdin) {
|
|
1483
|
-
const chunks = [];
|
|
1484
|
-
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
1485
|
-
const raw = Buffer.concat(chunks).toString('utf-8').trim();
|
|
1486
|
-
targets = raw.startsWith('[') ? JSON.parse(raw) : raw.split(/\r?\n/).filter(Boolean);
|
|
1487
|
-
} else {
|
|
1488
|
-
targets = splitTargets(positionalTargets);
|
|
1489
|
-
}
|
|
1490
|
-
} catch (err) {
|
|
1491
|
-
console.error(`Failed to parse targets: ${err.message}`);
|
|
1492
|
-
process.exit(1);
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
if (!targets || targets.length === 0) {
|
|
1496
|
-
console.error('No targets provided. Pass targets as arguments, --from-file, or --stdin.');
|
|
1497
|
-
process.exit(1);
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
const batchOpts = {
|
|
1501
|
-
depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
|
|
1502
|
-
file: opts.file,
|
|
1503
|
-
kind: opts.kind,
|
|
1504
|
-
noTests: resolveNoTests(opts),
|
|
1505
|
-
};
|
|
1506
|
-
|
|
1507
|
-
// Multi-command mode: items from --from-file / --stdin may be objects with { command, target }
|
|
1508
|
-
const isMulti = targets.length > 0 && typeof targets[0] === 'object' && targets[0].command;
|
|
1509
|
-
if (isMulti) {
|
|
1510
|
-
const data = multiBatchData(targets, opts.db, batchOpts);
|
|
1511
|
-
console.log(JSON.stringify(data, null, 2));
|
|
1512
|
-
} else {
|
|
1513
|
-
batch(command, targets, opts.db, batchOpts);
|
|
1514
|
-
}
|
|
1515
|
-
});
|
|
1516
|
-
|
|
1517
|
-
program.parse();
|