@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.
- package/dist/changes/index.test.js +2 -6
- package/dist/changes/index.test.js.map +1 -1
- package/dist/cli/index.js +184 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/git/index.test.js +4 -6
- package/dist/git/index.test.js.map +1 -1
- package/dist/opencode/index.js +1 -1
- package/dist/opencode/index.js.map +1 -1
- package/dist/opencode/index.test.js +2 -2
- package/dist/opencode/index.test.js.map +1 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +12 -4
- package/dist/search/index.js.map +1 -1
- package/package.json +16 -1
- package/AGENTS.md +0 -64
- package/TODOS.md +0 -61
- package/opencode.json +0 -24
- package/src/analysis/analysis.test.ts +0 -405
- package/src/analysis/complexity.ts +0 -107
- package/src/analysis/coupling.ts +0 -106
- package/src/analysis/hotspot.ts +0 -52
- package/src/analysis/index.ts +0 -17
- package/src/boundaries/index.test.ts +0 -335
- package/src/boundaries/index.ts +0 -137
- package/src/changes/index.test.ts +0 -114
- package/src/changes/index.ts +0 -95
- package/src/cli/index.ts +0 -736
- package/src/git/index.test.ts +0 -92
- package/src/git/index.ts +0 -86
- package/src/graph/types.test.ts +0 -383
- package/src/graph/types.ts +0 -353
- package/src/mcp/mcp.test.ts +0 -176
- package/src/mcp/server.ts +0 -217
- package/src/nextjs/index.ts +0 -23
- package/src/nextjs/nextjs.test.ts +0 -233
- package/src/nextjs/pages.ts +0 -43
- package/src/nextjs/react.ts +0 -100
- package/src/nextjs/router.ts +0 -102
- package/src/nextjs/routes.ts +0 -69
- package/src/opencode/index.test.ts +0 -90
- package/src/opencode/index.ts +0 -83
- package/src/parser/index.ts +0 -339
- package/src/parser/parser.test.ts +0 -282
- package/src/plan/index.test.ts +0 -162
- package/src/plan/index.ts +0 -161
- package/src/report/index.ts +0 -128
- package/src/scanner/index.ts +0 -97
- package/src/scanner/scanner.test.ts +0 -135
- package/src/search/index.ts +0 -163
- package/src/search/search.test.ts +0 -512
- package/src/traversal/index.ts +0 -5
- package/src/traversal/traversal.test.ts +0 -266
- package/src/traversal/traversal.ts +0 -185
- package/tsconfig.json +0 -20
- 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);
|