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