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 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
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+' &nbsp; <b>Transitive:</b> '+(data.transitive||[]).length+' &nbsp; <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+' &nbsp; <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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-ast-mapper",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "description": "MCP server that maps source files into a normalized code skeleton (JSON + HTML) using tree-sitter.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",