@tyroneross/navgator 0.1.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 (84) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +486 -0
  4. package/agents/architecture-advisor.md +109 -0
  5. package/commands/nav-check.md +64 -0
  6. package/commands/nav-connections.md +58 -0
  7. package/commands/nav-diagram.md +106 -0
  8. package/commands/nav-export.md +71 -0
  9. package/commands/nav-impact.md +58 -0
  10. package/commands/nav-scan.md +46 -0
  11. package/commands/nav-status.md +44 -0
  12. package/dist/cli/index.d.ts +7 -0
  13. package/dist/cli/index.d.ts.map +1 -0
  14. package/dist/cli/index.js +627 -0
  15. package/dist/cli/index.js.map +1 -0
  16. package/dist/config.d.ts +95 -0
  17. package/dist/config.d.ts.map +1 -0
  18. package/dist/config.js +262 -0
  19. package/dist/config.js.map +1 -0
  20. package/dist/diagram.d.ts +36 -0
  21. package/dist/diagram.d.ts.map +1 -0
  22. package/dist/diagram.js +333 -0
  23. package/dist/diagram.js.map +1 -0
  24. package/dist/index.d.ts +16 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +18 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/scanner.d.ts +57 -0
  29. package/dist/scanner.d.ts.map +1 -0
  30. package/dist/scanner.js +282 -0
  31. package/dist/scanner.js.map +1 -0
  32. package/dist/scanners/connections/ast-scanner.d.ts +26 -0
  33. package/dist/scanners/connections/ast-scanner.d.ts.map +1 -0
  34. package/dist/scanners/connections/ast-scanner.js +430 -0
  35. package/dist/scanners/connections/ast-scanner.js.map +1 -0
  36. package/dist/scanners/connections/service-calls.d.ts +14 -0
  37. package/dist/scanners/connections/service-calls.d.ts.map +1 -0
  38. package/dist/scanners/connections/service-calls.js +719 -0
  39. package/dist/scanners/connections/service-calls.js.map +1 -0
  40. package/dist/scanners/infrastructure/index.d.ts +27 -0
  41. package/dist/scanners/infrastructure/index.d.ts.map +1 -0
  42. package/dist/scanners/infrastructure/index.js +233 -0
  43. package/dist/scanners/infrastructure/index.js.map +1 -0
  44. package/dist/scanners/packages/npm.d.ts +18 -0
  45. package/dist/scanners/packages/npm.d.ts.map +1 -0
  46. package/dist/scanners/packages/npm.js +256 -0
  47. package/dist/scanners/packages/npm.js.map +1 -0
  48. package/dist/scanners/packages/pip.d.ts +14 -0
  49. package/dist/scanners/packages/pip.d.ts.map +1 -0
  50. package/dist/scanners/packages/pip.js +228 -0
  51. package/dist/scanners/packages/pip.js.map +1 -0
  52. package/dist/scanners/prompts/detector.d.ts +119 -0
  53. package/dist/scanners/prompts/detector.d.ts.map +1 -0
  54. package/dist/scanners/prompts/detector.js +617 -0
  55. package/dist/scanners/prompts/detector.js.map +1 -0
  56. package/dist/scanners/prompts/index.d.ts +51 -0
  57. package/dist/scanners/prompts/index.d.ts.map +1 -0
  58. package/dist/scanners/prompts/index.js +340 -0
  59. package/dist/scanners/prompts/index.js.map +1 -0
  60. package/dist/scanners/prompts/types.d.ts +127 -0
  61. package/dist/scanners/prompts/types.d.ts.map +1 -0
  62. package/dist/scanners/prompts/types.js +37 -0
  63. package/dist/scanners/prompts/types.js.map +1 -0
  64. package/dist/setup.d.ts +65 -0
  65. package/dist/setup.d.ts.map +1 -0
  66. package/dist/setup.js +261 -0
  67. package/dist/setup.js.map +1 -0
  68. package/dist/storage.d.ts +147 -0
  69. package/dist/storage.d.ts.map +1 -0
  70. package/dist/storage.js +931 -0
  71. package/dist/storage.js.map +1 -0
  72. package/dist/types.d.ts +296 -0
  73. package/dist/types.d.ts.map +1 -0
  74. package/dist/types.js +55 -0
  75. package/dist/types.js.map +1 -0
  76. package/dist/ui-server.d.ts +17 -0
  77. package/dist/ui-server.d.ts.map +1 -0
  78. package/dist/ui-server.js +815 -0
  79. package/dist/ui-server.js.map +1 -0
  80. package/hooks/hooks.json +57 -0
  81. package/package.json +80 -0
  82. package/scripts/ibr-ui-test.mjs +359 -0
  83. package/scripts/postinstall.cjs +35 -0
  84. package/skills/architecture-awareness/SKILL.md +141 -0
