@shvmgyl15/tsgraph 0.1.0 → 0.2.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.
Files changed (55) hide show
  1. package/dist/changes/index.test.js +2 -6
  2. package/dist/changes/index.test.js.map +1 -1
  3. package/dist/cli/index.js +184 -4
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/git/index.test.js +4 -6
  6. package/dist/git/index.test.js.map +1 -1
  7. package/dist/opencode/index.js +1 -1
  8. package/dist/opencode/index.js.map +1 -1
  9. package/dist/opencode/index.test.js +2 -2
  10. package/dist/opencode/index.test.js.map +1 -1
  11. package/dist/search/index.d.ts.map +1 -1
  12. package/dist/search/index.js +12 -4
  13. package/dist/search/index.js.map +1 -1
  14. package/package.json +16 -1
  15. package/AGENTS.md +0 -64
  16. package/TODOS.md +0 -61
  17. package/opencode.json +0 -24
  18. package/src/analysis/analysis.test.ts +0 -405
  19. package/src/analysis/complexity.ts +0 -107
  20. package/src/analysis/coupling.ts +0 -106
  21. package/src/analysis/hotspot.ts +0 -52
  22. package/src/analysis/index.ts +0 -17
  23. package/src/boundaries/index.test.ts +0 -335
  24. package/src/boundaries/index.ts +0 -137
  25. package/src/changes/index.test.ts +0 -114
  26. package/src/changes/index.ts +0 -95
  27. package/src/cli/index.ts +0 -736
  28. package/src/git/index.test.ts +0 -92
  29. package/src/git/index.ts +0 -86
  30. package/src/graph/types.test.ts +0 -383
  31. package/src/graph/types.ts +0 -353
  32. package/src/mcp/mcp.test.ts +0 -176
  33. package/src/mcp/server.ts +0 -217
  34. package/src/nextjs/index.ts +0 -23
  35. package/src/nextjs/nextjs.test.ts +0 -233
  36. package/src/nextjs/pages.ts +0 -43
  37. package/src/nextjs/react.ts +0 -100
  38. package/src/nextjs/router.ts +0 -102
  39. package/src/nextjs/routes.ts +0 -69
  40. package/src/opencode/index.test.ts +0 -90
  41. package/src/opencode/index.ts +0 -83
  42. package/src/parser/index.ts +0 -339
  43. package/src/parser/parser.test.ts +0 -282
  44. package/src/plan/index.test.ts +0 -162
  45. package/src/plan/index.ts +0 -161
  46. package/src/report/index.ts +0 -128
  47. package/src/scanner/index.ts +0 -97
  48. package/src/scanner/scanner.test.ts +0 -135
  49. package/src/search/index.ts +0 -163
  50. package/src/search/search.test.ts +0 -512
  51. package/src/traversal/index.ts +0 -5
  52. package/src/traversal/traversal.test.ts +0 -266
  53. package/src/traversal/traversal.ts +0 -185
  54. package/tsconfig.json +0 -20
  55. package/vitest.config.ts +0 -7
