universal-ast-mapper 2.0.1 → 2.0.2
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/CHANGELOG.md +6 -0
- package/dist/serve.js +144 -1
- package/dist/webapp.js +305 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,12 @@ since 1.0.0, guarantees a stable MCP tool / CLI surface across the 1.x line.
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [2.0.2] — 2026-06-21 · patch
|
|
10
|
+
|
|
11
|
+
- **feat:** interactive **Run Commands** page in `ast-map serve` web UI — 15 one-click analysis commands (dead exports, circular deps, duplicates, similar code, complexity, symbol search, change impact, file deps, explain symbol, code smells, security scan, arch rules, Mermaid diagram, Markdown docs, top symbols) with a two-panel layout: command palette on the left, closeable result tabs on the right
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
9
15
|
## [2.0.1] — 2026-06-21 · patch
|
|
10
16
|
|
|
11
17
|
- **fix:** add `prepare` script so `dist/` is built automatically on `npm install` (cloners no longer need a separate `npm run build`)
|
package/dist/serve.js
CHANGED
|
@@ -2,7 +2,7 @@ import http from "node:http";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { collectSourceFiles } from "./skeleton.js";
|
|
5
|
-
import { resolveOptions } from "./config.js";
|
|
5
|
+
import { resolveOptions, loadProjectConfig } from "./config.js";
|
|
6
6
|
import { buildSymbolGraph } from "./graph.js";
|
|
7
7
|
import { findDeadExports, findCircularDeps, getTopSymbols } from "./graph-analysis.js";
|
|
8
8
|
import { buildReport } from "./report.js";
|
|
@@ -11,6 +11,22 @@ import { detectSmells } from "./smells.js";
|
|
|
11
11
|
import { scanFileForSecurityIssues } from "./security.js";
|
|
12
12
|
import { buildSkeletonsBulk } from "./pool.js";
|
|
13
13
|
import { webAppHtml } from "./webapp.js";
|
|
14
|
+
import { computeFileComplexity } from "./complexity.js";
|
|
15
|
+
import { findDuplicateSymbols, getChangeImpact, getFileDeps } from "./graph-analysis.js";
|
|
16
|
+
import { findSimilar } from "./similar.js";
|
|
17
|
+
import { checkArchRules, loadArchRules } from "./arch-rules.js";
|
|
18
|
+
import { buildClassDiagram, buildDepsDiagram, buildModulesDiagram } from "./diagram.js";
|
|
19
|
+
import { buildDocOutput, renderMarkdown } from "./docgen.js";
|
|
20
|
+
import { buildExplainResult } from "./explain.js";
|
|
21
|
+
import { searchSymbols } from "./search.js";
|
|
22
|
+
function readBody(req) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const chunks = [];
|
|
25
|
+
req.on("data", (c) => chunks.push(c));
|
|
26
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
27
|
+
req.on("error", reject);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
14
30
|
export async function startServe(opts) {
|
|
15
31
|
const root = opts.root;
|
|
16
32
|
const scanDir = opts.scanDir ?? root;
|
|
@@ -61,6 +77,12 @@ export async function startServe(opts) {
|
|
|
61
77
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
62
78
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
63
79
|
try {
|
|
80
|
+
if (req.method === "OPTIONS") {
|
|
81
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
82
|
+
res.writeHead(204);
|
|
83
|
+
res.end();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
64
86
|
if (pathname === "/" || pathname === "/index.html") {
|
|
65
87
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
66
88
|
res.end(webAppHtml(port));
|
|
@@ -171,6 +193,127 @@ export async function startServe(opts) {
|
|
|
171
193
|
res.end(JSON.stringify(all, null, 2));
|
|
172
194
|
return;
|
|
173
195
|
}
|
|
196
|
+
if (pathname === "/api/run" && req.method === "POST") {
|
|
197
|
+
const body = await readBody(req);
|
|
198
|
+
const { cmd, args = {} } = JSON.parse(body);
|
|
199
|
+
const skeletons = await getSkeletons();
|
|
200
|
+
const graph = buildSymbolGraph(skeletons, root);
|
|
201
|
+
let data;
|
|
202
|
+
switch (cmd) {
|
|
203
|
+
case "dead":
|
|
204
|
+
data = findDeadExports(graph);
|
|
205
|
+
break;
|
|
206
|
+
case "cycles":
|
|
207
|
+
data = findCircularDeps(graph);
|
|
208
|
+
break;
|
|
209
|
+
case "duplicates":
|
|
210
|
+
data = findDuplicateSymbols(graph);
|
|
211
|
+
break;
|
|
212
|
+
case "top":
|
|
213
|
+
data = getTopSymbols(graph, args.limit ?? 20);
|
|
214
|
+
break;
|
|
215
|
+
case "similar":
|
|
216
|
+
data = findSimilar(skeletons, { minGroupSize: args.minGroupSize ?? 2 });
|
|
217
|
+
break;
|
|
218
|
+
case "smells": {
|
|
219
|
+
const all = [];
|
|
220
|
+
for (const skel of skeletons) {
|
|
221
|
+
try {
|
|
222
|
+
const src = fs.readFileSync(path.resolve(root, skel.file), "utf8");
|
|
223
|
+
all.push(...detectSmells(skel, src.split("\n").length));
|
|
224
|
+
}
|
|
225
|
+
catch { /* skip */ }
|
|
226
|
+
}
|
|
227
|
+
data = all;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
case "security": {
|
|
231
|
+
const all = [];
|
|
232
|
+
for (const skel of skeletons) {
|
|
233
|
+
try {
|
|
234
|
+
const src = fs.readFileSync(path.resolve(root, skel.file), "utf8");
|
|
235
|
+
all.push(...scanFileForSecurityIssues(src, skel.file));
|
|
236
|
+
}
|
|
237
|
+
catch { /* skip */ }
|
|
238
|
+
}
|
|
239
|
+
data = all;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case "complexity": {
|
|
243
|
+
const results = [];
|
|
244
|
+
for (const skel of skeletons) {
|
|
245
|
+
try {
|
|
246
|
+
results.push(await computeFileComplexity(path.resolve(root, skel.file), skel.file));
|
|
247
|
+
}
|
|
248
|
+
catch { /* skip */ }
|
|
249
|
+
}
|
|
250
|
+
data = results;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case "find": {
|
|
254
|
+
if (!args.query)
|
|
255
|
+
throw new Error("query required");
|
|
256
|
+
data = await searchSymbols(scanDir, args.query, root, {
|
|
257
|
+
matchType: args.matchType ?? "contains",
|
|
258
|
+
kind: args.kind,
|
|
259
|
+
});
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case "impact": {
|
|
263
|
+
if (!args.symbol)
|
|
264
|
+
throw new Error("symbol required");
|
|
265
|
+
data = getChangeImpact(graph, args.symbol);
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
case "fileDeps": {
|
|
269
|
+
if (!args.file)
|
|
270
|
+
throw new Error("file required");
|
|
271
|
+
data = getFileDeps(graph, args.file);
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
case "explain": {
|
|
275
|
+
if (!args.file || !args.symbol)
|
|
276
|
+
throw new Error("file and symbol required");
|
|
277
|
+
const skel = skeletons.find((s) => s.file === args.file || s.file.endsWith(args.file));
|
|
278
|
+
if (!skel)
|
|
279
|
+
throw new Error(`File not found: ${args.file}`);
|
|
280
|
+
const nodeId = `${skel.file}::${args.symbol}`;
|
|
281
|
+
const impact = getChangeImpact(graph, nodeId);
|
|
282
|
+
data = buildExplainResult(args.symbol, skel, graph, impact, []);
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
case "arch": {
|
|
286
|
+
const cfg = loadProjectConfig(root);
|
|
287
|
+
const rules = loadArchRules(cfg);
|
|
288
|
+
data = checkArchRules(graph, rules);
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
case "diagram": {
|
|
292
|
+
const type = args.type ?? "deps";
|
|
293
|
+
if (type === "class")
|
|
294
|
+
data = buildClassDiagram(skeletons);
|
|
295
|
+
else if (type === "modules")
|
|
296
|
+
data = buildModulesDiagram(graph);
|
|
297
|
+
else
|
|
298
|
+
data = buildDepsDiagram(graph, args.maxNodes ?? 50);
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
case "doc": {
|
|
302
|
+
const docOut = buildDocOutput(skeletons, { exportedOnly: args.exportedOnly ?? false });
|
|
303
|
+
data = {
|
|
304
|
+
markdown: renderMarkdown(docOut),
|
|
305
|
+
files: docOut.files.length,
|
|
306
|
+
symbols: docOut.files.reduce((a, f) => a + f.symbols.length, 0),
|
|
307
|
+
};
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
default:
|
|
311
|
+
throw new Error(`Unknown command: ${cmd}`);
|
|
312
|
+
}
|
|
313
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
314
|
+
res.end(JSON.stringify({ ok: true, cmd, data }));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
174
317
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
175
318
|
res.end("Not found");
|
|
176
319
|
}
|
package/dist/webapp.js
CHANGED
|
@@ -55,6 +55,29 @@ export function webAppHtml(port) {
|
|
|
55
55
|
.error-box { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.3); border-radius: 6px; padding: 12px 16px; color: var(--red); font-size: 13px; margin-top: 8px; }
|
|
56
56
|
.loading { color: var(--muted); font-size: 13px; padding: 32px; text-align: center; }
|
|
57
57
|
.timeline { height: 120px; width: 100%; }
|
|
58
|
+
.run-layout { display: grid; grid-template-columns: 260px 1fr; gap: 16px; height: calc(100vh - 120px); }
|
|
59
|
+
.cmd-panel { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; overflow-y: auto; padding: 10px; }
|
|
60
|
+
.cmd-group-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); padding: 10px 6px 4px; }
|
|
61
|
+
.cmd-btn { width: 100%; text-align: left; padding: 8px 10px; border: none; border-radius: 6px; background: transparent; color: var(--text); font-size: 13px; cursor: pointer; display: block; margin-bottom: 1px; }
|
|
62
|
+
.cmd-btn:hover { background: rgba(124,58,237,.15); color: var(--accent); }
|
|
63
|
+
.cmd-form { padding: 4px 10px 8px; display: none; }
|
|
64
|
+
.cmd-form.open { display: block; }
|
|
65
|
+
.cmd-input { width: 100%; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 5px 8px; color: var(--text); font-size: 12px; margin-bottom: 4px; outline: none; }
|
|
66
|
+
.cmd-input:focus { border-color: var(--accent); }
|
|
67
|
+
.cmd-run-btn { padding: 5px 14px; background: var(--accent); border: none; border-radius: 4px; color: #fff; font-size: 12px; cursor: pointer; }
|
|
68
|
+
.result-panel { display: flex; flex-direction: column; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
|
69
|
+
.tab-bar { display: flex; border-bottom: 1px solid var(--border); overflow-x: auto; flex-shrink: 0; background: var(--bg); min-height: 36px; }
|
|
70
|
+
.tab { display: flex; align-items: center; gap: 6px; padding: 8px 14px; font-size: 12px; cursor: pointer; border-right: 1px solid var(--border); white-space: nowrap; color: var(--muted); user-select: none; }
|
|
71
|
+
.tab.active { background: var(--surface); color: var(--text); border-bottom: 2px solid var(--accent); margin-bottom: -1px; }
|
|
72
|
+
.tab-close { opacity: .5; line-height: 1; }
|
|
73
|
+
.tab-close:hover { opacity: 1; }
|
|
74
|
+
.tab-content { flex: 1; overflow-y: auto; padding: 16px; }
|
|
75
|
+
.tab-pane { display: none; }
|
|
76
|
+
.tab-pane.active { display: block; }
|
|
77
|
+
.result-pre { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 12px; font-family: monospace; font-size: 12px; white-space: pre-wrap; overflow-x: auto; max-height: 500px; overflow-y: auto; }
|
|
78
|
+
.empty-tabs { display: flex; align-items: center; justify-content: center; height: 200px; color: var(--muted); font-size: 13px; }
|
|
79
|
+
.spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin .6s linear infinite; vertical-align: middle; margin-right: 6px; }
|
|
80
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
58
81
|
</style>
|
|
59
82
|
</head>
|
|
60
83
|
<body>
|
|
@@ -71,6 +94,8 @@ export function webAppHtml(port) {
|
|
|
71
94
|
<div class="nav-item" data-page="smells">🤢 Code Smells</div>
|
|
72
95
|
<div class="nav-item" data-page="security">🔒 Security</div>
|
|
73
96
|
<div class="nav-item" data-page="dead">💀 Dead Code</div>
|
|
97
|
+
<div class="nav-section">Commands</div>
|
|
98
|
+
<div class="nav-item" data-page="run">⚡ Run Commands</div>
|
|
74
99
|
</div>
|
|
75
100
|
|
|
76
101
|
<div id="main">
|
|
@@ -132,6 +157,21 @@ export function webAppHtml(port) {
|
|
|
132
157
|
<div class="subtitle">Exported symbols with no known importers inside the scanned directory</div>
|
|
133
158
|
<div class="card"><table><thead><tr><th>Symbol</th><th>Kind</th><th>File</th><th>Confidence</th></tr></thead><tbody id="dead-table"></tbody></table></div>
|
|
134
159
|
</div>
|
|
160
|
+
|
|
161
|
+
<!-- RUN COMMANDS -->
|
|
162
|
+
<div class="page" id="page-run">
|
|
163
|
+
<div class="header-row"><h1>Run Commands</h1></div>
|
|
164
|
+
<div class="subtitle">Interactive analysis — click a command to run it instantly</div>
|
|
165
|
+
<div class="run-layout">
|
|
166
|
+
<div class="cmd-panel" id="cmd-panel"></div>
|
|
167
|
+
<div class="result-panel">
|
|
168
|
+
<div class="tab-bar" id="tab-bar"></div>
|
|
169
|
+
<div class="tab-content" id="tab-content">
|
|
170
|
+
<div class="empty-tabs" id="empty-tabs">↑ Pick a command to run</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
135
175
|
</div>
|
|
136
176
|
|
|
137
177
|
<div class="tooltip" id="tooltip"></div>
|
|
@@ -181,6 +221,7 @@ function renderPage(name) {
|
|
|
181
221
|
else if (name === 'smells') renderSmells();
|
|
182
222
|
else if (name === 'security') renderSecurity();
|
|
183
223
|
else if (name === 'dead') renderDead();
|
|
224
|
+
else if (name === 'run') renderRun();
|
|
184
225
|
}
|
|
185
226
|
|
|
186
227
|
function grade(s) { return s >= 90 ? 'A' : s >= 80 ? 'B' : s >= 70 ? 'C' : s >= 60 ? 'D' : 'F'; }
|
|
@@ -326,6 +367,270 @@ function renderDead() {
|
|
|
326
367
|
|
|
327
368
|
function esc(s) { return String(s ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
328
369
|
|
|
370
|
+
// ─── Run Commands ─────────────────────────────────────────────────────────────
|
|
371
|
+
var CMDS = [
|
|
372
|
+
{ group: 'Analysis', items: [
|
|
373
|
+
{ id: 'dead', label: '💀 Dead Exports', desc: 'Find unused exported symbols' },
|
|
374
|
+
{ id: 'cycles', label: '🔄 Circular Deps', desc: 'Circular dependency chains' },
|
|
375
|
+
{ id: 'duplicates', label: '♊ Duplicates', desc: 'Symbols defined more than once' },
|
|
376
|
+
{ id: 'similar', label: '🔍 Similar Code', desc: 'Structurally similar functions' },
|
|
377
|
+
{ id: 'complexity', label: '📊 Complexity', desc: 'Cyclomatic complexity per file' },
|
|
378
|
+
{ id: 'top', label: '🏆 Top Symbols', desc: 'Most imported symbols' }
|
|
379
|
+
]},
|
|
380
|
+
{ group: 'Quality', items: [
|
|
381
|
+
{ id: 'smells', label: '🤢 Code Smells', desc: 'Detect anti-patterns' },
|
|
382
|
+
{ id: 'security', label: '🔒 Security Scan', desc: 'Security vulnerabilities' },
|
|
383
|
+
{ id: 'arch', label: '🏛️ Arch Rules', desc: 'Architecture rule violations' }
|
|
384
|
+
]},
|
|
385
|
+
{ group: 'Search & Explore', items: [
|
|
386
|
+
{ id: 'find', label: '🔎 Find Symbol', desc: 'Search symbols by name', fields: [
|
|
387
|
+
{ name: 'query', ph: 'Symbol name…', req: true },
|
|
388
|
+
{ name: 'kind', ph: 'Kind filter (fn, class…)', req: false }
|
|
389
|
+
]},
|
|
390
|
+
{ id: 'impact', label: '💥 Change Impact', desc: 'Blast radius of changing a symbol', fields: [
|
|
391
|
+
{ name: 'symbol', ph: 'file.ts::SymbolName', req: true }
|
|
392
|
+
]},
|
|
393
|
+
{ id: 'fileDeps', label: '📦 File Deps', desc: 'What a file imports and who imports it', fields: [
|
|
394
|
+
{ name: 'file', ph: 'src/foo.ts', req: true }
|
|
395
|
+
]},
|
|
396
|
+
{ id: 'explain', label: '💡 Explain Symbol', desc: 'Full structural context of a symbol', fields: [
|
|
397
|
+
{ name: 'file', ph: 'src/foo.ts', req: true },
|
|
398
|
+
{ name: 'symbol', ph: 'SymbolName', req: true }
|
|
399
|
+
]}
|
|
400
|
+
]},
|
|
401
|
+
{ group: 'Generate', items: [
|
|
402
|
+
{ id: 'diagram', label: '🕸️ Diagram', desc: 'Mermaid diagram of the codebase', fields: [
|
|
403
|
+
{ name: 'type', ph: 'deps | class | modules', req: false }
|
|
404
|
+
]},
|
|
405
|
+
{ id: 'doc', label: '📝 Docs', desc: 'Generate Markdown documentation' }
|
|
406
|
+
]}
|
|
407
|
+
];
|
|
408
|
+
|
|
409
|
+
var _tabCount = 0;
|
|
410
|
+
var _runInit = false;
|
|
411
|
+
|
|
412
|
+
function renderRun() {
|
|
413
|
+
if (_runInit) return;
|
|
414
|
+
_runInit = true;
|
|
415
|
+
var panel = document.getElementById('cmd-panel');
|
|
416
|
+
var html = '';
|
|
417
|
+
CMDS.forEach(function(g) {
|
|
418
|
+
html += '<div class="cmd-group-label">' + g.group + '</div>';
|
|
419
|
+
g.items.forEach(function(cmd) {
|
|
420
|
+
html += '<button class="cmd-btn" title="' + esc(cmd.desc) + '" onclick="toggleForm(\'' + cmd.id + '\')">' + cmd.label + '</button>';
|
|
421
|
+
if (cmd.fields) {
|
|
422
|
+
html += '<div class="cmd-form" id="form-' + cmd.id + '">';
|
|
423
|
+
cmd.fields.forEach(function(f) {
|
|
424
|
+
html += '<input class="cmd-input" id="inp-' + cmd.id + '-' + f.name + '" placeholder="' + esc(f.ph) + '" onkeydown="if(event.key===\'Enter\')runCmd(\'' + cmd.id + '\')" />';
|
|
425
|
+
});
|
|
426
|
+
html += '<button class="cmd-run-btn" onclick="runCmd(\'' + cmd.id + '\')">Run ▶</button>';
|
|
427
|
+
html += '</div>';
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
panel.innerHTML = html;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function findCmd(id) {
|
|
435
|
+
for (var i = 0; i < CMDS.length; i++) {
|
|
436
|
+
for (var j = 0; j < CMDS[i].items.length; j++) {
|
|
437
|
+
if (CMDS[i].items[j].id === id) return CMDS[i].items[j];
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function toggleForm(id) {
|
|
444
|
+
var cmd = findCmd(id);
|
|
445
|
+
if (!cmd || !cmd.fields) { runCmd(id); return; }
|
|
446
|
+
var form = document.getElementById('form-' + id);
|
|
447
|
+
if (form) form.classList.toggle('open');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function runCmd(id) {
|
|
451
|
+
var cmd = findCmd(id);
|
|
452
|
+
if (!cmd) return;
|
|
453
|
+
var args = {};
|
|
454
|
+
if (cmd.fields) {
|
|
455
|
+
for (var i = 0; i < cmd.fields.length; i++) {
|
|
456
|
+
var f = cmd.fields[i];
|
|
457
|
+
var inp = document.getElementById('inp-' + id + '-' + f.name);
|
|
458
|
+
var v = inp ? inp.value.trim() : '';
|
|
459
|
+
if (f.req && !v) { alert(f.name + ' is required'); return; }
|
|
460
|
+
if (v) args[f.name] = v;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
var firstArg = args.query || args.symbol || args.file || args.type || '';
|
|
464
|
+
var label = cmd.label + (firstArg ? ': ' + firstArg : '');
|
|
465
|
+
var tabId = ++_tabCount;
|
|
466
|
+
addTab(tabId, label, '<div class="loading"><span class="spinner"></span> Running…</div>');
|
|
467
|
+
try {
|
|
468
|
+
var r = await fetch('http://localhost:${port}/api/run', {
|
|
469
|
+
method: 'POST',
|
|
470
|
+
headers: { 'Content-Type': 'application/json' },
|
|
471
|
+
body: JSON.stringify({ cmd: id, args: args })
|
|
472
|
+
});
|
|
473
|
+
var json = await r.json();
|
|
474
|
+
if (!r.ok) throw new Error(json.error || r.statusText);
|
|
475
|
+
setTabContent(tabId, renderResult(id, json.data));
|
|
476
|
+
} catch(e) {
|
|
477
|
+
setTabContent(tabId, '<div class="error-box">' + esc(e.message) + '</div>');
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function addTab(id, label, content) {
|
|
482
|
+
var emptyEl = document.getElementById('empty-tabs');
|
|
483
|
+
if (emptyEl) emptyEl.remove();
|
|
484
|
+
var bar = document.getElementById('tab-bar');
|
|
485
|
+
var tc = document.getElementById('tab-content');
|
|
486
|
+
bar.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
|
|
487
|
+
tc.querySelectorAll('.tab-pane').forEach(function(t) { t.classList.remove('active'); });
|
|
488
|
+
var tab = document.createElement('div');
|
|
489
|
+
tab.className = 'tab active';
|
|
490
|
+
tab.dataset.tab = id;
|
|
491
|
+
tab.innerHTML = '<span>' + esc(label) + '</span><span class="tab-close" onclick="closeTab(' + id + ',event)">✕</span>';
|
|
492
|
+
tab.addEventListener('click', function(e) { if (!e.target.classList.contains('tab-close')) switchTab(id); });
|
|
493
|
+
bar.appendChild(tab);
|
|
494
|
+
var pane = document.createElement('div');
|
|
495
|
+
pane.className = 'tab-pane active';
|
|
496
|
+
pane.id = 'pane-' + id;
|
|
497
|
+
pane.innerHTML = content;
|
|
498
|
+
tc.appendChild(pane);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function setTabContent(id, html) {
|
|
502
|
+
var pane = document.getElementById('pane-' + id);
|
|
503
|
+
if (pane) pane.innerHTML = html;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function switchTab(id) {
|
|
507
|
+
document.querySelectorAll('.tab').forEach(function(t) { t.classList.toggle('active', t.dataset.tab == id); });
|
|
508
|
+
document.querySelectorAll('.tab-pane').forEach(function(t) { t.classList.toggle('active', t.id === 'pane-' + id); });
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function closeTab(id, e) {
|
|
512
|
+
e.stopPropagation();
|
|
513
|
+
var tab = document.querySelector('.tab[data-tab="' + id + '"]');
|
|
514
|
+
var pane = document.getElementById('pane-' + id);
|
|
515
|
+
var wasActive = tab && tab.classList.contains('active');
|
|
516
|
+
if (tab) tab.remove();
|
|
517
|
+
if (pane) pane.remove();
|
|
518
|
+
if (wasActive) {
|
|
519
|
+
var remaining = document.querySelectorAll('.tab');
|
|
520
|
+
if (remaining.length > 0) {
|
|
521
|
+
switchTab(remaining[remaining.length - 1].dataset.tab);
|
|
522
|
+
} else {
|
|
523
|
+
document.getElementById('tab-content').innerHTML = '<div class="empty-tabs" id="empty-tabs">↑ Pick a command to run</div>';
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function renderResult(cmd, data) {
|
|
529
|
+
if (data == null) return '<div style="color:var(--muted)">No results</div>';
|
|
530
|
+
if (cmd === 'dead') {
|
|
531
|
+
if (!data.length) return '<div style="color:var(--green)">No dead exports 🎉</div>';
|
|
532
|
+
return renderTable(['Symbol','Kind','File','Confidence'], data, function(d) {
|
|
533
|
+
return [d.symbol, '<span class="pill">'+esc(d.kind)+'</span>', d.file, '<span class="badge '+(d.confidence==='high'?'badge-red':'badge-yellow')+'">'+d.confidence+'</span>'];
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
if (cmd === 'cycles') {
|
|
537
|
+
if (!data.length) return '<div style="color:var(--green)">No cycles 🎉</div>';
|
|
538
|
+
return data.map(function(c) { return '<div class="card" style="margin-bottom:8px"><b>Cycle:</b> <span style="font-family:monospace;font-size:12px">'+esc((c.cycle||[]).join(' → '))+'</span></div>'; }).join('');
|
|
539
|
+
}
|
|
540
|
+
if (cmd === 'duplicates') {
|
|
541
|
+
if (!data.length) return '<div style="color:var(--green)">No duplicates 🎉</div>';
|
|
542
|
+
return renderTable(['Symbol','Kind','Files'], data, function(d) {
|
|
543
|
+
return [d.name, '<span class="pill">'+esc(d.kind)+'</span>', (d.locations||[]).map(function(l){return l.file;}).join(', ')];
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
if (cmd === 'similar') {
|
|
547
|
+
if (!data.length) return '<div style="color:var(--green)">No similar groups 🎉</div>';
|
|
548
|
+
return data.map(function(g) {
|
|
549
|
+
var members = (g.members||[]).map(function(m) { return '<span style="font-family:monospace;font-size:11px">'+esc(m.name)+' <span style="color:var(--muted)">('+esc(m.file)+')</span></span>'; }).join(', ');
|
|
550
|
+
return '<div class="card" style="margin-bottom:8px"><b>'+esc(g.kind||'similar')+'</b> — '+members+'</div>';
|
|
551
|
+
}).join('');
|
|
552
|
+
}
|
|
553
|
+
if (cmd === 'complexity') {
|
|
554
|
+
return renderTable(['File','Functions','Max CC','Avg CC','Rating'], data, function(d) {
|
|
555
|
+
var r = d.rating||'';
|
|
556
|
+
var cls = (r==='high'||r==='very-high')?'badge-red':r==='moderate'?'badge-yellow':'badge-green';
|
|
557
|
+
return [d.file, (d.functions||[]).length, d.maxComplexity||0, Math.round(d.avgComplexity||0), '<span class="badge '+cls+'">'+r+'</span>'];
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
if (cmd === 'top') {
|
|
561
|
+
return renderTable(['Symbol','Kind','File','Imports'], data, function(d) {
|
|
562
|
+
return [d.symbol||d.id, '<span class="pill">'+esc(d.kind||'')+'</span>', d.file||'', '<span class="badge badge-blue">'+(d.inDegree||d.importCount||0)+'</span>'];
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
if (cmd === 'smells') {
|
|
566
|
+
if (!data.length) return '<div style="color:var(--green)">No smells 🎉</div>';
|
|
567
|
+
return renderTable(['Smell','Symbol','File','Line','Sev'], data, function(d) {
|
|
568
|
+
return ['<span class="badge badge-yellow">'+esc(d.smell)+'</span>', d.symbol||'', d.file, d.line||'', d.severity];
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
if (cmd === 'security') {
|
|
572
|
+
if (!data.length) return '<div style="color:var(--green)">No issues 🎉</div>';
|
|
573
|
+
return renderTable(['Rule','Sev','File','Line','Message'], data, function(d) {
|
|
574
|
+
var cls = (d.severity==='critical'||d.severity==='high')?'badge-red':'badge-yellow';
|
|
575
|
+
return ['<span class="badge '+cls+'">'+esc(d.rule)+'</span>', d.severity, d.file, d.line, d.message];
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
if (cmd === 'arch') {
|
|
579
|
+
if (!data.length) return '<div style="color:var(--green)">No violations 🎉</div>';
|
|
580
|
+
return renderTable(['Rule','From','To','Severity'], data, function(d) {
|
|
581
|
+
return [d.rule||d.description||'', d.from||'', d.to||'', '<span class="badge '+(d.severity==='error'?'badge-red':'badge-yellow')+'">'+d.severity+'</span>'];
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
if (cmd === 'find') {
|
|
585
|
+
if (!data.length) return '<div style="color:var(--muted)">No symbols found</div>';
|
|
586
|
+
return renderTable(['Symbol','Kind','File','Line','Exported'], data, function(d) {
|
|
587
|
+
return [d.name, '<span class="pill">'+esc(d.kind)+'</span>', d.file, d.line||'', d.exported?'✓':''];
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
if (cmd === 'impact') {
|
|
591
|
+
if (!data) return '<div style="color:var(--muted)">Symbol not found in graph</div>';
|
|
592
|
+
var header = '<div class="card" style="margin-bottom:8px"><b>Direct:</b> '+(data.direct||[]).length+' <b>Transitive:</b> '+(data.transitive||[]).length+' <b>Total files:</b> '+data.totalFiles+'</div>';
|
|
593
|
+
var allNodes = (data.direct||[]).map(function(n){return Object.assign({},n,{rel:'direct'});}).concat((data.transitive||[]).map(function(n){return Object.assign({},n,{rel:'transitive'});}));
|
|
594
|
+
return header + renderTable(['File','Symbol','Relation'], allNodes, function(d) {
|
|
595
|
+
return [d.file, d.symbol||'', '<span class="badge '+(d.rel==='direct'?'badge-blue':'badge-yellow')+'">'+d.rel+'</span>'];
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
if (cmd === 'fileDeps') {
|
|
599
|
+
if (!data) return '<div style="color:var(--muted)">File not found in graph</div>';
|
|
600
|
+
var imH = (data.imports||[]).map(function(f) { return '<div style="font-family:monospace;font-size:12px;padding:2px 0">'+esc(f.file)+'</div>'; }).join('');
|
|
601
|
+
var ibH = (data.importedBy||[]).map(function(f) { return '<div style="font-family:monospace;font-size:12px;padding:2px 0">'+esc(f.file)+'</div>'; }).join('');
|
|
602
|
+
return '<div class="card" style="margin-bottom:8px"><b>Imports:</b> '+(data.imports||[]).length+' <b>Imported by:</b> '+(data.importedBy||[]).length+'</div>'
|
|
603
|
+
+ '<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">'
|
|
604
|
+
+ '<div><div class="stat-label" style="margin-bottom:6px">Imports</div>'+imH+'</div>'
|
|
605
|
+
+ '<div><div class="stat-label" style="margin-bottom:6px">Imported by</div>'+ibH+'</div></div>';
|
|
606
|
+
}
|
|
607
|
+
if (cmd === 'explain') {
|
|
608
|
+
if (!data) return '<div style="color:var(--muted)">Symbol not found</div>';
|
|
609
|
+
var detail = JSON.stringify({ signature: data.signature, params: data.params, returnType: data.returnType, exported: data.exported, calls: (data.calls||[]).length, calledBy: (data.calledBy||[]).length }, null, 2);
|
|
610
|
+
return '<div class="card" style="margin-bottom:8px"><b>'+esc(data.symbol||'')+'</b> <span class="pill">'+esc(data.kind||'')+'</span> in <code style="font-size:11px">'+esc(data.file||'')+'</code></div>'
|
|
611
|
+
+ (data.docstring ? '<div class="card" style="margin-bottom:8px;font-size:12px">'+esc(data.docstring)+'</div>' : '')
|
|
612
|
+
+ '<div class="result-pre">'+esc(detail)+'</div>';
|
|
613
|
+
}
|
|
614
|
+
if (cmd === 'diagram') {
|
|
615
|
+
return '<div class="card" style="margin-bottom:8px"><b>'+esc(data.title||'Diagram')+'</b> — '+data.nodeCount+' nodes, '+data.edgeCount+' edges</div>'
|
|
616
|
+
+ '<div class="result-pre">'+esc(data.mermaid||'')+'</div>';
|
|
617
|
+
}
|
|
618
|
+
if (cmd === 'doc') {
|
|
619
|
+
return '<div class="card" style="margin-bottom:8px"><b>Generated docs</b> — '+data.files+' files, '+data.symbols+' symbols</div>'
|
|
620
|
+
+ '<div class="result-pre">'+esc(data.markdown||'')+'</div>';
|
|
621
|
+
}
|
|
622
|
+
return '<div class="result-pre">'+esc(JSON.stringify(data, null, 2))+'</div>';
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function renderTable(headers, rows, mapper) {
|
|
626
|
+
if (!rows || !rows.length) return '<div style="color:var(--muted);font-size:13px;padding:12px">No results</div>';
|
|
627
|
+
var h = headers.map(function(x) { return '<th>'+x+'</th>'; }).join('');
|
|
628
|
+
var r = rows.slice(0, 200).map(function(d) {
|
|
629
|
+
return '<tr>' + mapper(d).map(function(c) { return '<td>'+(c==null?'':c)+'</td>'; }).join('') + '</tr>';
|
|
630
|
+
}).join('');
|
|
631
|
+
return '<div class="card"><table><thead><tr>'+h+'</tr></thead><tbody>'+r+'</tbody></table></div>';
|
|
632
|
+
}
|
|
633
|
+
|
|
329
634
|
// ─── Bootstrap ────────────────────────────────────────────────────────────────
|
|
330
635
|
loadAll();
|
|
331
636
|
|
package/package.json
CHANGED