@@ -0,0 +1,815 @@
1
+ /**
2
+ * NavGator UI Server
3
+ * Built-in dashboard server for viewing architecture data
4
+ * All data comes from real scans - no mock data
5
+ */
6
+ import * as http from 'http';
7
+ import { loadIndex, loadAllComponents, loadAllConnections, loadGraph } from './storage.js';
8
+ import { getConfig } from './config.js';
9
+ const DEFAULT_PORT = 3333;
10
+ /**
11
+ * Start the UI server
12
+ */
13
+ export async function startUIServer(options) {
14
+ const port = options.port || DEFAULT_PORT;
15
+ const projectPath = options.projectPath || process.cwd();
16
+ const config = getConfig();
17
+ const server = http.createServer(async (req, res) => {
18
+ const url = new URL(req.url || '/', `http://localhost:${port}`);
19
+ // CORS headers
20
+ res.setHeader('Access-Control-Allow-Origin', '*');
21
+ res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
22
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
23
+ if (req.method === 'OPTIONS') {
24
+ res.writeHead(200);
25
+ res.end();
26
+ return;
27
+ }
28
+ try {
29
+ // API endpoints - all return REAL data from scans
30
+ if (url.pathname === '/api/status') {
31
+ const index = await loadIndex(config, projectPath);
32
+ res.writeHead(200, { 'Content-Type': 'application/json' });
33
+ res.end(JSON.stringify(index || { error: 'No scan data. Run: navgator setup' }));
34
+ return;
35
+ }
36
+ if (url.pathname === '/api/components') {
37
+ const components = await loadAllComponents(config, projectPath);
38
+ res.writeHead(200, { 'Content-Type': 'application/json' });
39
+ res.end(JSON.stringify(components));
40
+ return;
41
+ }
42
+ if (url.pathname === '/api/connections') {
43
+ const connections = await loadAllConnections(config, projectPath);
44
+ res.writeHead(200, { 'Content-Type': 'application/json' });
45
+ res.end(JSON.stringify(connections));
46
+ return;
47
+ }
48
+ if (url.pathname === '/api/graph') {
49
+ const graph = await loadGraph(config, projectPath);
50
+ res.writeHead(200, { 'Content-Type': 'application/json' });
51
+ res.end(JSON.stringify(graph || { nodes: [], edges: [] }));
52
+ return;
53
+ }
54
+ // Serve dashboard HTML
55
+ if (url.pathname === '/' || url.pathname === '/index.html') {
56
+ res.writeHead(200, { 'Content-Type': 'text/html' });
57
+ res.end(generateDashboardHTML(projectPath));
58
+ return;
59
+ }
60
+ // 404
61
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
62
+ res.end('Not Found');
63
+ }
64
+ catch (error) {
65
+ res.writeHead(500, { 'Content-Type': 'application/json' });
66
+ res.end(JSON.stringify({ error: String(error) }));
67
+ }
68
+ });
69
+ return new Promise((resolve) => {
70
+ server.listen(port, () => {
71
+ resolve({
72
+ port,
73
+ close: () => server.close(),
74
+ });
75
+ });
76
+ });
77
+ }
78
+ /**
79
+ * Generate the dashboard HTML - fully functional, real data only
80
+ */
81
+ function generateDashboardHTML(projectPath) {
82
+ return `<!DOCTYPE html>
83
+ <html lang="en">
84
+ <head>
85
+ <meta charset="UTF-8">
86
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
87
+ <title>NavGator Dashboard</title>
88
+ <style>
89
+ * { box-sizing: border-box; margin: 0; padding: 0; }
90
+ body {
91
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
92
+ background: #0a0a0a;
93
+ color: #e5e5e5;
94
+ min-height: 100vh;
95
+ }
96
+ .header {
97
+ background: #171717;
98
+ border-bottom: 1px solid #262626;
99
+ padding: 12px 24px;
100
+ display: flex;
101
+ align-items: center;
102
+ gap: 12px;
103
+ position: sticky;
104
+ top: 0;
105
+ z-index: 100;
106
+ }
107
+ .header h1 {
108
+ font-size: 18px;
109
+ font-weight: 600;
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 8px;
113
+ }
114
+ .nav {
115
+ display: flex;
116
+ gap: 4px;
117
+ margin-left: 32px;
118
+ }
119
+ /* IBR-style buttons: min 44px touch targets, clear hierarchy */
120
+ .nav-btn {
121
+ background: transparent;
122
+ border: none;
123
+ color: #737373;
124
+ padding: 8px 16px;
125
+ border-radius: 6px;
126
+ cursor: pointer;
127
+ font-size: 14px;
128
+ font-weight: 500;
129
+ transition: all 0.15s;
130
+ min-height: 36px;
131
+ min-width: 44px;
132
+ }
133
+ .nav-btn:hover { color: #e5e5e5; background: #262626; }
134
+ .nav-btn.active { color: #22c55e; background: transparent; border-bottom: 2px solid #22c55e; border-radius: 0; }
135
+ .header-right {
136
+ margin-left: auto;
137
+ display: flex;
138
+ align-items: center;
139
+ gap: 12px;
140
+ }
141
+ .search-box {
142
+ background: #262626;
143
+ border: 1px solid #404040;
144
+ color: #e5e5e5;
145
+ padding: 8px 12px;
146
+ border-radius: 6px;
147
+ font-size: 14px;
148
+ width: 240px;
149
+ }
150
+ .search-box:focus { outline: none; border-color: #22c55e; }
151
+ .refresh-btn {
152
+ background: #262626;
153
+ border: 1px solid #404040;
154
+ color: #e5e5e5;
155
+ padding: 8px 16px;
156
+ border-radius: 6px;
157
+ cursor: pointer;
158
+ font-size: 14px;
159
+ }
160
+ .refresh-btn:hover { background: #333; }
161
+ .container { max-width: 1400px; margin: 0 auto; padding: 24px; }
162
+ .grid {
163
+ display: grid;
164
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
165
+ gap: 16px;
166
+ margin-bottom: 24px;
167
+ }
168
+ .card {
169
+ background: #171717;
170
+ border: 1px solid #262626;
171
+ border-radius: 8px;
172
+ padding: 20px;
173
+ }
174
+ .card h2 {
175
+ font-size: 12px;
176
+ font-weight: 500;
177
+ color: #737373;
178
+ margin-bottom: 8px;
179
+ text-transform: uppercase;
180
+ letter-spacing: 0.5px;
181
+ }
182
+ .stat { font-size: 32px; font-weight: 700; color: #22c55e; }
183
+ .stat-label { font-size: 13px; color: #525252; margin-top: 4px; }
184
+ .list { list-style: none; }
185
+ .list li {
186
+ padding: 10px 0;
187
+ border-bottom: 1px solid #262626;
188
+ display: flex;
189
+ justify-content: space-between;
190
+ align-items: center;
191
+ cursor: pointer;
192
+ transition: background 0.1s;
193
+ margin: 0 -20px;
194
+ padding-left: 20px;
195
+ padding-right: 20px;
196
+ }
197
+ .list li:hover { background: #1f1f1f; }
198
+ .list li:last-child { border-bottom: none; }
199
+ /* IBR-style badges: text color only (Calm Precision) */
200
+ .badge {
201
+ font-size: 12px;
202
+ font-weight: 500;
203
+ padding: 2px 0;
204
+ }
205
+ .badge.npm { color: #a78bfa; }
206
+ .badge.service { color: #60a5fa; }
207
+ .badge.database { color: #4ade80; }
208
+ .badge.queue { color: #fbbf24; }
209
+ .badge.infra { color: #f472b6; }
210
+ .badge.prompt { color: #22d3ee; }
211
+ .badge.llm { color: #f472b6; font-weight: 600; }
212
+ .badge.framework { color: #818cf8; }
213
+ /* Count badges need subtle background for tap target */
214
+ .badge-count {
215
+ background: #262626;
216
+ padding: 3px 10px;
217
+ border-radius: 4px;
218
+ font-size: 12px;
219
+ color: #a3a3a3;
220
+ font-weight: 500;
221
+ }
222
+ .section { margin-bottom: 24px; }
223
+ .section-header {
224
+ display: flex;
225
+ align-items: center;
226
+ justify-content: space-between;
227
+ margin-bottom: 16px;
228
+ }
229
+ .section-title { font-size: 16px; font-weight: 600; }
230
+ .filter-group { display: flex; gap: 8px; }
231
+ .filter-btn {
232
+ background: #262626;
233
+ border: 1px solid #333;
234
+ color: #a3a3a3;
235
+ padding: 6px 12px;
236
+ border-radius: 4px;
237
+ cursor: pointer;
238
+ font-size: 12px;
239
+ }
240
+ .filter-btn:hover, .filter-btn.active { background: #333; color: #e5e5e5; }
241
+ .table { width: 100%; border-collapse: collapse; }
242
+ .table th, .table td { text-align: left; padding: 12px 16px; border-bottom: 1px solid #262626; }
243
+ .table th { color: #525252; font-weight: 500; font-size: 11px; text-transform: uppercase; background: #0f0f0f; position: sticky; top: 0; }
244
+ .table tr { cursor: pointer; transition: background 0.1s; }
245
+ .table tr:hover td { background: #1a1a1a; }
246
+ .table-wrapper { max-height: 500px; overflow-y: auto; border-radius: 8px; border: 1px solid #262626; }
247
+ .loading, .empty { text-align: center; padding: 60px 20px; color: #525252; }
248
+ .empty h2 { color: #737373; margin-bottom: 12px; }
249
+ .empty code { background: #262626; padding: 4px 12px; border-radius: 4px; font-family: monospace; color: #22c55e; }
250
+ .file-path { font-family: 'SF Mono', Monaco, monospace; font-size: 12px; color: #a3a3a3; }
251
+ .code-snippet { font-family: 'SF Mono', Monaco, monospace; font-size: 11px; color: #525252; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
252
+ .detail-panel {
253
+ position: fixed;
254
+ top: 0;
255
+ right: 0;
256
+ width: 480px;
257
+ height: 100vh;
258
+ background: #171717;
259
+ border-left: 1px solid #262626;
260
+ transform: translateX(100%);
261
+ transition: transform 0.2s ease;
262
+ z-index: 200;
263
+ overflow-y: auto;
264
+ }
265
+ .detail-panel.open { transform: translateX(0); }
266
+ .detail-header {
267
+ padding: 20px;
268
+ border-bottom: 1px solid #262626;
269
+ display: flex;
270
+ align-items: center;
271
+ justify-content: space-between;
272
+ position: sticky;
273
+ top: 0;
274
+ background: #171717;
275
+ }
276
+ .detail-header h3 { font-size: 16px; font-weight: 600; }
277
+ .close-btn {
278
+ background: none;
279
+ border: none;
280
+ color: #737373;
281
+ font-size: 24px;
282
+ cursor: pointer;
283
+ padding: 4px 8px;
284
+ }
285
+ .close-btn:hover { color: #e5e5e5; }
286
+ .detail-content { padding: 20px; }
287
+ .detail-section { margin-bottom: 24px; }
288
+ .detail-section h4 { font-size: 12px; color: #525252; text-transform: uppercase; margin-bottom: 12px; }
289
+ .detail-item { padding: 12px; background: #0f0f0f; border-radius: 6px; margin-bottom: 8px; }
290
+ .detail-item .label { font-size: 11px; color: #525252; margin-bottom: 4px; }
291
+ .detail-item .value { font-size: 14px; color: #e5e5e5; }
292
+ .connection-item {
293
+ padding: 12px;
294
+ background: #0f0f0f;
295
+ border-radius: 6px;
296
+ margin-bottom: 8px;
297
+ border-left: 3px solid #22c55e;
298
+ }
299
+ .connection-item.incoming { border-left-color: #3b82f6; }
300
+ .connection-item.llm { border-left-color: #f472b6; }
301
+ .connection-item .file { font-family: monospace; font-size: 12px; color: #a3a3a3; }
302
+ .connection-item .type { font-size: 11px; color: #525252; margin-top: 4px; }
303
+ /* Code block styling for showing actual code */
304
+ .code-block {
305
+ background: #0a0a0a;
306
+ border: 1px solid #262626;
307
+ border-radius: 4px;
308
+ padding: 8px 12px;
309
+ margin-top: 8px;
310
+ font-family: 'SF Mono', Monaco, 'Fira Code', monospace;
311
+ font-size: 11px;
312
+ line-height: 1.5;
313
+ color: #a3a3a3;
314
+ overflow-x: auto;
315
+ white-space: pre-wrap;
316
+ word-break: break-word;
317
+ }
318
+ .code-block .line-num { color: #525252; margin-right: 12px; user-select: none; }
319
+ .code-block .keyword { color: #c678dd; }
320
+ .code-block .string { color: #98c379; }
321
+ .code-block .function { color: #61afef; }
322
+ .hidden { display: none; }
323
+ .project-path { font-size: 12px; color: #525252; margin-left: 8px; font-family: monospace; }
324
+ @media (max-width: 768px) {
325
+ .nav { display: none; }
326
+ .search-box { width: 160px; }
327
+ .detail-panel { width: 100%; }
328
+ }
329
+ </style>
330
+ </head>
331
+ <body>
332
+ <div class="header">
333
+ <h1>🐊 NavGator</h1>
334
+ <span class="project-path">${projectPath}</span>
335
+ <nav class="nav">
336
+ <button class="nav-btn active" data-view="overview">Overview</button>
337
+ <button class="nav-btn" data-view="components">Components</button>
338
+ <button class="nav-btn" data-view="connections">Connections</button>
339
+ <button class="nav-btn" data-view="impact">Impact</button>
340
+ </nav>
341
+ <div class="header-right">
342
+ <input type="text" class="search-box" placeholder="Search components..." id="searchInput">
343
+ <button class="refresh-btn" onclick="loadData()">↻ Refresh</button>
344
+ </div>
345
+ </div>
346
+
347
+ <div class="container">
348
+ <div id="content"><div class="loading">Loading architecture data...</div></div>
349
+ </div>
350
+
351
+ <div class="detail-panel" id="detailPanel">
352
+ <div class="detail-header">
353
+ <h3 id="detailTitle">Component Details</h3>
354
+ <button class="close-btn" onclick="closeDetail()">×</button>
355
+ </div>
356
+ <div class="detail-content" id="detailContent"></div>
357
+ </div>
358
+
359
+ <script>
360
+ let components = [];
361
+ let connections = [];
362
+ let status = {};
363
+ let currentView = 'overview';
364
+ let selectedComponent = null;
365
+ let searchQuery = '';
366
+ let typeFilter = 'all';
367
+
368
+ // Navigation
369
+ document.querySelectorAll('.nav-btn').forEach(btn => {
370
+ btn.addEventListener('click', () => {
371
+ document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
372
+ btn.classList.add('active');
373
+ currentView = btn.dataset.view;
374
+ render();
375
+ });
376
+ });
377
+
378
+ // Search
379
+ document.getElementById('searchInput').addEventListener('input', (e) => {
380
+ searchQuery = e.target.value.toLowerCase();
381
+ render();
382
+ });
383
+
384
+ // Load data from API (REAL data only)
385
+ async function loadData() {
386
+ const content = document.getElementById('content');
387
+ content.innerHTML = '<div class="loading">Loading...</div>';
388
+
389
+ try {
390
+ const [statusRes, componentsRes, connectionsRes] = await Promise.all([
391
+ fetch('/api/status'),
392
+ fetch('/api/components'),
393
+ fetch('/api/connections'),
394
+ ]);
395
+
396
+ status = await statusRes.json();
397
+ components = await componentsRes.json();
398
+ connections = await connectionsRes.json();
399
+
400
+ if (status.error || components.length === 0) {
401
+ content.innerHTML = \`
402
+ <div class="empty">
403
+ <h2>No Architecture Data</h2>
404
+ <p>Run <code>navgator setup</code> to scan this project.</p>
405
+ </div>
406
+ \`;
407
+ return;
408
+ }
409
+
410
+ render();
411
+ } catch (error) {
412
+ content.innerHTML = \`<div class="empty"><h2>Error</h2><p>\${error.message}</p></div>\`;
413
+ }
414
+ }
415
+
416
+ // Render current view
417
+ function render() {
418
+ const content = document.getElementById('content');
419
+
420
+ switch(currentView) {
421
+ case 'overview': content.innerHTML = renderOverview(); break;
422
+ case 'components': content.innerHTML = renderComponents(); break;
423
+ case 'connections': content.innerHTML = renderConnections(); break;
424
+ case 'impact': content.innerHTML = renderImpact(); break;
425
+ }
426
+
427
+ // Attach event listeners
428
+ document.querySelectorAll('[data-component]').forEach(el => {
429
+ el.addEventListener('click', () => showComponentDetail(el.dataset.component));
430
+ });
431
+ document.querySelectorAll('.filter-btn').forEach(btn => {
432
+ btn.addEventListener('click', () => {
433
+ typeFilter = btn.dataset.type;
434
+ document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
435
+ btn.classList.add('active');
436
+ render();
437
+ });
438
+ });
439
+ }
440
+
441
+ // Overview view
442
+ function renderOverview() {
443
+ const byType = {};
444
+ components.forEach(c => { byType[c.type] = (byType[c.type] || 0) + 1; });
445
+
446
+ const connByType = {};
447
+ connections.forEach(c => { connByType[c.connection_type] = (connByType[c.connection_type] || 0) + 1; });
448
+
449
+ const lastScan = status.last_scan ? new Date(status.last_scan).toLocaleString() : 'Never';
450
+
451
+ // Sort types to show LLM prominently
452
+ const typeOrder = ['llm', 'database', 'queue', 'framework', 'infra', 'service', 'npm', 'prompt'];
453
+ const sortedTypes = Object.entries(byType).sort((a, b) => {
454
+ const aIdx = typeOrder.indexOf(a[0]);
455
+ const bIdx = typeOrder.indexOf(b[0]);
456
+ if (aIdx === -1 && bIdx === -1) return b[1] - a[1];
457
+ if (aIdx === -1) return 1;
458
+ if (bIdx === -1) return -1;
459
+ return aIdx - bIdx;
460
+ });
461
+
462
+ return \`
463
+ <div class="grid">
464
+ <div class="card">
465
+ <h2>Components</h2>
466
+ <div class="stat">\${components.length}</div>
467
+ <div class="stat-label">Tracked in this project</div>
468
+ </div>
469
+ <div class="card">
470
+ <h2>Connections</h2>
471
+ <div class="stat">\${connections.length}</div>
472
+ <div class="stat-label">Mapped relationships</div>
473
+ </div>
474
+ <div class="card">
475
+ <h2>Last Scan</h2>
476
+ <div class="stat" style="font-size: 16px; color: #e5e5e5;">\${lastScan}</div>
477
+ <div class="stat-label">Run navgator scan to refresh</div>
478
+ </div>
479
+ </div>
480
+ <div class="grid">
481
+ <div class="card">
482
+ <h2>Components by Type</h2>
483
+ <ul class="list">
484
+ \${sortedTypes.map(([type, count]) => \`
485
+ <li data-filter="\${type}" style="cursor: pointer;">
486
+ <span class="badge \${type}" style="font-size: 14px;">\${type === 'llm' ? '🤖 LLM/AI' : type}</span>
487
+ <span class="badge-count">\${count}</span>
488
+ </li>
489
+ \`).join('')}
490
+ </ul>
491
+ </div>
492
+ <div class="card">
493
+ <h2>Connections by Type</h2>
494
+ <ul class="list">
495
+ \${Object.entries(connByType).sort((a,b) => b[1] - a[1]).map(([type, count]) => \`
496
+ <li><span>\${type.replace(/-/g, ' ')}</span><span class="badge-count">\${count}</span></li>
497
+ \`).join('')}
498
+ </ul>
499
+ </div>
500
+ <div class="card">
501
+ <h2>Quick Actions</h2>
502
+ <ul class="list">
503
+ <li onclick="currentView='components';document.querySelector('[data-view=components]').click()">
504
+ <span>Browse all components</span><span>→</span>
505
+ </li>
506
+ <li onclick="currentView='connections';document.querySelector('[data-view=connections]').click()">
507
+ <span>View connections</span><span>→</span>
508
+ </li>
509
+ <li onclick="currentView='impact';document.querySelector('[data-view=impact]').click()">
510
+ <span>Impact analysis</span><span>→</span>
511
+ </li>
512
+ </ul>
513
+ </div>
514
+ </div>
515
+ \`;
516
+ }
517
+
518
+ // Components view
519
+ function renderComponents() {
520
+ const types = [...new Set(components.map(c => c.type))];
521
+ let filtered = components;
522
+
523
+ if (searchQuery) {
524
+ filtered = filtered.filter(c =>
525
+ c.name.toLowerCase().includes(searchQuery) ||
526
+ c.type.toLowerCase().includes(searchQuery) ||
527
+ (c.role?.purpose || '').toLowerCase().includes(searchQuery)
528
+ );
529
+ }
530
+ if (typeFilter !== 'all') {
531
+ filtered = filtered.filter(c => c.type === typeFilter);
532
+ }
533
+
534
+ return \`
535
+ <div class="section">
536
+ <div class="section-header">
537
+ <div class="section-title">All Components (\${filtered.length})</div>
538
+ <div class="filter-group">
539
+ <button class="filter-btn \${typeFilter === 'all' ? 'active' : ''}" data-type="all">All</button>
540
+ \${types.map(t => \`<button class="filter-btn \${typeFilter === t ? 'active' : ''}" data-type="\${t}">\${t}</button>\`).join('')}
541
+ </div>
542
+ </div>
543
+ <div class="table-wrapper">
544
+ <table class="table">
545
+ <thead>
546
+ <tr>
547
+ <th>Name</th>
548
+ <th>Type</th>
549
+ <th>Layer</th>
550
+ <th>Version</th>
551
+ <th>Purpose</th>
552
+ <th>Connections</th>
553
+ </tr>
554
+ </thead>
555
+ <tbody>
556
+ \${filtered.map(c => {
557
+ const connCount = connections.filter(conn =>
558
+ conn.to?.component_id === c.component_id ||
559
+ conn.from?.component_id === c.component_id
560
+ ).length;
561
+ return \`
562
+ <tr data-component="\${c.component_id}">
563
+ <td><strong>\${c.name}</strong></td>
564
+ <td><span class="badge \${c.type}">\${c.type === 'llm' ? '🤖 LLM' : c.type}</span></td>
565
+ <td>\${c.role?.layer || '-'}</td>
566
+ <td>\${c.version || '-'}</td>
567
+ <td style="color: #737373; font-size: 13px;">\${c.role?.purpose || '-'}</td>
568
+ <td><span class="badge-count">\${connCount}</span></td>
569
+ </tr>
570
+ \`;
571
+ }).join('')}
572
+ </tbody>
573
+ </table>
574
+ </div>
575
+ </div>
576
+ \`;
577
+ }
578
+
579
+ // Connections view
580
+ function renderConnections() {
581
+ let filtered = connections;
582
+
583
+ if (searchQuery) {
584
+ filtered = filtered.filter(c =>
585
+ (c.code_reference?.file || '').toLowerCase().includes(searchQuery) ||
586
+ (c.to?.component_id || '').toLowerCase().includes(searchQuery) ||
587
+ (c.connection_type || '').toLowerCase().includes(searchQuery)
588
+ );
589
+ }
590
+
591
+ return \`
592
+ <div class="section">
593
+ <div class="section-header">
594
+ <div class="section-title">All Connections (\${filtered.length})</div>
595
+ </div>
596
+ <div class="table-wrapper">
597
+ <table class="table">
598
+ <thead>
599
+ <tr>
600
+ <th>File</th>
601
+ <th>Line</th>
602
+ <th>Type</th>
603
+ <th>Target</th>
604
+ <th>Code</th>
605
+ </tr>
606
+ </thead>
607
+ <tbody>
608
+ \${filtered.slice(0, 100).map(c => {
609
+ const target = components.find(comp => comp.component_id === c.to?.component_id);
610
+ return \`
611
+ <tr>
612
+ <td class="file-path">\${c.code_reference?.file || '-'}</td>
613
+ <td>\${c.code_reference?.line_start || '-'}</td>
614
+ <td><span class="badge">\${c.connection_type}</span></td>
615
+ <td>\${target?.name || c.to?.component_id?.replace('COMP_', '') || '-'}</td>
616
+ <td class="code-snippet">\${c.code_reference?.code_snippet || '-'}</td>
617
+ </tr>
618
+ \`;
619
+ }).join('')}
620
+ </tbody>
621
+ </table>
622
+ \${filtered.length > 100 ? '<p style="padding: 16px; color: #525252; text-align: center;">Showing 100 of ' + filtered.length + ' connections</p>' : ''}
623
+ </div>
624
+ </div>
625
+ \`;
626
+ }
627
+
628
+ // Impact view
629
+ function renderImpact() {
630
+ if (!selectedComponent) {
631
+ return \`
632
+ <div class="section">
633
+ <div class="section-header">
634
+ <div class="section-title">Impact Analysis</div>
635
+ </div>
636
+ <div class="card">
637
+ <p style="color: #737373; text-align: center; padding: 40px;">
638
+ Select a component to see what would be affected if you change it.
639
+ </p>
640
+ <div class="grid" style="margin-top: 20px;">
641
+ \${components.slice(0, 12).map(c => \`
642
+ <div class="detail-item" style="cursor: pointer;" onclick="selectForImpact('\${c.component_id}')">
643
+ <div class="value">\${c.name}</div>
644
+ <div class="label">\${c.type} · \${c.role?.layer || 'unknown'}</div>
645
+ </div>
646
+ \`).join('')}
647
+ </div>
648
+ </div>
649
+ </div>
650
+ \`;
651
+ }
652
+
653
+ const comp = components.find(c => c.component_id === selectedComponent);
654
+ const incoming = connections.filter(c => c.to?.component_id === selectedComponent);
655
+ const outgoing = connections.filter(c => c.from?.component_id === selectedComponent);
656
+
657
+ return \`
658
+ <div class="section">
659
+ <div class="section-header">
660
+ <div class="section-title">Impact Analysis: \${comp?.name || selectedComponent}</div>
661
+ <button class="filter-btn" onclick="selectedComponent=null;render()">← Back</button>
662
+ </div>
663
+
664
+ <div class="grid">
665
+ <div class="card">
666
+ <h2>Incoming (\${incoming.length})</h2>
667
+ <p style="color: #525252; font-size: 13px; margin-bottom: 16px;">These files USE this component</p>
668
+ \${incoming.length === 0 ? '<p style="color: #404040;">No incoming connections</p>' : ''}
669
+ \${incoming.slice(0, 20).map(c => \`
670
+ <div class="connection-item incoming">
671
+ <div class="file">\${c.code_reference?.file}:\${c.code_reference?.line_start}</div>
672
+ <div class="type">\${c.connection_type} · \${c.code_reference?.symbol || ''}</div>
673
+ </div>
674
+ \`).join('')}
675
+ \${incoming.length > 20 ? '<p style="color: #525252; margin-top: 12px;">+ ' + (incoming.length - 20) + ' more</p>' : ''}
676
+ </div>
677
+
678
+ <div class="card">
679
+ <h2>Outgoing (\${outgoing.length})</h2>
680
+ <p style="color: #525252; font-size: 13px; margin-bottom: 16px;">This component DEPENDS on these</p>
681
+ \${outgoing.length === 0 ? '<p style="color: #404040;">No outgoing connections</p>' : ''}
682
+ \${outgoing.slice(0, 20).map(c => {
683
+ const target = components.find(comp => comp.component_id === c.to?.component_id);
684
+ return \`
685
+ <div class="connection-item">
686
+ <div class="file">\${target?.name || c.to?.component_id}</div>
687
+ <div class="type">\${c.connection_type}</div>
688
+ </div>
689
+ \`;
690
+ }).join('')}
691
+ </div>
692
+ </div>
693
+
694
+ <div class="card" style="margin-top: 16px;">
695
+ <h2>Files That May Need Changes</h2>
696
+ <p style="color: #525252; font-size: 13px; margin-bottom: 16px;">If you modify \${comp?.name}, check these files:</p>
697
+ <div class="table-wrapper" style="max-height: 300px;">
698
+ <table class="table">
699
+ <thead><tr><th>File</th><th>Line</th><th>Function</th></tr></thead>
700
+ <tbody>
701
+ \${[...new Map(incoming.map(c => [c.code_reference?.file, c])).values()].map(c => \`
702
+ <tr>
703
+ <td class="file-path">\${c.code_reference?.file}</td>
704
+ <td>\${c.code_reference?.line_start}</td>
705
+ <td>\${c.code_reference?.symbol || '-'}</td>
706
+ </tr>
707
+ \`).join('')}
708
+ </tbody>
709
+ </table>
710
+ </div>
711
+ </div>
712
+ </div>
713
+ \`;
714
+ }
715
+
716
+ function selectForImpact(componentId) {
717
+ selectedComponent = componentId;
718
+ render();
719
+ }
720
+
721
+ // Show component detail panel with code snippets
722
+ function showComponentDetail(componentId) {
723
+ const comp = components.find(c => c.component_id === componentId);
724
+ if (!comp) return;
725
+
726
+ const incoming = connections.filter(c => c.to?.component_id === componentId);
727
+ const outgoing = connections.filter(c => c.from?.component_id === componentId);
728
+
729
+ // Helper to escape HTML
730
+ const escapeHtml = (str) => str?.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') || '';
731
+
732
+ // Helper to highlight code syntax
733
+ const highlightCode = (code) => {
734
+ if (!code) return '';
735
+ return escapeHtml(code)
736
+ .replace(/(const|let|var|function|async|await|import|from|export|new|return)/g, '<span class="keyword">$1</span>')
737
+ .replace(/(['"\`][^'"\`]*['"\`])/g, '<span class="string">$1</span>');
738
+ };
739
+
740
+ document.getElementById('detailTitle').textContent = comp.name;
741
+ document.getElementById('detailContent').innerHTML = \`
742
+ <div class="detail-section">
743
+ <h4>Details</h4>
744
+ <div class="detail-item">
745
+ <div class="label">Type</div>
746
+ <div class="value"><span class="badge \${comp.type}">\${comp.type === 'llm' ? '🤖 LLM/AI' : comp.type}</span></div>
747
+ </div>
748
+ <div class="detail-item">
749
+ <div class="label">Layer</div>
750
+ <div class="value">\${comp.role?.layer || '-'}</div>
751
+ </div>
752
+ \${comp.version ? \`<div class="detail-item"><div class="label">Version</div><div class="value">\${comp.version}</div></div>\` : ''}
753
+ \${comp.role?.purpose ? \`<div class="detail-item"><div class="label">Purpose</div><div class="value">\${comp.role.purpose}</div></div>\` : ''}
754
+ </div>
755
+
756
+ <div class="detail-section">
757
+ <h4>Usage Locations (\${incoming.length})</h4>
758
+ <p style="color: #525252; font-size: 12px; margin-bottom: 12px;">Code that uses this component:</p>
759
+ \${incoming.length === 0 ? '<p style="color: #404040;">No usages detected</p>' : ''}
760
+ \${incoming.slice(0, 8).map(c => \`
761
+ <div class="connection-item incoming \${comp.type}">
762
+ <div class="file">\${c.code_reference?.file}:\${c.code_reference?.line_start}</div>
763
+ <div class="type">\${c.connection_type} · \${c.code_reference?.symbol || 'unknown'}</div>
764
+ \${c.code_reference?.code_snippet ? \`
765
+ <div class="code-block">
766
+ <span class="line-num">\${c.code_reference.line_start}</span>\${highlightCode(c.code_reference.code_snippet)}
767
+ </div>
768
+ \` : ''}
769
+ </div>
770
+ \`).join('')}
771
+ \${incoming.length > 8 ? '<p style="color: #525252; margin-top: 12px;">+ ' + (incoming.length - 8) + ' more locations</p>' : ''}
772
+ </div>
773
+
774
+ <div class="detail-section">
775
+ <h4>Dependencies (\${outgoing.length})</h4>
776
+ <p style="color: #525252; font-size: 12px; margin-bottom: 12px;">What this component depends on:</p>
777
+ \${outgoing.length === 0 ? '<p style="color: #404040;">No dependencies detected</p>' : ''}
778
+ \${outgoing.slice(0, 8).map(c => {
779
+ const target = components.find(comp => comp.component_id === c.to?.component_id);
780
+ return \`
781
+ <div class="connection-item">
782
+ <div class="file">\${target?.name || c.to?.component_id}</div>
783
+ <div class="type">\${c.connection_type}</div>
784
+ \${c.code_reference?.code_snippet ? \`
785
+ <div class="code-block">\${highlightCode(c.code_reference.code_snippet)}</div>
786
+ \` : ''}
787
+ </div>
788
+ \`;
789
+ }).join('')}
790
+ </div>
791
+
792
+ <button class="refresh-btn" style="width: 100%; margin-top: 16px; min-height: 44px;" onclick="selectedComponent='\${componentId}';currentView='impact';document.querySelector('[data-view=impact]').click();closeDetail();">
793
+ View Full Impact Analysis →
794
+ </button>
795
+ \`;
796
+
797
+ document.getElementById('detailPanel').classList.add('open');
798
+ }
799
+
800
+ function closeDetail() {
801
+ document.getElementById('detailPanel').classList.remove('open');
802
+ }
803
+
804
+ // Close panel on escape
805
+ document.addEventListener('keydown', (e) => {
806
+ if (e.key === 'Escape') closeDetail();
807
+ });
808
+
809
+ // Initial load
810
+ loadData();
811
+ </script>
812
+ </body>
813
+ </html>`;
814
+ }
815
+ //# sourceMappingURL=ui-server.js.map