package/src/cli/index.ts DELETED
@@ -1,736 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import path from "node:path";
4
- import fs from "node:fs";
5
- import { Command } from "commander";
6
- import { scanFiles } from "../scanner/index.js";
7
- import { parseProject } from "../parser/index.js";
8
- import { serialize } from "../graph/types.js";
9
- import { generateReport } from "../report/index.js";
10
- import {
11
- loadGraph,
12
- findCallers,
13
- findCallees,
14
- findNode,
15
- readSource,
16
- querySymbols,
17
- findImports,
18
- findPublic,
19
- focusPackage,
20
- context,
21
- } from "../search/index.js";
22
- import {
23
- analyzeComplexity,
24
- findHotspots,
25
- analyzeCoupling,
26
- dependencyTree,
27
- } from "../analysis/index.js";
28
- import type { DepsNode } from "../analysis/index.js";
29
- import {
30
- impact,
31
- findPath,
32
- findOrphans,
33
- trace,
34
- } from "../traversal/index.js";
35
- import type { ImpactNode } from "../traversal/index.js";
36
- import { startMcpServer } from "../mcp/server.js";
37
- import {
38
- checkBoundaries,
39
- loadBoundariesConfig,
40
- } from "../boundaries/index.js";
41
- import { getChanges, getStale } from "../changes/index.js";
42
- import { generatePlan, generateReview } from "../plan/index.js";
43
- import { addOpencodePlugin } from "../opencode/index.js";
44
-
45
- const program = new Command();
46
-
47
- function loadGraphFromCwd(): ReturnType<typeof loadGraph> {
48
- const graphPath = path.resolve(process.cwd(), ".tsgraph", "graph.json");
49
- return loadGraph(graphPath);
50
- }
51
-
52
- function handleError(err: unknown) {
53
- console.error((err as Error).message);
54
- process.exit(1);
55
- }
56
-
57
- program
58
- .name("tsgraph")
59
- .description("Local AST-based TypeScript/React/Next.js codebase indexer")
60
- .version("0.1.0");
61
-
62
- program
63
- .command("build")
64
- .description("Generate .tsgraph/graph.json and GRAPH_REPORT.md")
65
- .argument("<root>", "root directory of the project")
66
- .option("--precise", "use type-checked enrichment (slower)")
67
- .action((root: string) => {
68
- const rootDir = path.resolve(root);
69
-
70
- const { files, errors } = scanFiles(rootDir);
71
- for (const err of errors) {
72
- console.error("scan warning:", err.message);
73
- }
74
-
75
- const graph = parseProject(rootDir, files);
76
-
77
- const outDir = path.join(rootDir, ".tsgraph");
78
- fs.mkdirSync(outDir, { recursive: true });
79
-
80
- const graphPath = path.join(outDir, "graph.json");
81
- fs.writeFileSync(graphPath, serialize(graph), "utf-8");
82
-
83
- const reportPath = path.join(outDir, "GRAPH_REPORT.md");
84
- fs.writeFileSync(
85
- reportPath,
86
- generateReport(graph, {
87
- rootDir,
88
- includeBoundaries: true,
89
- includeStale: true,
90
- includeHotspots: true,
91
- }),
92
- "utf-8",
93
- );
94
-
95
- const fileCount = graph.files.length;
96
- const symbolCount = graph.symbols.length;
97
- const callCount = graph.calls.length;
98
- const depCount = graph.dependencies.length;
99
-
100
- console.log(
101
- `tsgraph: indexed ${fileCount} files, ${symbolCount} symbols, ${callCount} calls, ${depCount} deps`,
102
- );
103
- });
104
-
105
- program
106
- .command("callers <symbol>")
107
- .description("Show which functions call a given symbol")
108
- .action((symbol: string) => {
109
- try {
110
- const graph = loadGraphFromCwd();
111
- const results = findCallers(graph, symbol);
112
- if (results.length === 0) {
113
- console.log(`No callers found for "${symbol}"`);
114
- return;
115
- }
116
- console.log(`Callers of "${symbol}" (${results.length}):`);
117
- for (const r of results) {
118
- const locations = r.edges
119
- .map((e) => `${e.file}:${e.line}`)
120
- .join(", ");
121
- console.log(` ${r.callerSymbol.name} — ${locations}`);
122
- }
123
- } catch (err) {
124
- handleError(err);
125
- }
126
- });
127
-
128
- program
129
- .command("callees <symbol>")
130
- .description("Show which functions a given symbol calls")
131
- .action((symbol: string) => {
132
- try {
133
- const graph = loadGraphFromCwd();
134
- const results = findCallees(graph, symbol);
135
- if (results.length === 0) {
136
- console.log(`No callees found for "${symbol}"`);
137
- return;
138
- }
139
- console.log(`Callees of "${symbol}" (${results.length}):`);
140
- for (const r of results) {
141
- const locations = r.edges
142
- .map((e) => `${e.file}:${e.line}`)
143
- .join(", ");
144
- console.log(` ${r.calleeRaw} — ${locations}`);
145
- }
146
- } catch (err) {
147
- handleError(err);
148
- }
149
- });
150
-
151
- program
152
- .command("node <symbol>")
153
- .description("Show detailed information about a symbol")
154
- .action((symbol: string) => {
155
- try {
156
- const graph = loadGraphFromCwd();
157
- const n = findNode(graph, symbol);
158
- if (!n) {
159
- console.log(`Symbol "${symbol}" not found`);
160
- return;
161
- }
162
- console.log(`Node: ${n.name}`);
163
- console.log(` Kind: ${n.kind}`);
164
- console.log(` File: ${n.file}:${n.line}`);
165
- console.log(` Lines: ${n.line}–${n.endLine}`);
166
- console.log(` Exported: ${n.isExported}`);
167
- console.log(` Package: ${n.packageName}`);
168
- if (n.receiver) console.log(` Receiver: ${n.receiver}`);
169
- if (n.arity !== undefined) console.log(` Arity: ${n.arity}`);
170
- if (n.doc) console.log(` Doc: ${n.doc}`);
171
- } catch (err) {
172
- handleError(err);
173
- }
174
- });
175
-
176
- program
177
- .command("source <symbol>")
178
- .description("Extract the source code for a specific symbol")
179
- .action((symbol: string) => {
180
- try {
181
- const graph = loadGraphFromCwd();
182
- const n = findNode(graph, symbol);
183
- if (!n) {
184
- console.log(`Symbol "${symbol}" not found`);
185
- return;
186
- }
187
- try {
188
- const src = readSource(graph, n);
189
- console.log(`// ${n.file}:${n.line}–${n.endLine}`);
190
- console.log(src);
191
- } catch {
192
- console.log(`Could not read source file: ${n.file}`);
193
- }
194
- } catch (err) {
195
- handleError(err);
196
- }
197
- });
198
-
199
- program
200
- .command("query <pattern>")
201
- .description("Search for symbols matching a pattern")
202
- .action((pattern: string) => {
203
- try {
204
- const graph = loadGraphFromCwd();
205
- const results = querySymbols(graph, pattern);
206
- if (results.length === 0) {
207
- console.log(`No symbols matching "${pattern}"`);
208
- return;
209
- }
210
- const kindCount = new Map<string, number>();
211
- for (const s of results) {
212
- kindCount.set(s.kind, (kindCount.get(s.kind) ?? 0) + 1);
213
- }
214
- console.log(
215
- `Found ${results.length} symbols matching "${pattern}":`,
216
- );
217
- console.log(` Kinds: ${[...kindCount.entries()].map(([k, c]) => `${k}(${c})`).join(", ")}`);
218
- for (const s of results) {
219
- const exported = s.isExported ? "export " : "";
220
- const receiver = s.receiver ? `${s.receiver}.` : "";
221
- console.log(` ${exported}${s.kind} ${receiver}${s.name} — ${s.file}:${s.line}`);
222
- }
223
- } catch (err) {
224
- handleError(err);
225
- }
226
- });
227
-
228
- program
229
- .command("imports <path>")
230
- .description("Find all files importing a specific package path")
231
- .action((importPath: string) => {
232
- try {
233
- const graph = loadGraphFromCwd();
234
- const results = findImports(graph, importPath);
235
- if (results.length === 0) {
236
- console.log(`No imports matching "${importPath}"`);
237
- return;
238
- }
239
- console.log(`Imports matching "${importPath}" (${results.length}):`);
240
- for (const r of results) {
241
- const alias = r.alias ? ` as ${r.alias}` : "";
242
- const kind = r.isDefault ? "default" : "named";
243
- console.log(` ${r.fromFile} → ${kind} import ${r.importPath}${alias}`);
244
- }
245
- } catch (err) {
246
- handleError(err);
247
- }
248
- });
249
-
250
- program
251
- .command("public [package]")
252
- .description("Show exported symbols, optionally scoped to a package")
253
- .action((packageName?: string) => {
254
- try {
255
- const graph = loadGraphFromCwd();
256
- const results = findPublic(graph, packageName);
257
- if (results.length === 0) {
258
- console.log("No exported symbols found");
259
- return;
260
- }
261
- const scope = packageName ? ` in package "${packageName}"` : "";
262
- console.log(`Exported symbols${scope} (${results.length}):`);
263
- for (const s of results) {
264
- const receiver = s.receiver ? `${s.receiver}.` : "";
265
- console.log(` ${s.kind} ${receiver}${s.name} — ${s.file}:${s.line}`);
266
- }
267
- } catch (err) {
268
- handleError(err);
269
- }
270
- });
271
-
272
- program
273
- .command("focus <package>")
274
- .description("Show all assets for a specific package")
275
- .action((packageName: string) => {
276
- try {
277
- const graph = loadGraphFromCwd();
278
- const result = focusPackage(graph, packageName);
279
- if (!result) {
280
- console.log(`Package "${packageName}" not found`);
281
- return;
282
- }
283
- console.log(`Package: ${result.pkg.name}`);
284
- console.log(` Files: ${result.files.length}`);
285
- console.log(` Symbols: ${result.symbols.length}`);
286
- console.log(` Imports: ${result.imports.length}`);
287
- console.log("");
288
- if (result.symbols.length > 0) {
289
- console.log("Symbols:");
290
- for (const s of result.symbols.slice(0, 30)) {
291
- const exported = s.isExported ? "export " : "";
292
- const receiver = s.receiver ? `${s.receiver}.` : "";
293
- console.log(` ${exported}${s.kind} ${receiver}${s.name} — ${s.file}:${s.line}`);
294
- }
295
- if (result.symbols.length > 30) {
296
- console.log(` ... and ${result.symbols.length - 30} more`);
297
- }
298
- }
299
- } catch (err) {
300
- handleError(err);
301
- }
302
- });
303
-
304
- program
305
- .command("context <symbol>")
306
- .description("Bundle node, source, callers, and callees for a symbol")
307
- .action((symbol: string) => {
308
- try {
309
- const graph = loadGraphFromCwd();
310
- const ctx = context(graph, symbol);
311
- if (!ctx.node) {
312
- console.log(`Symbol "${symbol}" not found`);
313
- return;
314
- }
315
- console.log(`=== Context: ${ctx.node.name} ===`);
316
- console.log(`Kind: ${ctx.node.kind} | File: ${ctx.node.file}:${ctx.node.line}`);
317
- console.log("");
318
-
319
- if (ctx.source) {
320
- console.log("--- Source ---");
321
- console.log(ctx.source);
322
- console.log("");
323
- }
324
-
325
- if (ctx.callers.length > 0) {
326
- console.log(`--- Callers (${ctx.callers.length}) ---`);
327
- for (const r of ctx.callers) {
328
- const locations = r.edges
329
- .map((e) => `${e.file}:${e.line}`)
330
- .join(", ");
331
- console.log(` ${r.callerSymbol.name} — ${locations}`);
332
- }
333
- console.log("");
334
- }
335
-
336
- if (ctx.callees.length > 0) {
337
- console.log(`--- Callees (${ctx.callees.length}) ---`);
338
- for (const r of ctx.callees) {
339
- const locations = r.edges
340
- .map((e) => `${e.file}:${e.line}`)
341
- .join(", ");
342
- console.log(` ${r.calleeRaw} — ${locations}`);
343
- }
344
- console.log("");
345
- }
346
- } catch (err) {
347
- handleError(err);
348
- }
349
- });
350
-
351
- function printDepsTree(node: DepsNode, prefix: string = "", isLast: boolean = true) {
352
- const connector = isLast ? "└── " : "├── ";
353
- console.log(`${prefix}${connector}${node.name} (${node.kind}, ${node.file}:${node.line})`);
354
- const childPrefix = prefix + (isLast ? " " : "│ ");
355
- for (let i = 0; i < node.children.length; i++) {
356
- printDepsTree(node.children[i], childPrefix, i === node.children.length - 1);
357
- }
358
- }
359
-
360
- program
361
- .command("complexity [file]")
362
- .description("Show cyclomatic complexity for functions and methods")
363
- .option("-s, --sort", "Sort by complexity descending")
364
- .option("-m, --min <number>", "Minimum complexity threshold")
365
- .option("-j, --json", "Output as JSON")
366
- .action((file: string | undefined, opts: { sort?: boolean; min?: string; json?: boolean }) => {
367
- try {
368
- const graph = loadGraphFromCwd();
369
- const results = analyzeComplexity(graph, file);
370
-
371
- if (opts.json) {
372
- console.log(JSON.stringify(results, null, 2));
373
- return;
374
- }
375
-
376
- if (results.length === 0) {
377
- console.log("No functions or methods found.");
378
- return;
379
- }
380
-
381
- let filtered = results;
382
- if (opts.min) {
383
- const threshold = parseInt(opts.min, 10);
384
- filtered = results.filter((r) => r.complexity >= threshold);
385
- }
386
-
387
- if (opts.sort) {
388
- filtered.sort((a, b) => b.complexity - a.complexity);
389
- }
390
-
391
- const maxNameLen = Math.max(...filtered.map((r) => r.symbol.name.length), 6);
392
- const header = `${"Symbol".padEnd(maxNameLen)} Complexity File:Line`;
393
- console.log(header);
394
- console.log("─".repeat(header.length));
395
- for (const r of filtered) {
396
- const receiver = r.symbol.receiver ? `${r.symbol.receiver}.` : "";
397
- console.log(
398
- `${(receiver + r.symbol.name).padEnd(maxNameLen)} ${String(r.complexity).padStart(9)} ${r.symbol.file}:${r.symbol.line}`,
399
- );
400
- }
401
- } catch (err) {
402
- handleError(err);
403
- }
404
- });
405
-
406
- program
407
- .command("hotspot")
408
- .description("Rank files by complexity × size (hotness score)")
409
- .option("-t, --top <number>", "Number of results", "10")
410
- .option("-j, --json", "Output as JSON")
411
- .action((opts: { top?: string; json?: boolean }) => {
412
- try {
413
- const graph = loadGraphFromCwd();
414
- const topN = parseInt(opts.top ?? "10", 10);
415
- const hotspots = findHotspots(graph, topN);
416
-
417
- if (opts.json) {
418
- console.log(JSON.stringify(hotspots, null, 2));
419
- return;
420
- }
421
-
422
- if (hotspots.length === 0) {
423
- console.log("No hotspots found.");
424
- return;
425
- }
426
-
427
- const header = "File Score Symbols Complexity Lines";
428
- console.log(header);
429
- console.log("─".repeat(header.length));
430
- for (const h of hotspots) {
431
- console.log(
432
- `${h.file.padEnd(52)} ${String(h.score).padStart(7)} ${String(h.symbolCount).padStart(8)} ${String(h.totalComplexity).padStart(11)} ${String(h.lines).padStart(6)}`,
433
- );
434
- }
435
- } catch (err) {
436
- handleError(err);
437
- }
438
- });
439
-
440
- program
441
- .command("coupling")
442
- .description("Show package coupling based on import edges")
443
- .option("-p, --package <name>", "Filter to a specific package")
444
- .option("-j, --json", "Output as JSON")
445
- .action((opts: { package?: string; json?: boolean }) => {
446
- try {
447
- const graph = loadGraphFromCwd();
448
- let results = analyzeCoupling(graph);
449
-
450
- if (opts.package) {
451
- results = results.filter((r) => r.packageName === opts.package);
452
- }
453
-
454
- if (opts.json) {
455
- console.log(JSON.stringify(results, null, 2));
456
- return;
457
- }
458
-
459
- if (results.length === 0) {
460
- console.log("No coupling data found.");
461
- return;
462
- }
463
-
464
- const header = "Package Coupled To Imports Files";
465
- console.log(header);
466
- console.log("─".repeat(header.length));
467
- for (const r of results) {
468
- console.log(
469
- `${r.packageName.padEnd(14)} ${r.coupledTo.padEnd(18)} ${String(r.importCount).padStart(7)} ${String(r.fileCount).padStart(6)}`,
470
- );
471
- }
472
- } catch (err) {
473
- handleError(err);
474
- }
475
- });
476
-
477
- program
478
- .command("deps <symbol>")
479
- .description("Show the call dependency tree for a symbol")
480
- .option("-d, --depth <number>", "Max tree depth", "3")
481
- .action((symbol: string, opts: { depth?: string }) => {
482
- try {
483
- const graph = loadGraphFromCwd();
484
- const maxDepth = parseInt(opts.depth ?? "3", 10);
485
- const tree = dependencyTree(graph, symbol, maxDepth);
486
-
487
- if (!tree) {
488
- console.log(`Symbol "${symbol}" not found.`);
489
- return;
490
- }
491
-
492
- console.log(`Dependency tree for "${symbol}" (depth ${maxDepth}):`);
493
- console.log("");
494
- printDepsTree(tree);
495
- } catch (err) {
496
- handleError(err);
497
- }
498
- });
499
-
500
- function printImpactTree(results: ImpactNode[], symbolName: string) {
501
- const byDepth = new Map<number, ImpactNode[]>();
502
- for (const r of results) {
503
- const list = byDepth.get(r.depth) ?? [];
504
- list.push(r);
505
- byDepth.set(r.depth, list);
506
- }
507
-
508
- console.log(`Impact of "${symbolName}":`);
509
- for (const [depth, nodes] of [...byDepth.entries()].sort((a, b) => a[0] - b[0])) {
510
- for (const n of nodes) {
511
- const prefix = " ".repeat(depth);
512
- console.log(`${prefix}${n.symbol.name} (depth ${depth}, ${n.symbol.file}:${n.symbol.line})`);
513
- }
514
- }
515
- }
516
-
517
- program
518
- .command("boundaries")
519
- .description("Check architecture boundaries defined in .tsgraph/boundaries.json")
520
- .option("-j, --json", "Output as JSON")
521
- .action((opts: { json?: boolean }) => {
522
- try {
523
- const graph = loadGraphFromCwd();
524
- const config = loadBoundariesConfig(process.cwd());
525
- if (!config) {
526
- console.log("No .tsgraph/boundaries.json found.");
527
- return;
528
- }
529
- const result = checkBoundaries(graph, config);
530
-
531
- if (opts.json) {
532
- console.log(JSON.stringify(result, null, 2));
533
- return;
534
- }
535
-
536
- console.log(`Boundary check: ${result.violations.length} violation(s), ${result.allowed} allowed`);
537
- console.log(`Layers: ${config.layers.map((l) => l.name).join(", ")}`);
538
- console.log("");
539
- if (result.violations.length > 0) {
540
- console.log("Violations:");
541
- for (const v of result.violations) {
542
- console.log(` ❌ ${v.fromFile} → ${v.toFile}: ${v.rule}`);
543
- }
544
- } else {
545
- console.log("All imports respect layer boundaries.");
546
- }
547
- } catch (err) {
548
- handleError(err);
549
- }
550
- });
551
-
552
- program
553
- .command("changes")
554
- .description("Show files and symbols changed vs a base branch")
555
- .option("--base <branch>", "Base branch to compare against", "main")
556
- .option("-j, --json", "Output as JSON")
557
- .action((opts: { base?: string; json?: boolean }) => {
558
- try {
559
- const graph = loadGraphFromCwd();
560
- const result = getChanges(graph, process.cwd(), opts.base ?? "main");
561
-
562
- if (opts.json) {
563
- console.log(JSON.stringify(result, null, 2));
564
- return;
565
- }
566
-
567
- if (result.totalFiles === 0) {
568
- console.log("No changes found.");
569
- return;
570
- }
571
-
572
- console.log(`Changes vs "${opts.base}" (${result.totalFiles} files, ${result.totalSymbols} symbols):`);
573
- for (const f of result.files) {
574
- const status = f.status === "added" ? "+" : f.status === "deleted" ? "-" : "M";
575
- console.log(` ${status} ${f.path} (${f.symbolCount} symbols)`);
576
- }
577
- } catch (err) {
578
- handleError(err);
579
- }
580
- });
581
-
582
- program
583
- .command("stale")
584
- .description("Show files not modified in N days")
585
- .option("--days <number>", "Staleness threshold in days", "90")
586
- .option("-j, --json", "Output as JSON")
587
- .action((opts: { days?: string; json?: boolean }) => {
588
- try {
589
- const graph = loadGraphFromCwd();
590
- const days = parseInt(opts.days ?? "90", 10);
591
- const result = getStale(graph, process.cwd(), days);
592
-
593
- if (opts.json) {
594
- console.log(JSON.stringify(result, null, 2));
595
- return;
596
- }
597
-
598
- if (result.totalFiles === 0) {
599
- console.log("No stale files found.");
600
- return;
601
- }
602
-
603
- console.log(`Stale files (${days}+ days, ${result.totalFiles} total):`);
604
- for (const f of result.files) {
605
- console.log(` ${f.path} — ${f.symbolCount} symbol(s): ${f.symbolNames.join(", ")}`);
606
- }
607
- } catch (err) {
608
- handleError(err);
609
- }
610
- });
611
-
612
- program
613
- .command("plan")
614
- .description("Generate a change plan showing blast radius for given files/symbols")
615
- .argument("<files...>", "Files being changed")
616
- .option("-s, --symbols <symbols>", "Comma-separated symbols being changed")
617
- .option("--md", "Output as Markdown")
618
- .action((files: string[], opts: { symbols?: string; md?: boolean }) => {
619
- try {
620
- const graph = loadGraphFromCwd();
621
- const symbols = opts.symbols ? opts.symbols.split(",").map((s) => s.trim()).filter(Boolean) : [];
622
- const result = generatePlan(graph, files, symbols);
623
-
624
- if (opts.md) {
625
- console.log(`## Change Plan`);
626
- console.log(``);
627
- console.log(`${result.summary}`);
628
- console.log(``);
629
- console.log(`### Files Changed`);
630
- for (const f of result.changes.files) {
631
- console.log(`- \`${f}\``);
632
- }
633
- console.log(``);
634
- console.log(`### Symbols Changed`);
635
- for (const s of result.changes.symbols) {
636
- console.log(`- \`${s}\``);
637
- }
638
- console.log(``);
639
- console.log(`### Affected Files`);
640
- for (const f of result.affectedFiles) {
641
- console.log(`- \`${f}\``);
642
- }
643
- console.log(``);
644
- console.log(`### Caller Impact`);
645
- for (const c of result.affectedCallers) {
646
- console.log(`- \`${c.symbol.name}\` — ${c.callerCount} caller(s) at ${c.symbol.file}:${c.symbol.line}`);
647
- }
648
- return;
649
- }
650
-
651
- console.log(result.summary);
652
- console.log("");
653
- console.log(`Files changed: ${result.changes.files.length}`);
654
- for (const f of result.changes.files) {
655
- console.log(` ${f}`);
656
- }
657
- console.log(`Symbols changed: ${result.changes.symbols.length}`);
658
- for (const s of result.changes.symbols) {
659
- const sym = graph.symbols.find((n) => n.name === s);
660
- if (sym) console.log(` ${sym.kind} ${s} — ${sym.file}:${sym.line}`);
661
- }
662
- console.log(`Affected files: ${result.affectedFiles.length}`);
663
- } catch (err) {
664
- handleError(err);
665
- }
666
- });
667
-
668
- program
669
- .command("review")
670
- .description("Generate a code review summary vs a base branch")
671
- .option("--base <branch>", "Base branch to compare against", "main")
672
- .option("--md", "Output as Markdown")
673
- .action((opts: { base?: string; md?: boolean }) => {
674
- try {
675
- const graph = loadGraphFromCwd();
676
- const result = generateReview(graph, process.cwd(), opts.base ?? "main");
677
-
678
- if (opts.md) {
679
- console.log(`## Code Review`);
680
- console.log(``);
681
- console.log(`${result.summary}`);
682
- console.log(``);
683
- if (result.findings.length > 0) {
684
- console.log(`### Findings`);
685
- for (const f of result.findings) {
686
- const icon = f.type === "orphan" ? "💀" : f.type === "boundary" ? "🚫" : "📦";
687
- console.log(`- ${icon} \`${f.type}\`: ${f.detail}`);
688
- }
689
- }
690
- return;
691
- }
692
-
693
- console.log(result.summary);
694
- if (result.findings.length > 0) {
695
- console.log("");
696
- console.log("Findings:");
697
- for (const f of result.findings) {
698
- console.log(` [${f.type}] ${f.detail}`);
699
- }
700
- }
701
- } catch (err) {
702
- handleError(err);
703
- }
704
- });
705
-
706
- program
707
- .command("add-opencode-plugin")
708
- .description("Configure opencode to use tsgraph (updates opencode.json, creates .opencode/agents/tsgraph.json)")
709
- .action(() => {
710
- try {
711
- const result = addOpencodePlugin(process.cwd());
712
- if (result.errors.length > 0) {
713
- for (const err of result.errors) {
714
- console.error("Error:", err);
715
- }
716
- }
717
- if (result.opencodeJsonUpdated) console.log("✓ Updated opencode.json with tsgraph MCP server");
718
- if (result.agentCreated) console.log("✓ Created .opencode/agents/tsgraph.json");
719
- if (result.errors.length === 0) console.log("Done.");
720
- } catch (err) {
721
- handleError(err);
722
- }
723
- });
724
-
725
- program
726
- .command("mcp")
727
- .description("Start the MCP stdio server for AI agent integration")
728
- .action(async () => {
729
- try {
730
- await startMcpServer(process.cwd());
731
- } catch (err) {
732
- handleError(err);
733
- }
734
- });
735
-
736
- program.parse(process.argv);