@optave/codegraph 2.6.0 → 3.0.1
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 +111 -54
- package/package.json +5 -5
- package/src/ast.js +418 -0
- package/src/batch.js +93 -3
- package/src/builder.js +371 -103
- package/src/cfg.js +1452 -0
- package/src/change-journal.js +130 -0
- package/src/cli.js +415 -139
- package/src/complexity.js +8 -8
- package/src/dataflow.js +1190 -0
- package/src/db.js +96 -0
- package/src/embedder.js +16 -16
- package/src/export.js +305 -0
- package/src/extractors/csharp.js +64 -1
- package/src/extractors/go.js +66 -1
- package/src/extractors/hcl.js +22 -0
- package/src/extractors/java.js +61 -1
- package/src/extractors/javascript.js +193 -0
- package/src/extractors/php.js +79 -0
- package/src/extractors/python.js +134 -0
- package/src/extractors/ruby.js +89 -0
- package/src/extractors/rust.js +71 -1
- package/src/flow.js +5 -2
- package/src/index.js +52 -4
- package/src/mcp.js +403 -222
- package/src/paginate.js +3 -3
- package/src/parser.js +24 -0
- package/src/queries.js +362 -36
- package/src/structure.js +64 -8
- package/src/viewer.js +948 -0
- package/src/watcher.js +36 -1
package/src/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import { audit } from './audit.js';
|
|
7
|
-
import { BATCH_COMMANDS, batch } from './batch.js';
|
|
7
|
+
import { BATCH_COMMANDS, batch, multiBatchData, splitTargets } from './batch.js';
|
|
8
8
|
import { buildGraph } from './builder.js';
|
|
9
9
|
import { loadConfig } from './config.js';
|
|
10
10
|
import { findCycles, formatCycles } from './cycles.js';
|
|
@@ -16,20 +16,28 @@ import {
|
|
|
16
16
|
MODELS,
|
|
17
17
|
search,
|
|
18
18
|
} from './embedder.js';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
exportDOT,
|
|
21
|
+
exportGraphML,
|
|
22
|
+
exportGraphSON,
|
|
23
|
+
exportJSON,
|
|
24
|
+
exportMermaid,
|
|
25
|
+
exportNeo4jCSV,
|
|
26
|
+
} from './export.js';
|
|
20
27
|
import { setVerbose } from './logger.js';
|
|
21
28
|
import { printNdjson } from './paginate.js';
|
|
22
29
|
import {
|
|
23
|
-
|
|
30
|
+
children,
|
|
24
31
|
context,
|
|
25
32
|
diffImpact,
|
|
33
|
+
EVERY_SYMBOL_KIND,
|
|
26
34
|
explain,
|
|
27
35
|
fileDeps,
|
|
36
|
+
fileExports,
|
|
28
37
|
fnDeps,
|
|
29
38
|
fnImpact,
|
|
30
39
|
impactAnalysis,
|
|
31
40
|
moduleMap,
|
|
32
|
-
queryName,
|
|
33
41
|
roles,
|
|
34
42
|
stats,
|
|
35
43
|
symbolPath,
|
|
@@ -97,16 +105,35 @@ program
|
|
|
97
105
|
.command('build [dir]')
|
|
98
106
|
.description('Parse repo and build graph in .codegraph/graph.db')
|
|
99
107
|
.option('--no-incremental', 'Force full rebuild (ignore file hashes)')
|
|
108
|
+
.option('--no-ast', 'Skip AST node extraction (calls, new, string, regex, throw, await)')
|
|
109
|
+
.option('--no-complexity', 'Skip complexity metrics computation')
|
|
110
|
+
.option('--no-dataflow', 'Skip data flow edge extraction')
|
|
111
|
+
.option('--no-cfg', 'Skip control flow graph building')
|
|
100
112
|
.action(async (dir, opts) => {
|
|
101
113
|
const root = path.resolve(dir || '.');
|
|
102
114
|
const engine = program.opts().engine;
|
|
103
|
-
await buildGraph(root, {
|
|
115
|
+
await buildGraph(root, {
|
|
116
|
+
incremental: opts.incremental,
|
|
117
|
+
ast: opts.ast,
|
|
118
|
+
complexity: opts.complexity,
|
|
119
|
+
engine,
|
|
120
|
+
dataflow: opts.dataflow,
|
|
121
|
+
cfg: opts.cfg,
|
|
122
|
+
});
|
|
104
123
|
});
|
|
105
124
|
|
|
106
125
|
program
|
|
107
126
|
.command('query <name>')
|
|
108
|
-
.description('
|
|
127
|
+
.description('Function-level dependency chain or shortest path between symbols')
|
|
109
128
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
129
|
+
.option('--depth <n>', 'Transitive caller depth', '3')
|
|
130
|
+
.option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
|
|
131
|
+
.option('-k, --kind <kind>', 'Filter to a specific symbol kind')
|
|
132
|
+
.option('--path <to>', 'Path mode: find shortest path to <to>')
|
|
133
|
+
.option('--kinds <kinds>', 'Path mode: comma-separated edge kinds to follow (default: calls)')
|
|
134
|
+
.option('--reverse', 'Path mode: follow edges backward')
|
|
135
|
+
.option('--from-file <path>', 'Path mode: disambiguate source symbol by file')
|
|
136
|
+
.option('--to-file <path>', 'Path mode: disambiguate target symbol by file')
|
|
110
137
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
111
138
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
112
139
|
.option('-j, --json', 'Output as JSON')
|
|
@@ -114,12 +141,63 @@ program
|
|
|
114
141
|
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
115
142
|
.option('--ndjson', 'Newline-delimited JSON output')
|
|
116
143
|
.action((name, opts) => {
|
|
117
|
-
|
|
144
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
145
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
if (opts.path) {
|
|
149
|
+
console.error('Note: "query --path" is deprecated, use "codegraph path <from> <to>" instead');
|
|
150
|
+
symbolPath(name, opts.path, opts.db, {
|
|
151
|
+
maxDepth: opts.depth ? parseInt(opts.depth, 10) : 10,
|
|
152
|
+
edgeKinds: opts.kinds ? opts.kinds.split(',').map((s) => s.trim()) : undefined,
|
|
153
|
+
reverse: opts.reverse,
|
|
154
|
+
fromFile: opts.fromFile,
|
|
155
|
+
toFile: opts.toFile,
|
|
156
|
+
kind: opts.kind,
|
|
157
|
+
noTests: resolveNoTests(opts),
|
|
158
|
+
json: opts.json,
|
|
159
|
+
});
|
|
160
|
+
} else {
|
|
161
|
+
fnDeps(name, opts.db, {
|
|
162
|
+
depth: parseInt(opts.depth, 10),
|
|
163
|
+
file: opts.file,
|
|
164
|
+
kind: opts.kind,
|
|
165
|
+
noTests: resolveNoTests(opts),
|
|
166
|
+
json: opts.json,
|
|
167
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
168
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
169
|
+
ndjson: opts.ndjson,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
program
|
|
175
|
+
.command('path <from> <to>')
|
|
176
|
+
.description('Find shortest path between two symbols')
|
|
177
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
178
|
+
.option('--reverse', 'Follow edges backward')
|
|
179
|
+
.option('--kinds <kinds>', 'Comma-separated edge kinds to follow (default: calls)')
|
|
180
|
+
.option('--from-file <path>', 'Disambiguate source symbol by file')
|
|
181
|
+
.option('--to-file <path>', 'Disambiguate target symbol by file')
|
|
182
|
+
.option('--depth <n>', 'Max traversal depth', '10')
|
|
183
|
+
.option('-k, --kind <kind>', 'Filter to a specific symbol kind')
|
|
184
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
185
|
+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
186
|
+
.option('-j, --json', 'Output as JSON')
|
|
187
|
+
.action((from, to, opts) => {
|
|
188
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
189
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
symbolPath(from, to, opts.db, {
|
|
193
|
+
maxDepth: opts.depth ? parseInt(opts.depth, 10) : 10,
|
|
194
|
+
edgeKinds: opts.kinds ? opts.kinds.split(',').map((s) => s.trim()) : undefined,
|
|
195
|
+
reverse: opts.reverse,
|
|
196
|
+
fromFile: opts.fromFile,
|
|
197
|
+
toFile: opts.toFile,
|
|
198
|
+
kind: opts.kind,
|
|
118
199
|
noTests: resolveNoTests(opts),
|
|
119
200
|
json: opts.json,
|
|
120
|
-
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
121
|
-
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
122
|
-
ndjson: opts.ndjson,
|
|
123
201
|
});
|
|
124
202
|
});
|
|
125
203
|
|
|
@@ -190,27 +268,17 @@ program
|
|
|
190
268
|
});
|
|
191
269
|
|
|
192
270
|
program
|
|
193
|
-
.command('
|
|
194
|
-
.description('
|
|
271
|
+
.command('exports <file>')
|
|
272
|
+
.description('Show exported symbols with per-symbol consumers (who calls each export)')
|
|
195
273
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
196
|
-
.option('--depth <n>', 'Transitive caller depth', '3')
|
|
197
|
-
.option('-f, --file <path>', 'Scope search to functions in this file (partial match)')
|
|
198
|
-
.option('-k, --kind <kind>', 'Filter to a specific symbol kind')
|
|
199
274
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
200
275
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
201
276
|
.option('-j, --json', 'Output as JSON')
|
|
202
277
|
.option('--limit <number>', 'Max results to return')
|
|
203
278
|
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
204
279
|
.option('--ndjson', 'Newline-delimited JSON output')
|
|
205
|
-
.action((
|
|
206
|
-
|
|
207
|
-
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
|
|
208
|
-
process.exit(1);
|
|
209
|
-
}
|
|
210
|
-
fnDeps(name, opts.db, {
|
|
211
|
-
depth: parseInt(opts.depth, 10),
|
|
212
|
-
file: opts.file,
|
|
213
|
-
kind: opts.kind,
|
|
280
|
+
.action((file, opts) => {
|
|
281
|
+
fileExports(file, opts.db, {
|
|
214
282
|
noTests: resolveNoTests(opts),
|
|
215
283
|
json: opts.json,
|
|
216
284
|
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
@@ -233,8 +301,8 @@ program
|
|
|
233
301
|
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
234
302
|
.option('--ndjson', 'Newline-delimited JSON output')
|
|
235
303
|
.action((name, opts) => {
|
|
236
|
-
if (opts.kind && !
|
|
237
|
-
console.error(`Invalid kind "${opts.kind}". Valid: ${
|
|
304
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
305
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
238
306
|
process.exit(1);
|
|
239
307
|
}
|
|
240
308
|
fnImpact(name, opts.db, {
|
|
@@ -249,36 +317,6 @@ program
|
|
|
249
317
|
});
|
|
250
318
|
});
|
|
251
319
|
|
|
252
|
-
program
|
|
253
|
-
.command('path <from> <to>')
|
|
254
|
-
.description('Find shortest path between two symbols (A calls...calls B)')
|
|
255
|
-
.option('-d, --db <path>', 'Path to graph.db')
|
|
256
|
-
.option('--max-depth <n>', 'Maximum BFS depth', '10')
|
|
257
|
-
.option('--kinds <kinds>', 'Comma-separated edge kinds to follow (default: calls)')
|
|
258
|
-
.option('--reverse', 'Follow edges backward (B is called by...called by A)')
|
|
259
|
-
.option('--from-file <path>', 'Disambiguate source symbol by file (partial match)')
|
|
260
|
-
.option('--to-file <path>', 'Disambiguate target symbol by file (partial match)')
|
|
261
|
-
.option('-k, --kind <kind>', 'Filter both symbols by kind')
|
|
262
|
-
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
263
|
-
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
264
|
-
.option('-j, --json', 'Output as JSON')
|
|
265
|
-
.action((from, to, opts) => {
|
|
266
|
-
if (opts.kind && !ALL_SYMBOL_KINDS.includes(opts.kind)) {
|
|
267
|
-
console.error(`Invalid kind "${opts.kind}". Valid: ${ALL_SYMBOL_KINDS.join(', ')}`);
|
|
268
|
-
process.exit(1);
|
|
269
|
-
}
|
|
270
|
-
symbolPath(from, to, opts.db, {
|
|
271
|
-
maxDepth: parseInt(opts.maxDepth, 10),
|
|
272
|
-
edgeKinds: opts.kinds ? opts.kinds.split(',').map((s) => s.trim()) : undefined,
|
|
273
|
-
reverse: opts.reverse,
|
|
274
|
-
fromFile: opts.fromFile,
|
|
275
|
-
toFile: opts.toFile,
|
|
276
|
-
kind: opts.kind,
|
|
277
|
-
noTests: resolveNoTests(opts),
|
|
278
|
-
json: opts.json,
|
|
279
|
-
});
|
|
280
|
-
});
|
|
281
|
-
|
|
282
320
|
program
|
|
283
321
|
.command('context <name>')
|
|
284
322
|
.description('Full context for a function: source, deps, callers, tests, signature')
|
|
@@ -295,8 +333,8 @@ program
|
|
|
295
333
|
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
296
334
|
.option('--ndjson', 'Newline-delimited JSON output')
|
|
297
335
|
.action((name, opts) => {
|
|
298
|
-
if (opts.kind && !
|
|
299
|
-
console.error(`Invalid kind "${opts.kind}". Valid: ${
|
|
336
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
337
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
300
338
|
process.exit(1);
|
|
301
339
|
}
|
|
302
340
|
context(name, opts.db, {
|
|
@@ -314,24 +352,27 @@ program
|
|
|
314
352
|
});
|
|
315
353
|
|
|
316
354
|
program
|
|
317
|
-
.command('
|
|
318
|
-
.description('
|
|
355
|
+
.command('children <name>')
|
|
356
|
+
.description('List parameters, properties, and constants of a symbol')
|
|
319
357
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
320
|
-
.option('--
|
|
358
|
+
.option('-f, --file <path>', 'Scope search to symbols in this file (partial match)')
|
|
359
|
+
.option('-k, --kind <kind>', 'Filter to a specific symbol kind')
|
|
321
360
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
322
|
-
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
323
361
|
.option('-j, --json', 'Output as JSON')
|
|
324
362
|
.option('--limit <number>', 'Max results to return')
|
|
325
363
|
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
326
|
-
.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
364
|
+
.action((name, opts) => {
|
|
365
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
366
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
children(name, opts.db, {
|
|
370
|
+
file: opts.file,
|
|
371
|
+
kind: opts.kind,
|
|
330
372
|
noTests: resolveNoTests(opts),
|
|
331
373
|
json: opts.json,
|
|
332
374
|
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
333
375
|
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
334
|
-
ndjson: opts.ndjson,
|
|
335
376
|
});
|
|
336
377
|
});
|
|
337
378
|
|
|
@@ -339,17 +380,32 @@ program
|
|
|
339
380
|
.command('audit <target>')
|
|
340
381
|
.description('Composite report: explain + impact + health metrics per function')
|
|
341
382
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
342
|
-
.option('--
|
|
383
|
+
.option('--quick', 'Structural summary only (skip impact analysis and health metrics)')
|
|
384
|
+
.option('--depth <n>', 'Impact/explain depth', '3')
|
|
343
385
|
.option('-f, --file <path>', 'Scope to file (partial match)')
|
|
344
386
|
.option('-k, --kind <kind>', 'Filter by symbol kind')
|
|
345
387
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
346
388
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
347
389
|
.option('-j, --json', 'Output as JSON')
|
|
390
|
+
.option('--limit <number>', 'Max results to return (quick mode)')
|
|
391
|
+
.option('--offset <number>', 'Skip N results (quick mode)')
|
|
392
|
+
.option('--ndjson', 'Newline-delimited JSON output (quick mode)')
|
|
348
393
|
.action((target, opts) => {
|
|
349
|
-
if (opts.kind && !
|
|
350
|
-
console.error(`Invalid kind "${opts.kind}". Valid: ${
|
|
394
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
395
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
351
396
|
process.exit(1);
|
|
352
397
|
}
|
|
398
|
+
if (opts.quick) {
|
|
399
|
+
explain(target, opts.db, {
|
|
400
|
+
depth: parseInt(opts.depth, 10),
|
|
401
|
+
noTests: resolveNoTests(opts),
|
|
402
|
+
json: opts.json,
|
|
403
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
404
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
405
|
+
ndjson: opts.ndjson,
|
|
406
|
+
});
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
353
409
|
audit(target, opts.db, {
|
|
354
410
|
depth: parseInt(opts.depth, 10),
|
|
355
411
|
file: opts.file,
|
|
@@ -415,18 +471,48 @@ program
|
|
|
415
471
|
|
|
416
472
|
program
|
|
417
473
|
.command('check [ref]')
|
|
418
|
-
.description(
|
|
474
|
+
.description(
|
|
475
|
+
'CI gate: run manifesto rules (no args), diff predicates (with ref/--staged), or both (--rules)',
|
|
476
|
+
)
|
|
419
477
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
420
478
|
.option('--staged', 'Analyze staged changes')
|
|
479
|
+
.option('--rules', 'Also run manifesto rules alongside diff predicates')
|
|
421
480
|
.option('--cycles', 'Assert no dependency cycles involve changed files')
|
|
422
481
|
.option('--blast-radius <n>', 'Assert no function exceeds N transitive callers')
|
|
423
482
|
.option('--signatures', 'Assert no function declaration lines were modified')
|
|
424
483
|
.option('--boundaries', 'Assert no cross-owner boundary violations')
|
|
425
484
|
.option('--depth <n>', 'Max BFS depth for blast radius (default: 3)')
|
|
485
|
+
.option('-f, --file <path>', 'Scope to file (partial match, manifesto mode)')
|
|
486
|
+
.option('-k, --kind <kind>', 'Filter by symbol kind (manifesto mode)')
|
|
426
487
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
427
488
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
428
489
|
.option('-j, --json', 'Output as JSON')
|
|
490
|
+
.option('--limit <number>', 'Max results to return (manifesto mode)')
|
|
491
|
+
.option('--offset <number>', 'Skip N results (manifesto mode)')
|
|
492
|
+
.option('--ndjson', 'Newline-delimited JSON output (manifesto mode)')
|
|
429
493
|
.action(async (ref, opts) => {
|
|
494
|
+
const isDiffMode = ref || opts.staged;
|
|
495
|
+
|
|
496
|
+
if (!isDiffMode && !opts.rules) {
|
|
497
|
+
// No ref, no --staged → run manifesto rules on whole codebase
|
|
498
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
499
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
const { manifesto } = await import('./manifesto.js');
|
|
503
|
+
manifesto(opts.db, {
|
|
504
|
+
file: opts.file,
|
|
505
|
+
kind: opts.kind,
|
|
506
|
+
noTests: resolveNoTests(opts),
|
|
507
|
+
json: opts.json,
|
|
508
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
509
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
510
|
+
ndjson: opts.ndjson,
|
|
511
|
+
});
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Diff predicates mode
|
|
430
516
|
const { check } = await import('./check.js');
|
|
431
517
|
check(opts.db, {
|
|
432
518
|
ref,
|
|
@@ -439,15 +525,37 @@ program
|
|
|
439
525
|
noTests: resolveNoTests(opts),
|
|
440
526
|
json: opts.json,
|
|
441
527
|
});
|
|
528
|
+
|
|
529
|
+
// If --rules, also run manifesto after diff predicates
|
|
530
|
+
if (opts.rules) {
|
|
531
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
532
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
533
|
+
process.exit(1);
|
|
534
|
+
}
|
|
535
|
+
const { manifesto } = await import('./manifesto.js');
|
|
536
|
+
manifesto(opts.db, {
|
|
537
|
+
file: opts.file,
|
|
538
|
+
kind: opts.kind,
|
|
539
|
+
noTests: resolveNoTests(opts),
|
|
540
|
+
json: opts.json,
|
|
541
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
542
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
543
|
+
ndjson: opts.ndjson,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
442
546
|
});
|
|
443
547
|
|
|
444
548
|
// ─── New commands ────────────────────────────────────────────────────────
|
|
445
549
|
|
|
446
550
|
program
|
|
447
551
|
.command('export')
|
|
448
|
-
.description('Export dependency graph as DOT
|
|
552
|
+
.description('Export dependency graph as DOT, Mermaid, JSON, GraphML, GraphSON, or Neo4j CSV')
|
|
449
553
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
450
|
-
.option(
|
|
554
|
+
.option(
|
|
555
|
+
'-f, --format <format>',
|
|
556
|
+
'Output format: dot, mermaid, json, graphml, graphson, neo4j',
|
|
557
|
+
'dot',
|
|
558
|
+
)
|
|
451
559
|
.option('--functions', 'Function-level graph instead of file-level')
|
|
452
560
|
.option('-T, --no-tests', 'Exclude test/spec files')
|
|
453
561
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
@@ -471,6 +579,25 @@ program
|
|
|
471
579
|
case 'json':
|
|
472
580
|
output = JSON.stringify(exportJSON(db, exportOpts), null, 2);
|
|
473
581
|
break;
|
|
582
|
+
case 'graphml':
|
|
583
|
+
output = exportGraphML(db, exportOpts);
|
|
584
|
+
break;
|
|
585
|
+
case 'graphson':
|
|
586
|
+
output = JSON.stringify(exportGraphSON(db, exportOpts), null, 2);
|
|
587
|
+
break;
|
|
588
|
+
case 'neo4j': {
|
|
589
|
+
const csv = exportNeo4jCSV(db, exportOpts);
|
|
590
|
+
if (opts.output) {
|
|
591
|
+
const base = opts.output.replace(/\.[^.]+$/, '') || opts.output;
|
|
592
|
+
fs.writeFileSync(`${base}-nodes.csv`, csv.nodes, 'utf-8');
|
|
593
|
+
fs.writeFileSync(`${base}-relationships.csv`, csv.relationships, 'utf-8');
|
|
594
|
+
db.close();
|
|
595
|
+
console.log(`Exported to ${base}-nodes.csv and ${base}-relationships.csv`);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
output = `--- nodes.csv ---\n${csv.nodes}\n\n--- relationships.csv ---\n${csv.relationships}`;
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
474
601
|
default:
|
|
475
602
|
output = exportDOT(db, exportOpts);
|
|
476
603
|
break;
|
|
@@ -486,6 +613,81 @@ program
|
|
|
486
613
|
}
|
|
487
614
|
});
|
|
488
615
|
|
|
616
|
+
program
|
|
617
|
+
.command('plot')
|
|
618
|
+
.description('Generate an interactive HTML dependency graph viewer')
|
|
619
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
620
|
+
.option('--functions', 'Function-level graph instead of file-level')
|
|
621
|
+
.option('-T, --no-tests', 'Exclude test/spec files')
|
|
622
|
+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
623
|
+
.option('--min-confidence <score>', 'Minimum edge confidence threshold (default: 0.5)', '0.5')
|
|
624
|
+
.option('-o, --output <file>', 'Write HTML to file')
|
|
625
|
+
.option('-c, --config <path>', 'Path to .plotDotCfg config file')
|
|
626
|
+
.option('--no-open', 'Do not open in browser')
|
|
627
|
+
.option('--cluster <mode>', 'Cluster nodes: none | community | directory')
|
|
628
|
+
.option('--overlay <list>', 'Comma-separated overlays: complexity,risk')
|
|
629
|
+
.option('--seed <strategy>', 'Seed strategy: all | top-fanin | entry')
|
|
630
|
+
.option('--seed-count <n>', 'Number of seed nodes (default: 30)')
|
|
631
|
+
.option('--size-by <metric>', 'Size nodes by: uniform | fan-in | fan-out | complexity')
|
|
632
|
+
.option('--color-by <mode>', 'Color nodes by: kind | role | community | complexity')
|
|
633
|
+
.action(async (opts) => {
|
|
634
|
+
const { generatePlotHTML, loadPlotConfig } = await import('./viewer.js');
|
|
635
|
+
const os = await import('node:os');
|
|
636
|
+
const db = openReadonlyOrFail(opts.db);
|
|
637
|
+
|
|
638
|
+
let plotCfg;
|
|
639
|
+
if (opts.config) {
|
|
640
|
+
try {
|
|
641
|
+
plotCfg = JSON.parse(fs.readFileSync(opts.config, 'utf-8'));
|
|
642
|
+
} catch (e) {
|
|
643
|
+
console.error(`Failed to load config: ${e.message}`);
|
|
644
|
+
db.close();
|
|
645
|
+
process.exitCode = 1;
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
} else {
|
|
649
|
+
plotCfg = loadPlotConfig(process.cwd());
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Merge CLI flags into config
|
|
653
|
+
if (opts.cluster) plotCfg.clusterBy = opts.cluster;
|
|
654
|
+
if (opts.colorBy) plotCfg.colorBy = opts.colorBy;
|
|
655
|
+
if (opts.sizeBy) plotCfg.sizeBy = opts.sizeBy;
|
|
656
|
+
if (opts.seed) plotCfg.seedStrategy = opts.seed;
|
|
657
|
+
if (opts.seedCount) plotCfg.seedCount = parseInt(opts.seedCount, 10);
|
|
658
|
+
if (opts.overlay) {
|
|
659
|
+
const parts = opts.overlay.split(',').map((s) => s.trim());
|
|
660
|
+
if (!plotCfg.overlays) plotCfg.overlays = {};
|
|
661
|
+
if (parts.includes('complexity')) plotCfg.overlays.complexity = true;
|
|
662
|
+
if (parts.includes('risk')) plotCfg.overlays.risk = true;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const html = generatePlotHTML(db, {
|
|
666
|
+
fileLevel: !opts.functions,
|
|
667
|
+
noTests: resolveNoTests(opts),
|
|
668
|
+
minConfidence: parseFloat(opts.minConfidence),
|
|
669
|
+
config: plotCfg,
|
|
670
|
+
});
|
|
671
|
+
db.close();
|
|
672
|
+
|
|
673
|
+
const outPath = opts.output || path.join(os.tmpdir(), `codegraph-plot-${Date.now()}.html`);
|
|
674
|
+
fs.writeFileSync(outPath, html, 'utf-8');
|
|
675
|
+
console.log(`Plot written to ${outPath}`);
|
|
676
|
+
|
|
677
|
+
if (opts.open !== false) {
|
|
678
|
+
const { execFile } = await import('node:child_process');
|
|
679
|
+
const args =
|
|
680
|
+
process.platform === 'win32'
|
|
681
|
+
? ['cmd', ['/c', 'start', '', outPath]]
|
|
682
|
+
: process.platform === 'darwin'
|
|
683
|
+
? ['open', [outPath]]
|
|
684
|
+
: ['xdg-open', [outPath]];
|
|
685
|
+
execFile(args[0], args[1], (err) => {
|
|
686
|
+
if (err) console.error('Could not open browser:', err.message);
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
|
|
489
691
|
program
|
|
490
692
|
.command('cycles')
|
|
491
693
|
.description('Detect circular dependencies in the codebase')
|
|
@@ -799,38 +1001,6 @@ program
|
|
|
799
1001
|
}
|
|
800
1002
|
});
|
|
801
1003
|
|
|
802
|
-
program
|
|
803
|
-
.command('hotspots')
|
|
804
|
-
.description(
|
|
805
|
-
'Find structural hotspots: files or directories with extreme fan-in, fan-out, or symbol density',
|
|
806
|
-
)
|
|
807
|
-
.option('-d, --db <path>', 'Path to graph.db')
|
|
808
|
-
.option('-n, --limit <number>', 'Number of results', '10')
|
|
809
|
-
.option('--metric <metric>', 'fan-in | fan-out | density | coupling', 'fan-in')
|
|
810
|
-
.option('--level <level>', 'file | directory', 'file')
|
|
811
|
-
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
812
|
-
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
813
|
-
.option('-j, --json', 'Output as JSON')
|
|
814
|
-
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
815
|
-
.option('--ndjson', 'Newline-delimited JSON output')
|
|
816
|
-
.action(async (opts) => {
|
|
817
|
-
const { hotspotsData, formatHotspots } = await import('./structure.js');
|
|
818
|
-
const data = hotspotsData(opts.db, {
|
|
819
|
-
metric: opts.metric,
|
|
820
|
-
level: opts.level,
|
|
821
|
-
limit: parseInt(opts.limit, 10),
|
|
822
|
-
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
823
|
-
noTests: resolveNoTests(opts),
|
|
824
|
-
});
|
|
825
|
-
if (opts.ndjson) {
|
|
826
|
-
printNdjson(data, 'hotspots');
|
|
827
|
-
} else if (opts.json) {
|
|
828
|
-
console.log(JSON.stringify(data, null, 2));
|
|
829
|
-
} else {
|
|
830
|
-
console.log(formatHotspots(data));
|
|
831
|
-
}
|
|
832
|
-
});
|
|
833
|
-
|
|
834
1004
|
program
|
|
835
1005
|
.command('roles')
|
|
836
1006
|
.description('Show node role classification: entry, core, utility, adapter, dead, leaf')
|
|
@@ -949,8 +1119,8 @@ program
|
|
|
949
1119
|
console.error('Provide a function/entry point name or use --list to see all entry points.');
|
|
950
1120
|
process.exit(1);
|
|
951
1121
|
}
|
|
952
|
-
if (opts.kind && !
|
|
953
|
-
console.error(`Invalid kind "${opts.kind}". Valid: ${
|
|
1122
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
1123
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
954
1124
|
process.exit(1);
|
|
955
1125
|
}
|
|
956
1126
|
const { flow } = await import('./flow.js');
|
|
@@ -967,6 +1137,70 @@ program
|
|
|
967
1137
|
});
|
|
968
1138
|
});
|
|
969
1139
|
|
|
1140
|
+
program
|
|
1141
|
+
.command('dataflow <name>')
|
|
1142
|
+
.description('Show data flow for a function: parameters, return consumers, mutations')
|
|
1143
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
1144
|
+
.option('-f, --file <path>', 'Scope to file (partial match)')
|
|
1145
|
+
.option('-k, --kind <kind>', 'Filter by symbol kind')
|
|
1146
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
1147
|
+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
1148
|
+
.option('-j, --json', 'Output as JSON')
|
|
1149
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
1150
|
+
.option('--limit <number>', 'Max results to return')
|
|
1151
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
1152
|
+
.option('--impact', 'Show data-dependent blast radius')
|
|
1153
|
+
.option('--depth <n>', 'Max traversal depth', '5')
|
|
1154
|
+
.action(async (name, opts) => {
|
|
1155
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
1156
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
1157
|
+
process.exit(1);
|
|
1158
|
+
}
|
|
1159
|
+
const { dataflow } = await import('./dataflow.js');
|
|
1160
|
+
dataflow(name, opts.db, {
|
|
1161
|
+
file: opts.file,
|
|
1162
|
+
kind: opts.kind,
|
|
1163
|
+
noTests: resolveNoTests(opts),
|
|
1164
|
+
json: opts.json,
|
|
1165
|
+
ndjson: opts.ndjson,
|
|
1166
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
1167
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
1168
|
+
impact: opts.impact,
|
|
1169
|
+
depth: opts.depth,
|
|
1170
|
+
});
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
program
|
|
1174
|
+
.command('cfg <name>')
|
|
1175
|
+
.description('Show control flow graph for a function')
|
|
1176
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
1177
|
+
.option('--format <fmt>', 'Output format: text, dot, mermaid', 'text')
|
|
1178
|
+
.option('-f, --file <path>', 'Scope to file (partial match)')
|
|
1179
|
+
.option('-k, --kind <kind>', 'Filter by symbol kind')
|
|
1180
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
1181
|
+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
1182
|
+
.option('-j, --json', 'Output as JSON')
|
|
1183
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
1184
|
+
.option('--limit <number>', 'Max results to return')
|
|
1185
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
1186
|
+
.action(async (name, opts) => {
|
|
1187
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
1188
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
1189
|
+
process.exit(1);
|
|
1190
|
+
}
|
|
1191
|
+
const { cfg } = await import('./cfg.js');
|
|
1192
|
+
cfg(name, opts.db, {
|
|
1193
|
+
format: opts.format,
|
|
1194
|
+
file: opts.file,
|
|
1195
|
+
kind: opts.kind,
|
|
1196
|
+
noTests: resolveNoTests(opts),
|
|
1197
|
+
json: opts.json,
|
|
1198
|
+
ndjson: opts.ndjson,
|
|
1199
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
1200
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
1201
|
+
});
|
|
1202
|
+
});
|
|
1203
|
+
|
|
970
1204
|
program
|
|
971
1205
|
.command('complexity [target]')
|
|
972
1206
|
.description('Show per-function complexity metrics (cognitive, cyclomatic, nesting depth, MI)')
|
|
@@ -987,8 +1221,8 @@ program
|
|
|
987
1221
|
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
988
1222
|
.option('--ndjson', 'Newline-delimited JSON output')
|
|
989
1223
|
.action(async (target, opts) => {
|
|
990
|
-
if (opts.kind && !
|
|
991
|
-
console.error(`Invalid kind "${opts.kind}". Valid: ${
|
|
1224
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
1225
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
992
1226
|
process.exit(1);
|
|
993
1227
|
}
|
|
994
1228
|
const { complexity } = await import('./complexity.js');
|
|
@@ -1008,31 +1242,31 @@ program
|
|
|
1008
1242
|
});
|
|
1009
1243
|
|
|
1010
1244
|
program
|
|
1011
|
-
.command('
|
|
1012
|
-
.description('
|
|
1245
|
+
.command('ast [pattern]')
|
|
1246
|
+
.description('Search stored AST nodes (calls, new, string, regex, throw, await) by pattern')
|
|
1013
1247
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
1248
|
+
.option('-k, --kind <kind>', 'Filter by AST node kind (call, new, string, regex, throw, await)')
|
|
1249
|
+
.option('-f, --file <path>', 'Scope to file (partial match)')
|
|
1014
1250
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
1015
1251
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
1016
|
-
.option('-f, --file <path>', 'Scope to file (partial match)')
|
|
1017
|
-
.option('-k, --kind <kind>', 'Filter by symbol kind')
|
|
1018
1252
|
.option('-j, --json', 'Output as JSON')
|
|
1253
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
1019
1254
|
.option('--limit <number>', 'Max results to return')
|
|
1020
1255
|
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
1021
|
-
.
|
|
1022
|
-
|
|
1023
|
-
if (opts.kind && !
|
|
1024
|
-
console.error(`Invalid kind "${opts.kind}". Valid: ${
|
|
1256
|
+
.action(async (pattern, opts) => {
|
|
1257
|
+
const { AST_NODE_KINDS, astQuery } = await import('./ast.js');
|
|
1258
|
+
if (opts.kind && !AST_NODE_KINDS.includes(opts.kind)) {
|
|
1259
|
+
console.error(`Invalid AST kind "${opts.kind}". Valid: ${AST_NODE_KINDS.join(', ')}`);
|
|
1025
1260
|
process.exit(1);
|
|
1026
1261
|
}
|
|
1027
|
-
|
|
1028
|
-
manifesto(opts.db, {
|
|
1029
|
-
file: opts.file,
|
|
1262
|
+
astQuery(pattern, opts.db, {
|
|
1030
1263
|
kind: opts.kind,
|
|
1264
|
+
file: opts.file,
|
|
1031
1265
|
noTests: resolveNoTests(opts),
|
|
1032
1266
|
json: opts.json,
|
|
1267
|
+
ndjson: opts.ndjson,
|
|
1033
1268
|
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
1034
1269
|
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
1035
|
-
ndjson: opts.ndjson,
|
|
1036
1270
|
});
|
|
1037
1271
|
});
|
|
1038
1272
|
|
|
@@ -1070,7 +1304,16 @@ program
|
|
|
1070
1304
|
)
|
|
1071
1305
|
.option('-d, --db <path>', 'Path to graph.db')
|
|
1072
1306
|
.option('-n, --limit <number>', 'Max results to return', '20')
|
|
1073
|
-
.option(
|
|
1307
|
+
.option(
|
|
1308
|
+
'--level <level>',
|
|
1309
|
+
'Granularity: function (default) | file | directory. File/directory level shows hotspots',
|
|
1310
|
+
'function',
|
|
1311
|
+
)
|
|
1312
|
+
.option(
|
|
1313
|
+
'--sort <metric>',
|
|
1314
|
+
'Sort metric: risk | complexity | churn | fan-in | mi (function level); fan-in | fan-out | density | coupling (file/directory level)',
|
|
1315
|
+
'risk',
|
|
1316
|
+
)
|
|
1074
1317
|
.option('--min-score <score>', 'Only show symbols with risk score >= threshold')
|
|
1075
1318
|
.option('--role <role>', 'Filter by role (entry, core, utility, adapter, leaf, dead)')
|
|
1076
1319
|
.option('-f, --file <path>', 'Scope to a specific file (partial match)')
|
|
@@ -1082,8 +1325,29 @@ program
|
|
|
1082
1325
|
.option('--ndjson', 'Newline-delimited JSON output')
|
|
1083
1326
|
.option('--weights <json>', 'Custom weights JSON (e.g. \'{"fanIn":1,"complexity":0}\')')
|
|
1084
1327
|
.action(async (opts) => {
|
|
1085
|
-
if (opts.
|
|
1086
|
-
|
|
1328
|
+
if (opts.level === 'file' || opts.level === 'directory') {
|
|
1329
|
+
// Delegate to hotspots for file/directory level
|
|
1330
|
+
const { hotspotsData, formatHotspots } = await import('./structure.js');
|
|
1331
|
+
const metric = opts.sort === 'risk' ? 'fan-in' : opts.sort;
|
|
1332
|
+
const data = hotspotsData(opts.db, {
|
|
1333
|
+
metric,
|
|
1334
|
+
level: opts.level,
|
|
1335
|
+
limit: parseInt(opts.limit, 10),
|
|
1336
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
1337
|
+
noTests: resolveNoTests(opts),
|
|
1338
|
+
});
|
|
1339
|
+
if (opts.ndjson) {
|
|
1340
|
+
printNdjson(data, 'hotspots');
|
|
1341
|
+
} else if (opts.json) {
|
|
1342
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1343
|
+
} else {
|
|
1344
|
+
console.log(formatHotspots(data));
|
|
1345
|
+
}
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
1350
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
1087
1351
|
process.exit(1);
|
|
1088
1352
|
}
|
|
1089
1353
|
if (opts.role && !VALID_ROLES.includes(opts.role)) {
|
|
@@ -1245,26 +1509,31 @@ program
|
|
|
1245
1509
|
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
1246
1510
|
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
1247
1511
|
.action(async (command, positionalTargets, opts) => {
|
|
1248
|
-
if (opts.kind && !
|
|
1249
|
-
console.error(`Invalid kind "${opts.kind}". Valid: ${
|
|
1512
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
1513
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
1250
1514
|
process.exit(1);
|
|
1251
1515
|
}
|
|
1252
1516
|
|
|
1253
1517
|
let targets;
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1518
|
+
try {
|
|
1519
|
+
if (opts.fromFile) {
|
|
1520
|
+
const raw = fs.readFileSync(opts.fromFile, 'utf-8').trim();
|
|
1521
|
+
if (raw.startsWith('[')) {
|
|
1522
|
+
targets = JSON.parse(raw);
|
|
1523
|
+
} else {
|
|
1524
|
+
targets = raw.split(/\r?\n/).filter(Boolean);
|
|
1525
|
+
}
|
|
1526
|
+
} else if (opts.stdin) {
|
|
1527
|
+
const chunks = [];
|
|
1528
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
1529
|
+
const raw = Buffer.concat(chunks).toString('utf-8').trim();
|
|
1530
|
+
targets = raw.startsWith('[') ? JSON.parse(raw) : raw.split(/\r?\n/).filter(Boolean);
|
|
1258
1531
|
} else {
|
|
1259
|
-
targets =
|
|
1532
|
+
targets = splitTargets(positionalTargets);
|
|
1260
1533
|
}
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
const raw = Buffer.concat(chunks).toString('utf-8').trim();
|
|
1265
|
-
targets = raw.startsWith('[') ? JSON.parse(raw) : raw.split(/\r?\n/).filter(Boolean);
|
|
1266
|
-
} else {
|
|
1267
|
-
targets = positionalTargets;
|
|
1534
|
+
} catch (err) {
|
|
1535
|
+
console.error(`Failed to parse targets: ${err.message}`);
|
|
1536
|
+
process.exit(1);
|
|
1268
1537
|
}
|
|
1269
1538
|
|
|
1270
1539
|
if (!targets || targets.length === 0) {
|
|
@@ -1279,7 +1548,14 @@ program
|
|
|
1279
1548
|
noTests: resolveNoTests(opts),
|
|
1280
1549
|
};
|
|
1281
1550
|
|
|
1282
|
-
|
|
1551
|
+
// Multi-command mode: items from --from-file / --stdin may be objects with { command, target }
|
|
1552
|
+
const isMulti = targets.length > 0 && typeof targets[0] === 'object' && targets[0].command;
|
|
1553
|
+
if (isMulti) {
|
|
1554
|
+
const data = multiBatchData(targets, opts.db, batchOpts);
|
|
1555
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1556
|
+
} else {
|
|
1557
|
+
batch(command, targets, opts.db, batchOpts);
|
|
1558
|
+
}
|
|
1283
1559
|
});
|
|
1284
1560
|
|
|
1285
1561
|
program.parse();
|