projectify-cli 1.0.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.
@@ -0,0 +1,582 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateHtmlReport = generateHtmlReport;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ // Helper to escape JSON for HTML injection
10
+ const safeJSON = (data) => {
11
+ return JSON.stringify(data)
12
+ .replace(/</g, '\\u003c')
13
+ .replace(/>/g, '\\u003e')
14
+ .replace(/&/g, '\\u0026');
15
+ };
16
+ async function generateHtmlReport(projectPath, analysis, graph, outputPath, gitStats) {
17
+ const nodes = [];
18
+ const edges = [];
19
+ const topRisks = graph.getTopBlastRadius(10);
20
+ const topRiskIds = new Set(topRisks.map(n => n.id));
21
+ // Calculate node sizes and colors
22
+ graph.getNodes().forEach(node => {
23
+ let color = '#22d3ee'; // Cyan-400 (Default)
24
+ let size = 20;
25
+ let shape = 'dot';
26
+ let shadowColor = 'rgba(34, 211, 238, 0.4)';
27
+ if (topRiskIds.has(node.id)) {
28
+ color = '#f472b6'; // Pink-400 (High Risk)
29
+ shadowColor = 'rgba(244, 114, 182, 0.6)';
30
+ size = 35 + (node.blastRadius / 2);
31
+ shape = 'diamond';
32
+ }
33
+ else if (node.affectedFiles > 5) {
34
+ color = '#818cf8'; // Indigo-400 (Medium)
35
+ shadowColor = 'rgba(129, 140, 248, 0.5)';
36
+ size = 25;
37
+ }
38
+ nodes.push({
39
+ id: node.id,
40
+ label: path_1.default.basename(node.id),
41
+ title: undefined,
42
+ value: size,
43
+ color: {
44
+ background: color,
45
+ border: '#ffffff',
46
+ highlight: { background: '#ffffff', border: color }
47
+ },
48
+ shape: shape,
49
+ font: { color: '#a5f3fc', face: 'JetBrains Mono', strokeWidth: 0, size: 14 },
50
+ shadow: { enabled: true, color: shadowColor, size: 15, x: 0, y: 0 },
51
+ data: {
52
+ fullPath: node.id,
53
+ blastRadius: node.blastRadius.toFixed(2),
54
+ affectedFiles: node.affectedFiles,
55
+ inDegree: node.inDegree,
56
+ outDegree: node.outDegree
57
+ }
58
+ });
59
+ });
60
+ graph.getEdges().forEach((targets, source) => {
61
+ targets.forEach(target => {
62
+ edges.push({
63
+ from: source,
64
+ to: target,
65
+ arrows: 'to',
66
+ color: { color: '#1e293b', opacity: 0.2, highlight: '#38bdf8' }, // Slate-800
67
+ dashes: false,
68
+ width: 1
69
+ });
70
+ });
71
+ });
72
+ // Aggregate functions for the view
73
+ const functionsList = [];
74
+ Object.entries(analysis.files).forEach(([file, data]) => {
75
+ data.functions.forEach((fn) => {
76
+ // Handle both old (string) and new (FunctionInfo) formats safely
77
+ if (typeof fn === 'object') {
78
+ functionsList.push({
79
+ name: fn.name,
80
+ line: fn.line,
81
+ file: file,
82
+ params: fn.params || [],
83
+ doc: fn.doc || '',
84
+ code: fn.code || ''
85
+ });
86
+ }
87
+ else {
88
+ functionsList.push({
89
+ name: fn,
90
+ line: 0,
91
+ file: file,
92
+ params: [],
93
+ doc: '',
94
+ code: ''
95
+ });
96
+ }
97
+ });
98
+ });
99
+ const html = `
100
+ <!DOCTYPE html>
101
+ <html lang="en" class="dark">
102
+ <head>
103
+ <meta charset="UTF-8">
104
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
105
+ <title>Projetify</title>
106
+ <script src="https://cdn.tailwindcss.com"></script>
107
+ <script>
108
+ tailwind.config = {
109
+ darkMode: 'class',
110
+ theme: {
111
+ extend: {
112
+ fontFamily: {
113
+ sans: ['Inter', 'sans-serif'],
114
+ mono: ['JetBrains Mono', 'monospace'],
115
+ },
116
+ colors: {
117
+ background: '#000000',
118
+ surface: '#0a0a0a',
119
+ primary: '#22d3ee', // cyan-400
120
+ secondary: '#818cf8', // indigo-400
121
+ accent: '#f472b6', // pink-400
122
+ }
123
+ }
124
+ }
125
+ }
126
+ </script>
127
+ <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
128
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
129
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/prism.min.js"></script>
130
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/themes/prism-tomorrow.min.css" rel="stylesheet" />
131
+
132
+ <style>
133
+ body {
134
+ background-color: #000000;
135
+ color: #ecfeff;
136
+ overflow: hidden;
137
+ background-image: radial-gradient(circle at 50% 50%, #111827 0%, #000000 100%);
138
+ }
139
+
140
+ .glass {
141
+ background: rgba(10, 10, 10, 0.6);
142
+ backdrop-filter: blur(8px);
143
+ border: 1px solid rgba(255, 255, 255, 0.08);
144
+ }
145
+
146
+ .neon-text {
147
+ text-shadow: 0 0 10px rgba(34, 211, 238, 0.5);
148
+ }
149
+
150
+ .vis-network { outline: none; }
151
+
152
+ /* Custom Scrollbar */
153
+ ::-webkit-scrollbar { width: 4px; }
154
+ ::-webkit-scrollbar-track { background: transparent; }
155
+ ::-webkit-scrollbar-thumb { background: #334155; border-radius: 2px; }
156
+
157
+ /* Navigation Tabs */
158
+ .nav-tab {
159
+ position: relative;
160
+ color: #94a3b8;
161
+ transition: color 0.2s;
162
+ }
163
+ .nav-tab.active {
164
+ color: #fff;
165
+ font-weight: 600;
166
+ }
167
+ .nav-tab.active::after {
168
+ content: '';
169
+ position: absolute;
170
+ bottom: -18px;
171
+ left: 0;
172
+ right: 0;
173
+ height: 2px;
174
+ background: #a855f7; /* Purple */
175
+ box-shadow: 0 0 10px #a855f7;
176
+ }
177
+
178
+ /* Snap Button */
179
+ .snap-btn {
180
+ background: rgba(20, 20, 20, 0.8);
181
+ border: 1px solid rgba(255,255,255,0.1);
182
+ backdrop-filter: blur(4px);
183
+ color: #94a3b8;
184
+ border-radius: 16px;
185
+ transition: all 0.2s;
186
+ }
187
+ .snap-btn:hover {
188
+ color: #fff;
189
+ border-color: rgba(255,255,255,0.3);
190
+ box-shadow: 0 0 20px rgba(0,0,0,0.5);
191
+ }
192
+
193
+ /* Custom Tooltip */
194
+ #custom-tooltip {
195
+ pointer-events: none;
196
+ z-index: 100;
197
+ transition: opacity 0.1s;
198
+ }
199
+
200
+ /* Code Block Styling */
201
+ pre[class*="language-"] {
202
+ background: #0f172a !important;
203
+ border-radius: 8px;
204
+ border: 1px solid #1e293b;
205
+ margin: 0;
206
+ }
207
+ </style>
208
+ </head>
209
+ <body class="flex flex-col h-screen font-sans selection:bg-primary/30 selection:text-white">
210
+
211
+ <!-- TOP NAVIGATION -->
212
+ <nav class="h-16 border-b border-white/5 flex items-center justify-between px-6 bg-black z-50">
213
+ <!-- Logo & Breadcrumb -->
214
+ <div class="flex items-center gap-4">
215
+ <div class="flex items-center gap-2 text-primary font-mono tracking-wider font-bold text-lg neon-text">
216
+ <span>&gt;</span> Projectify
217
+ </div>
218
+ <div class="text-zinc-600 font-light text-sm tracking-widest uppercase flex items-center gap-2">
219
+ <span>//</span>
220
+ <span>KNOWLEDGE GRAPH</span>
221
+ </div>
222
+ <!-- Navigation Arrows (Static) -->
223
+ <div class="flex gap-1 ml-4 text-zinc-700">
224
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
225
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>
226
+ </div>
227
+ </div>
228
+
229
+ <!-- Center Tabs -->
230
+ <div class="hidden md:flex items-center gap-8 text-sm uppercase tracking-wider">
231
+ <button class="nav-tab hover:text-white" onclick="switchView('network')">GRAPH</button>
232
+ <button class="nav-tab active" onclick="switchView('functions')">FUNCTIONS</button>
233
+ <button class="nav-tab hover:text-white" onclick="switchView('files')">FILES</button>
234
+ </div>
235
+
236
+ <!-- Right Search -->
237
+ <div class="relative group w-64">
238
+ <input type="text" id="search-input"
239
+ class="w-full bg-zinc-900/50 border border-zinc-800 rounded-full px-4 py-1.5 text-xs text-zinc-300 focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/50 transition-all font-mono"
240
+ placeholder="SEARCH NODE..."
241
+ onkeydown="if(event.key === 'Enter') searchNodes()">
242
+ <div class="absolute right-2 top-1.5 text-zinc-600">
243
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
244
+ </div>
245
+ </div>
246
+
247
+ <div class="ml-4 p-2 rounded border border-zinc-800 text-zinc-500 hover:text-white hover:border-zinc-600 transition-colors cursor-pointer">
248
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
249
+ </div>
250
+ </nav>
251
+
252
+ <!-- CONTENT AREA -->
253
+ <div class="flex-1 relative overflow-hidden">
254
+
255
+ <!-- GRAPH CONTAINER -->
256
+ <div id="network-view" class="view-panel absolute inset-0 z-0 cursor-crosshair"></div>
257
+
258
+ <!-- FUNCTIONS CONTAINER -->
259
+ <div id="functions-view" class="view-panel absolute inset-0 z-20 bg-black hidden flex flex-col p-8 overflow-y-auto">
260
+ <div class="max-w-6xl mx-auto w-full">
261
+ <h2 class="text-2xl font-mono text-primary mb-6 neon-text">Available Functions</h2>
262
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" id="functions-grid">
263
+ <!-- Functions injected here -->
264
+ </div>
265
+ </div>
266
+ </div>
267
+
268
+ <!-- FILES CONTAINER (Placeholder) -->
269
+ <div id="files-view" class="view-panel absolute inset-0 z-20 bg-black hidden flex items-center justify-center">
270
+ <div class="text-zinc-600 font-mono">FILES VIEW UNDER CONSTRUCTION</div>
271
+ </div>
272
+
273
+ <!-- CUSTOM TOOLTIP -->
274
+ <div id="custom-tooltip" class="fixed hidden p-4 bg-black/90 border border-primary/50 text-xs font-mono rounded shadow-[0_0_20px_rgba(34,211,238,0.2)]">
275
+ <div id="tooltip-title" class="text-primary font-bold text-sm mb-2"></div>
276
+ <div class="space-y-1 text-zinc-400">
277
+ <div class="flex gap-2"><span class="w-16 text-zinc-600">PATH:</span> <span id="tooltip-path" class="text-white"></span></div>
278
+ <div class="flex gap-2"><span class="w-16 text-zinc-600">FILE:</span> <span id="tooltip-file" class="text-white"></span></div>
279
+ <div class="flex gap-2"><span class="w-16 text-zinc-600">LINE:</span> <span id="tooltip-line" class="text-accent"></span></div>
280
+ </div>
281
+ </div>
282
+
283
+ <!-- DETAILS SIDEBAR (Overlay, conditionally hidden) -->
284
+ <div id="details-panel" class="absolute top-0 right-0 h-full w-[500px] bg-black/95 backdrop-blur-xl border-l border-white/10 transform translate-x-full transition-transform duration-300 z-30 flex flex-col shadow-2xl">
285
+ <div class="p-6 border-b border-white/10 flex justify-between items-center">
286
+ <div>
287
+ <div class="text-[10px] text-primary tracking-widest uppercase mb-1" id="detail-type">Node Details</div>
288
+ <h2 id="detail-title" class="text-lg font-mono font-bold text-white break-all"></h2>
289
+ </div>
290
+ <button onclick="closeDetails()" class="text-zinc-500 hover:text-white">
291
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
292
+ </button>
293
+ </div>
294
+
295
+ <div class="p-6 space-y-8 flex-1 overflow-y-auto" id="detail-content">
296
+ <!-- Content injected dynamically -->
297
+ </div>
298
+ </div>
299
+
300
+ <!-- BOTTOM CONTROLS (Floating) -->
301
+ <div class="absolute bottom-8 left-1/2 transform -translate-x-1/2 z-10">
302
+ <button class="snap-btn py-4 px-6 flex flex-col items-center gap-1 group" onclick="resetView()">
303
+ <div class="w-6 h-6 border-2 border-current rounded mb-1 bg-gradient-to-tr from-transparent to-white/10"></div>
304
+ <span class="text-[10px] tracking-widest font-bold font-mono">SNAP</span>
305
+ </button>
306
+ </div>
307
+
308
+ </div>
309
+
310
+
311
+ <!-- DATA INJECTION -->
312
+ <script id="nodes-data" type="application/json">
313
+ ${safeJSON(nodes)}
314
+ </script>
315
+ <script id="edges-data" type="application/json">
316
+ ${safeJSON(edges)}
317
+ </script>
318
+ <script id="functions-data" type="application/json">
319
+ ${safeJSON(functionsList)}
320
+ </script>
321
+
322
+ <script>
323
+ // Safe data retrieval
324
+ const nodesData = JSON.parse(document.getElementById('nodes-data').textContent);
325
+ const edgesData = JSON.parse(document.getElementById('edges-data').textContent);
326
+ const functionsData = JSON.parse(document.getElementById('functions-data').textContent);
327
+
328
+ const nodes = new vis.DataSet(nodesData);
329
+ const edges = new vis.DataSet(edgesData);
330
+
331
+ const container = document.getElementById('network-view');
332
+ const data = { nodes: nodes, edges: edges };
333
+
334
+ const options = {
335
+ nodes: {
336
+ borderWidth: 0,
337
+ shadow: true,
338
+ font: { face: 'JetBrains Mono', size: 12, color: '#a5f3fc', strokeWidth: 0, vadjust: -30 },
339
+ title: undefined // Disable default title
340
+ },
341
+ edges: {
342
+ width: 1,
343
+ smooth: { type: 'continuous', roundness: 0.4 },
344
+ color: { inherit: false, color: 'rgba(30, 41, 59, 0.4)', highlight: '#38bdf8' },
345
+ arrows: { to: { enabled: true, scaleFactor: 0.5 } }
346
+ },
347
+ physics: {
348
+ forceAtlas2Based: {
349
+ gravitationalConstant: -50,
350
+ centralGravity: 0.005,
351
+ springLength: 200,
352
+ springConstant: 0.08
353
+ },
354
+ maxVelocity: 50,
355
+ solver: 'forceAtlas2Based',
356
+ timestep: 0.35,
357
+ stabilization: { enabled: true, iterations: 1000 }
358
+ },
359
+ interaction: {
360
+ hover: true,
361
+ tooltipDelay: 100,
362
+ hideEdgesOnDrag: true,
363
+ dragNodes: true,
364
+ zoomView: true
365
+ }
366
+ };
367
+
368
+ const network = new vis.Network(container, data, options);
369
+
370
+ // -- MOUSE INTERACTION FOR TOOLTIP --
371
+ network.on("hoverNode", function (params) {
372
+ const nodeId = params.node;
373
+ const node = nodes.get(nodeId);
374
+ const tooltip = document.getElementById('custom-tooltip');
375
+
376
+ // Populate tooltip
377
+ document.getElementById('tooltip-title').innerText = node.label.startsWith('GET') || node.label.startsWith('POST') ? node.label : 'FUNCTION / MODULE';
378
+ document.getElementById('tooltip-path').innerText = node.label;
379
+ document.getElementById('tooltip-file').innerText = node.id.split('/').pop(); // Simple filename
380
+ document.getElementById('tooltip-line').innerText = '--';
381
+
382
+ // Position tooltip
383
+ const canvasPos = network.canvasToDOM(network.getPositions([nodeId])[nodeId]);
384
+ tooltip.style.left = (canvasPos.x + 20) + 'px';
385
+ tooltip.style.top = (canvasPos.y - 20) + 'px';
386
+ tooltip.style.display = 'block';
387
+ });
388
+
389
+ network.on("blurNode", function (params) {
390
+ document.getElementById('custom-tooltip').style.display = 'none';
391
+ });
392
+
393
+ // -- CLICK INTERACTION --
394
+ network.on("click", function (params) {
395
+ if (params.nodes.length > 0) {
396
+ const nodeId = params.nodes[0];
397
+ const node = nodes.get(nodeId);
398
+ openNodeDetails(node);
399
+ } else {
400
+ closeDetails();
401
+ }
402
+ });
403
+
404
+ function openNodeDetails(node) {
405
+ const container = document.getElementById('detail-content');
406
+ document.getElementById('detail-type').innerText = "NODE DETAILS";
407
+ document.getElementById('detail-title').innerText = node.label;
408
+
409
+ container.innerHTML = \`
410
+ <div class="grid grid-cols-2 gap-4">
411
+ <div class="bg-zinc-900/50 p-4 rounded border border-zinc-800">
412
+ <div class="text-xs text-zinc-500 uppercase tracking-widest mb-1">Impact</div>
413
+ <div class="text-2xl font-mono text-accent">\${node.data.blastRadius}%</div>
414
+ </div>
415
+ <div class="bg-zinc-900/50 p-4 rounded border border-zinc-800">
416
+ <div class="text-xs text-zinc-500 uppercase tracking-widest mb-1">References</div>
417
+ <div class="text-2xl font-mono text-secondary">\${node.data.affectedFiles}</div>
418
+ </div>
419
+ </div>
420
+
421
+ <div>
422
+ <div class="text-xs text-zinc-500 uppercase tracking-widest mb-3 flex items-center gap-2">
423
+ <span class="w-1.5 h-1.5 bg-primary rounded-full animate-pulse"></span>
424
+ AI Analysis
425
+ </div>
426
+ <div class="text-sm text-zinc-400 leading-relaxed font-light">
427
+ High connectivity detected. This module acts as a central hub for data processing. Recommended to decouple dependencies to reduce blast radius.
428
+ </div>
429
+ </div>
430
+
431
+ <div>
432
+ <div class="text-xs text-zinc-500 uppercase tracking-widest mb-3">Location</div>
433
+ <div class="text-sm font-mono text-zinc-300 bg-zinc-900/50 p-2 rounded">\${node.id}</div>
434
+ </div>
435
+ \`;
436
+
437
+ document.getElementById('details-panel').classList.remove('translate-x-full');
438
+ document.getElementById('details-panel').classList.remove('hidden');
439
+ }
440
+
441
+ function openFunctionDetails(fn) {
442
+ const container = document.getElementById('detail-content');
443
+ document.getElementById('detail-type').innerText = "FUNCTION DETAILS";
444
+ document.getElementById('detail-title').innerText = fn.name;
445
+
446
+ const safeCode = fn.code.replace(/</g, '&lt;').replace(/>/g, '&gt;');
447
+ const safeDoc = fn.doc ? fn.doc : 'No documentation available.';
448
+ const paramsList = fn.params.length > 0 ? fn.params.join(', ') : 'None';
449
+
450
+ container.innerHTML = \`
451
+ <div class="space-y-6">
452
+ <!-- Metrics Row -->
453
+ <div class="flex gap-4 border-b border-zinc-800 pb-6">
454
+ <div class="flex-1">
455
+ <div class="text-[10px] text-zinc-500 uppercase tracking-widest mb-1">File</div>
456
+ <div class="text-xs font-mono text-zinc-300 break-all">\${fn.file.split('/').pop()}</div>
457
+ </div>
458
+ <div>
459
+ <div class="text-[10px] text-zinc-500 uppercase tracking-widest mb-1">Line</div>
460
+ <div class="text-xs font-mono text-accent">L\${fn.line}</div>
461
+ </div>
462
+ </div>
463
+
464
+ <!-- Params -->
465
+ <div>
466
+ <div class="text-[10px] text-zinc-500 uppercase tracking-widest mb-2">Parameters</div>
467
+ <div class="flex flex-wrap gap-2">
468
+ \${fn.params.map(p => \`<span class="px-2 py-1 bg-zinc-800 rounded text-xs font-mono text-primary">\${p}</span>\`).join('') || '<span class="text-zinc-600 text-xs italic">None</span>'}
469
+ </div>
470
+ </div>
471
+
472
+ <!-- Description / Doc -->
473
+ <div>
474
+ <div class="text-[10px] text-zinc-500 uppercase tracking-widest mb-2 flex items-center gap-2">
475
+ Description
476
+ </div>
477
+ <div class="text-sm text-zinc-400 font-light leading-relaxed bg-zinc-900/30 p-3 rounded border border-white/5 whitespace-pre-wrap">\${safeDoc}</div>
478
+ </div>
479
+
480
+ <!-- Source Toggle -->
481
+ <div>
482
+ <div class="text-[10px] text-zinc-500 uppercase tracking-widest mb-2">Source Preview</div>
483
+ <pre class="language-javascript text-xs max-h-[300px] overflow-auto"><code class="language-javascript">\${safeCode}</code></pre>
484
+ </div>
485
+ </div>
486
+ \`;
487
+
488
+ // Re-highlight prism
489
+ Prism.highlightAllUnder(container);
490
+
491
+ document.getElementById('details-panel').classList.remove('translate-x-full');
492
+ document.getElementById('details-panel').classList.remove('hidden');
493
+ }
494
+
495
+ function closeDetails() {
496
+ document.getElementById('details-panel').classList.add('translate-x-full');
497
+ network.unselectAll();
498
+ }
499
+
500
+ function resetView() {
501
+ network.fit({ animation: true });
502
+ }
503
+
504
+ function searchNodes() {
505
+ const query = document.getElementById('search-input').value.toLowerCase();
506
+ if (!query) return;
507
+
508
+ const allNodes = nodes.get();
509
+ const found = allNodes.find(n => n.label.toLowerCase().includes(query));
510
+
511
+ if (found) {
512
+ switchView('network'); // Ensure we are on graph view
513
+ network.focus(found.id, {
514
+ scale: 1.5,
515
+ animation: { duration: 1000, easingFunction: 'easeInOutQuad' }
516
+ });
517
+ network.selectNodes([found.id]);
518
+ openNodeDetails(found);
519
+ }
520
+ }
521
+
522
+ function switchView(viewName) {
523
+ // Update tabs
524
+ document.querySelectorAll('.nav-tab').forEach(el => el.classList.remove('active'));
525
+ // Find button by onclick text content
526
+ const btns = document.querySelectorAll('.nav-tab');
527
+ for(let btn of btns) {
528
+ if(btn.getAttribute('onclick').includes(viewName)) {
529
+ btn.classList.add('active');
530
+ break;
531
+ }
532
+ }
533
+
534
+ // Hide all views
535
+ document.querySelectorAll('.view-panel').forEach(el => el.classList.add('hidden'));
536
+
537
+ if (viewName === 'network') {
538
+ document.getElementById('network-view').classList.remove('hidden');
539
+ } else if (viewName === 'functions') {
540
+ document.getElementById('functions-view').classList.remove('hidden');
541
+ renderFunctions();
542
+ } else if (viewName === 'files') {
543
+ document.getElementById('files-view').classList.remove('hidden');
544
+ }
545
+ }
546
+
547
+ let functionsRendered = false;
548
+ function renderFunctions() {
549
+ if (functionsRendered) return;
550
+ const grid = document.getElementById('functions-grid');
551
+
552
+ // Limit to 200 functions to assume performance if huge repo
553
+ const displayData = functionsData; // .slice(0, 200);
554
+
555
+ displayData.forEach((fn, index) => {
556
+ const card = document.createElement('div');
557
+ card.className = 'bg-zinc-900/50 border border-zinc-800 p-4 rounded hover:border-primary/50 transition-colors group cursor-pointer flex flex-col justify-between h-[100px]';
558
+ card.onclick = () => openFunctionDetails(fn);
559
+
560
+ card.innerHTML = \`
561
+ <div class="flex justify-between items-start">
562
+ <span class="text-primary font-mono font-bold group-hover:text-white transition-colors truncate w-3/4">\${fn.name}</span>
563
+ <span class="text-xs text-zinc-500 font-mono">L\${fn.line}</span>
564
+ </div>
565
+ <div class="mt-2">
566
+ <div class="text-[10px] text-zinc-500 uppercase tracking-wider mb-1">File</div>
567
+ <div class="text-xs text-zinc-400 truncate">\${fn.file.split('/').slice(-2).join('/')}</div>
568
+ </div>
569
+ \`;
570
+ grid.appendChild(card);
571
+ });
572
+ functionsRendered = true;
573
+ }
574
+
575
+ // Initialize view
576
+ document.querySelector('.nav-tab').classList.add('active'); // Default to first (Graph)
577
+ </script>
578
+ </body>
579
+ </html>
580
+ `;
581
+ await fs_extra_1.default.writeFile(outputPath, html);
582
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.scanProject = scanProject;
7
+ const fast_glob_1 = __importDefault(require("fast-glob"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const fileUtils_1 = require("../utils/fileUtils");
10
+ async function scanProject(options) {
11
+ const rootPath = path_1.default.resolve(options.path);
12
+ const entries = await (0, fast_glob_1.default)('**/*', {
13
+ cwd: rootPath,
14
+ dot: false,
15
+ absolute: true,
16
+ ignore: [
17
+ '**/node_modules/**',
18
+ '**/dist/**',
19
+ '**/build/**',
20
+ '**/.git/**',
21
+ '**/.env*',
22
+ '**/__pycache__/**',
23
+ ...(options.ignore || [])
24
+ ],
25
+ onlyFiles: true
26
+ });
27
+ // Filter for text files only (or meaningful code files)
28
+ return entries.filter(fileUtils_1.isTextFile);
29
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readFileSafe = readFileSafe;
7
+ exports.isTextFile = isTextFile;
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const path_1 = __importDefault(require("path"));
10
+ async function readFileSafe(filePath) {
11
+ try {
12
+ return await fs_extra_1.default.readFile(filePath, 'utf-8');
13
+ }
14
+ catch (error) {
15
+ console.error(`Error reading file ${filePath}:`, error);
16
+ return null;
17
+ }
18
+ }
19
+ function isTextFile(filePath) {
20
+ // Simple heuristic, can be improved
21
+ const ext = path_1.default.extname(filePath).toLowerCase();
22
+ const binaryExts = ['.png', '.jpg', '.jpeg', '.gif', '.ico', '.pdf', '.exe', '.bin', '.pyc'];
23
+ return !binaryExts.includes(ext);
24
+ }