projectify-cli 2.0.3 → 2.0.5

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.
@@ -11,23 +11,57 @@ const python_1 = require("./parsers/python");
11
11
  async function analyzeFiles(filePaths) {
12
12
  const analysis = {
13
13
  fileCount: filePaths.length,
14
- files: {}
14
+ files: {},
15
+ dependencies: {}
15
16
  };
16
17
  for (const filePath of filePaths) {
17
18
  const content = await (0, fileUtils_1.readFileSafe)(filePath);
18
- if (!content)
19
- continue;
19
+ if (content === null)
20
+ continue; // Skip if read failed
20
21
  const ext = path_1.default.extname(filePath);
22
+ const basename = path_1.default.basename(filePath);
21
23
  let fileResult = { imports: [], exports: [], functions: [], classes: [] };
24
+ // Dependency Parsing
25
+ if (basename === 'package.json') {
26
+ try {
27
+ const pkg = JSON.parse(content);
28
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies };
29
+ Object.assign(analysis.dependencies, deps);
30
+ }
31
+ catch (e) {
32
+ console.warn(`⚠️ Failed to parse package.json: ${e}`);
33
+ }
34
+ }
35
+ else if (basename === 'requirements.txt') {
36
+ const lines = content.split('\n');
37
+ lines.forEach(line => {
38
+ const parts = line.split('==');
39
+ if (parts.length > 0 && parts[0].trim()) {
40
+ analysis.dependencies[parts[0].trim()] = parts[1]?.trim() || 'latest';
41
+ }
42
+ });
43
+ }
44
+ // Code Level Analysis
22
45
  if (['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
23
46
  fileResult = (0, javascript_1.parseJS)(content, filePath);
47
+ fileResult.language = 'javascript';
24
48
  }
25
49
  else if (['.py'].includes(ext)) {
26
50
  fileResult = (0, python_1.parsePython)(content, filePath);
51
+ fileResult.language = 'python';
52
+ }
53
+ else if (['.json'].includes(ext)) {
54
+ fileResult.language = 'json';
55
+ }
56
+ else if (['.md', '.txt'].includes(ext)) {
57
+ fileResult.language = 'markdown';
27
58
  }
28
59
  else {
29
- // TODO: Add generic regex parser
60
+ fileResult.language = 'text';
30
61
  }
62
+ // Store Content + Meta
63
+ fileResult.content = content;
64
+ fileResult.size = content.length;
31
65
  analysis.files[filePath] = fileResult;
32
66
  }
33
67
  return analysis;
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ const program = new commander_1.Command();
18
18
  program
19
19
  .name('projectify')
20
20
  .description('Projectify - Autonomous Code Analysis & Visualization')
21
- .version('2.0.3');
21
+ .version('2.0.5');
22
22
  program
23
23
  .argument('[path]', 'Project path to analyze', '.')
24
24
  .option('--no-ai', 'Skip AI analysis')
@@ -71,14 +71,22 @@ async function generateHtmlReport(projectPath, analysis, graph, outputPath, gitS
71
71
  });
72
72
  // Aggregate functions for the view
73
73
  const functionsList = [];
74
+ const relativeFiles = {};
75
+ const absoluteProjectPath = path_1.default.resolve(projectPath);
74
76
  Object.entries(analysis.files).forEach(([file, data]) => {
77
+ // Normalize path for frontend (relative + forward slashes)
78
+ let relPath = path_1.default.relative(absoluteProjectPath, file);
79
+ if (path_1.default.sep === '\\') {
80
+ relPath = relPath.replace(/\\/g, '/');
81
+ }
82
+ relativeFiles[relPath] = data;
75
83
  data.functions.forEach((fn) => {
76
84
  // Handle both old (string) and new (FunctionInfo) formats safely
77
85
  if (typeof fn === 'object') {
78
86
  functionsList.push({
79
87
  name: fn.name,
80
88
  line: fn.line,
81
- file: file,
89
+ file: relPath, // Use relative path here too
82
90
  params: fn.params || [],
83
91
  doc: fn.doc || '',
84
92
  code: fn.code || ''
@@ -88,7 +96,7 @@ async function generateHtmlReport(projectPath, analysis, graph, outputPath, gitS
88
96
  functionsList.push({
89
97
  name: fn,
90
98
  line: 0,
91
- file: file,
99
+ file: relPath,
92
100
  params: [],
93
101
  doc: '',
94
102
  code: ''
@@ -96,487 +104,46 @@ async function generateHtmlReport(projectPath, analysis, graph, outputPath, gitS
96
104
  }
97
105
  });
98
106
  });
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' }
107
+ // --- DEPENDENCY USAGE LOGIC ---
108
+ const depUsageData = {};
109
+ const dependencies = analysis.dependencies || {};
110
+ // Initialize usage arrays
111
+ Object.keys(dependencies).forEach(dep => depUsageData[dep] = []);
112
+ // Scan all files imports to map back to dependencies
113
+ Object.entries(relativeFiles).forEach(([relPath, data]) => {
114
+ if (data.imports && Array.isArray(data.imports)) {
115
+ data.imports.forEach((imp) => {
116
+ // Check against all dependencies (exact match or scoped/subpath)
117
+ // e.g. import 'react' matches dep 'react'
118
+ // e.g. import 'lodash/map' matches dep 'lodash'
119
+ const matchedDep = Object.keys(dependencies).find(depName => {
120
+ return imp === depName || imp.startsWith(depName + '/');
516
121
  });
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;
122
+ if (matchedDep) {
123
+ if (!depUsageData[matchedDep].includes(relPath)) {
124
+ depUsageData[matchedDep].push(relPath);
125
+ }
531
126
  }
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
127
  });
572
- functionsRendered = true;
573
128
  }
574
-
575
- // Initialize view
576
- document.querySelector('.nav-tab').classList.add('active'); // Default to first (Graph)
577
- </script>
578
- </body>
579
- </html>
580
- `;
129
+ });
130
+ // --- TEMPLATE LOADING ---
131
+ const templatePath = path_1.default.join(__dirname, 'template.html');
132
+ let html = '';
133
+ try {
134
+ html = await fs_extra_1.default.readFile(templatePath, 'utf-8');
135
+ }
136
+ catch (e) {
137
+ console.error('Error reading HTML template:', e);
138
+ html = '<h1>Error loading template. Ensure dist/report/template.html exists.</h1>';
139
+ }
140
+ // --- DATA INJECTION ---
141
+ const base64JSON = (data) => Buffer.from(JSON.stringify(data)).toString('base64');
142
+ html = html.replace('{{NODES_JSON}}', () => base64JSON(nodes));
143
+ html = html.replace('{{EDGES_JSON}}', () => base64JSON(edges));
144
+ html = html.replace('{{FUNCTIONS_JSON}}', () => base64JSON(functionsList));
145
+ html = html.replace('{{FILES_JSON}}', () => base64JSON(relativeFiles));
146
+ html = html.replace('{{DEPS_JSON}}', () => base64JSON(dependencies));
147
+ html = html.replace('{{DEP_USAGE_JSON}}', () => base64JSON(depUsageData));
581
148
  await fs_extra_1.default.writeFile(outputPath, html);
582
149
  }
@@ -0,0 +1,516 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="dark">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Projetify</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script>
10
+ tailwind.config = {
11
+ darkMode: 'class',
12
+ theme: {
13
+ extend: {
14
+ fontFamily: {
15
+ sans: ['Inter', 'sans-serif'],
16
+ mono: ['JetBrains Mono', 'monospace'],
17
+ },
18
+ colors: {
19
+ background: '#000000',
20
+ surface: '#0a0a0a',
21
+ primary: '#22d3ee', // cyan-400
22
+ secondary: '#818cf8', // indigo-400
23
+ accent: '#f472b6', // pink-400
24
+ }
25
+ }
26
+ }
27
+ }
28
+ </script>
29
+ <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
30
+ <link
31
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap"
32
+ rel="stylesheet">
33
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/prism.min.js"></script>
34
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/themes/prism-tomorrow.min.css" rel="stylesheet" />
35
+
36
+ <style>
37
+ body {
38
+ background-color: #000000;
39
+ color: #ecfeff;
40
+ overflow: hidden;
41
+ background-image: radial-gradient(circle at 50% 50%, #111827 0%, #000000 100%);
42
+ }
43
+
44
+ .glass {
45
+ background: rgba(10, 10, 10, 0.6);
46
+ backdrop-filter: blur(8px);
47
+ border: 1px solid rgba(255, 255, 255, 0.08);
48
+ }
49
+
50
+ .neon-text {
51
+ text-shadow: 0 0 10px rgba(34, 211, 238, 0.5);
52
+ }
53
+
54
+ .vis-network {
55
+ outline: none;
56
+ }
57
+
58
+ /* Custom Scrollbar */
59
+ ::-webkit-scrollbar {
60
+ width: 4px;
61
+ }
62
+
63
+ ::-webkit-scrollbar-track {
64
+ background: transparent;
65
+ }
66
+
67
+ ::-webkit-scrollbar-thumb {
68
+ background: #334155;
69
+ border-radius: 2px;
70
+ }
71
+
72
+ /* Navigation Tabs */
73
+ .nav-tab {
74
+ position: relative;
75
+ color: #94a3b8;
76
+ transition: color 0.2s;
77
+ }
78
+
79
+ .nav-tab.active {
80
+ color: #fff;
81
+ font-weight: 600;
82
+ }
83
+
84
+ .nav-tab.active::after {
85
+ content: '';
86
+ position: absolute;
87
+ bottom: -18px;
88
+ left: 0;
89
+ right: 0;
90
+ height: 2px;
91
+ background: #a855f7;
92
+ /* Purple */
93
+ box-shadow: 0 0 10px #a855f7;
94
+ }
95
+
96
+ /* Snap Button */
97
+ .snap-btn {
98
+ background: rgba(20, 20, 20, 0.8);
99
+ border: 1px solid rgba(255, 255, 255, 0.1);
100
+ backdrop-filter: blur(4px);
101
+ color: #94a3b8;
102
+ border-radius: 16px;
103
+ transition: all 0.2s;
104
+ }
105
+
106
+ .snap-btn:hover {
107
+ color: #fff;
108
+ border-color: rgba(255, 255, 255, 0.3);
109
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
110
+ }
111
+
112
+ /* Custom Tooltip */
113
+ #custom-tooltip {
114
+ pointer-events: none;
115
+ z-index: 100;
116
+ transition: opacity 0.1s;
117
+ }
118
+
119
+ /* Code Block Styling */
120
+ pre[class*="language-"] {
121
+ background: #0f172a !important;
122
+ border-radius: 8px;
123
+ border: 1px solid #1e293b;
124
+ margin: 0;
125
+ }
126
+ </style>
127
+ </head>
128
+
129
+ <body class="flex flex-col h-screen font-sans selection:bg-primary/30 selection:text-white">
130
+
131
+ <!-- TOP NAVIGATION -->
132
+ <nav class="h-16 border-b border-white/5 flex items-center justify-between px-6 bg-black z-50">
133
+ <!-- Logo & Breadcrumb -->
134
+ <div class="flex items-center gap-4">
135
+ <div class="flex items-center gap-2 text-primary font-mono tracking-wider font-bold text-lg neon-text">
136
+ <span>&gt;</span> Projectify
137
+ </div>
138
+ <div class="text-zinc-600 font-light text-sm tracking-widest uppercase flex items-center gap-2">
139
+ <span>//</span>
140
+ <span>KNOWLEDGE GRAPH</span>
141
+ </div>
142
+ <!-- Navigation Arrows (Static) -->
143
+ <div class="flex gap-1 ml-4 text-zinc-700">
144
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
145
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
146
+ d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
147
+ </svg>
148
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
149
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3">
150
+ </path>
151
+ </svg>
152
+ </div>
153
+ </div>
154
+
155
+ <!-- Center Tabs -->
156
+ <div class="hidden md:flex items-center gap-8 text-sm uppercase tracking-wider">
157
+ <button class="nav-tab hover:text-white" onclick="switchView('network')">GRAPH</button>
158
+ <button class="nav-tab active" onclick="switchView('functions')">FUNCTIONS</button>
159
+ <button class="nav-tab hover:text-white" onclick="switchView('files')">FILES</button>
160
+ <button class="nav-tab hover:text-white" onclick="switchView('dependencies')">DEPENDENCIES</button>
161
+ </div>
162
+
163
+ <!-- Right Search -->
164
+ <div class="relative group w-64">
165
+ <input type="text" id="search-input"
166
+ 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"
167
+ placeholder="SEARCH NODE..." onkeydown="if(event.key === 'Enter') searchNodes()">
168
+ <div class="absolute right-2 top-1.5 text-zinc-600">
169
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
170
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
171
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
172
+ </svg>
173
+ </div>
174
+ </div>
175
+
176
+ <div
177
+ class="ml-4 p-2 rounded border border-zinc-800 text-zinc-500 hover:text-white hover:border-zinc-600 transition-colors cursor-pointer">
178
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
179
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
180
+ 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">
181
+ </path>
182
+ </svg>
183
+ </div>
184
+ </nav>
185
+
186
+ <!-- CONTENT AREA -->
187
+ <div class="flex-1 relative overflow-hidden">
188
+
189
+ <!-- GRAPH CONTAINER -->
190
+ <div id="network-view" class="view-panel absolute inset-0 z-0 cursor-crosshair"></div>
191
+
192
+ <!-- FUNCTIONS CONTAINER -->
193
+ <div id="functions-view"
194
+ class="view-panel absolute inset-0 z-20 bg-black hidden flex flex-col p-8 overflow-y-auto">
195
+ <div class="max-w-6xl mx-auto w-full">
196
+ <h2 class="text-2xl font-mono text-primary mb-6 neon-text">Available Functions</h2>
197
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" id="functions-grid">
198
+ <!-- Functions injected here -->
199
+ </div>
200
+ </div>
201
+ </div>
202
+
203
+ <!-- FILES CONTAINER -->
204
+ <div id="files-view" class="view-panel absolute inset-0 z-20 bg-black hidden flex h-full">
205
+ <!-- Sidebar -->
206
+ <div class="w-1/4 h-full border-r border-zinc-800 bg-zinc-900/30 overflow-y-auto p-4 custom-scrollbar">
207
+ <h3 class="text-xs font-mono uppercase tracking-widest text-zinc-500 mb-4">Project Explorer</h3>
208
+ <div id="file-tree" class="font-mono text-sm space-y-1"></div>
209
+ </div>
210
+ <!-- Code Viewer -->
211
+ <div class="flex-1 h-full bg-zinc-950 overflow-auto p-6 relative">
212
+ <div id="file-placeholder"
213
+ class="absolute inset-0 flex items-center justify-center text-zinc-700 font-mono text-sm">
214
+ &lt; SELECT A FILE TO VIEW SOURCE &gt;
215
+ </div>
216
+ <div id="code-container" class="hidden">
217
+ <div class="flex justify-between items-center mb-4 border-b border-zinc-800 pb-2">
218
+ <span id="current-filename" class="text-primary font-mono font-bold"></span>
219
+ <span id="current-lang"
220
+ class="text-[10px] uppercase text-zinc-500 bg-zinc-900 px-2 py-1 rounded"></span>
221
+ </div>
222
+ <pre><code id="code-content" class="language-javascript"></code></pre>
223
+ </div>
224
+ </div>
225
+ </div>
226
+
227
+ <!-- DEPENDENCIES CONTAINER -->
228
+ <div id="dependencies-view"
229
+ class="view-panel absolute inset-0 z-20 bg-black hidden flex flex-col p-8 overflow-y-auto">
230
+ <div class="max-w-4xl mx-auto w-full">
231
+ <h2 class="text-2xl font-mono text-accent mb-6 neon-text">Dependencies & Libraries</h2>
232
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4" id="deps-grid">
233
+ <!-- Deps injected here -->
234
+ </div>
235
+ </div>
236
+ </div>
237
+
238
+ <!-- CUSTOM TOOLTIP -->
239
+ <div id="custom-tooltip"
240
+ 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)]">
241
+ <div id="tooltip-title" class="text-primary font-bold text-sm mb-2"></div>
242
+ <div class="space-y-1 text-zinc-400">
243
+ <div class="flex gap-2"><span class="w-16 text-zinc-600">PATH:</span> <span id="tooltip-path"
244
+ class="text-white"></span></div>
245
+ <div class="flex gap-2"><span class="w-16 text-zinc-600">FILE:</span> <span id="tooltip-file"
246
+ class="text-white"></span></div>
247
+ <div class="flex gap-2"><span class="w-16 text-zinc-600">LINE:</span> <span id="tooltip-line"
248
+ class="text-accent"></span></div>
249
+ </div>
250
+ </div>
251
+
252
+ <!-- DETAILS SIDEBAR (Overlay, conditionally hidden) -->
253
+ <div id="details-panel"
254
+ 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">
255
+ <div class="p-6 border-b border-white/10 flex justify-between items-center">
256
+ <div>
257
+ <div class="text-[10px] text-primary tracking-widest uppercase mb-1" id="detail-type">Node Details
258
+ </div>
259
+ <h2 id="detail-title" class="text-lg font-mono font-bold text-white break-all"></h2>
260
+ </div>
261
+ <button onclick="closeDetails()" class="text-zinc-500 hover:text-white">
262
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
263
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12">
264
+ </path>
265
+ </svg>
266
+ </button>
267
+ </div>
268
+
269
+ <div class="p-6 space-y-8 flex-1 overflow-y-auto" id="detail-content">
270
+ <!-- Content injected dynamically -->
271
+ </div>
272
+ </div>
273
+
274
+ <!-- BOTTOM CONTROLS (Floating) -->
275
+ <div class="absolute bottom-8 left-1/2 transform -translate-x-1/2 z-10">
276
+ <button class="snap-btn py-4 px-6 flex flex-col items-center gap-1 group" onclick="resetView()">
277
+ <div
278
+ class="w-6 h-6 border-2 border-current rounded mb-1 bg-gradient-to-tr from-transparent to-white/10">
279
+ </div>
280
+ <span class="text-[10px] tracking-widest font-bold font-mono">SNAP</span>
281
+ </button>
282
+ </div>
283
+
284
+ </div>
285
+
286
+
287
+ <!-- DATA INJECTION -->
288
+ <script id="nodes-data" type="application/json">
289
+ {{NODES_JSON}}
290
+ </script>
291
+ <script id="edges-data" type="application/json">
292
+ {{EDGES_JSON}}
293
+ </script>
294
+ <script id="functions-data" type="application/json">
295
+ {{FUNCTIONS_JSON}}
296
+ </script>
297
+ <script id="files-data" type="application/json">
298
+ {{FILES_JSON}}
299
+ </script>
300
+ <script id="deps-data" type="application/json">
301
+ {{DEPS_JSON}}
302
+ </script>
303
+ <script id="dep-usage-data" type="application/json">
304
+ {{DEP_USAGE_JSON}}
305
+ </script>
306
+
307
+ <script>
308
+ // Helper to safely decode Base64 JSON
309
+ const decodeData = (id) => {
310
+ try {
311
+ const raw = document.getElementById(id).textContent.trim();
312
+ return JSON.parse(atob(raw));
313
+ } catch (e) {
314
+ console.error(`Failed to decode data for ${id}`, e);
315
+ return {};
316
+ }
317
+ };
318
+
319
+ const nodesData = decodeData('nodes-data');
320
+ const edgesData = decodeData('edges-data');
321
+ const functionsData = decodeData('functions-data');
322
+ const filesData = decodeData('files-data');
323
+ const depsData = decodeData('deps-data');
324
+ const depUsageData = decodeData('dep-usage-data');
325
+
326
+ const nodes = new vis.DataSet(nodesData);
327
+ const edges = new vis.DataSet(edgesData);
328
+
329
+ const container = document.getElementById('network-view');
330
+ const data = { nodes: nodes, edges: edges };
331
+
332
+ const options = {
333
+ nodes: { borderWidth: 0, shadow: true, font: { face: 'JetBrains Mono', size: 12, color: '#a5f3fc', strokeWidth: 0, vadjust: -30 }, title: undefined },
334
+ edges: { width: 1, smooth: { type: 'continuous', roundness: 0.4 }, color: { inherit: false, color: 'rgba(30, 41, 59, 0.4)', highlight: '#38bdf8' }, arrows: { to: { enabled: true, scaleFactor: 0.5 } } },
335
+ physics: { forceAtlas2Based: { gravitationalConstant: -50, centralGravity: 0.005, springLength: 200, springConstant: 0.08 }, maxVelocity: 50, solver: 'forceAtlas2Based', timestep: 0.35, stabilization: { enabled: true, iterations: 1000 } },
336
+ interaction: { hover: true, tooltipDelay: 100, hideEdgesOnDrag: true, dragNodes: true, zoomView: true }
337
+ };
338
+
339
+ const network = new vis.Network(container, data, options);
340
+
341
+ // -- MOUSE INTERACTION --
342
+ network.on("hoverNode", function (params) {
343
+ const nodeId = params.node;
344
+ const node = nodes.get(nodeId);
345
+ const tooltip = document.getElementById('custom-tooltip');
346
+ document.getElementById('tooltip-title').innerText = node.label.startsWith('GET') || node.label.startsWith('POST') ? node.label : 'FUNCTION / MODULE';
347
+ document.getElementById('tooltip-path').innerText = node.label;
348
+ document.getElementById('tooltip-file').innerText = node.id.split('/').pop();
349
+ document.getElementById('tooltip-line').innerText = '--';
350
+ const canvasPos = network.canvasToDOM(network.getPositions([nodeId])[nodeId]);
351
+ tooltip.style.left = (canvasPos.x + 20) + 'px';
352
+ tooltip.style.top = (canvasPos.y - 20) + 'px';
353
+ tooltip.style.display = 'block';
354
+ });
355
+ network.on("blurNode", params => document.getElementById('custom-tooltip').style.display = 'none');
356
+ network.on("click", params => params.nodes.length > 0 ? openNodeDetails(nodes.get(params.nodes[0])) : closeDetails());
357
+
358
+ function openNodeDetails(node) {
359
+ const container = document.getElementById('detail-content');
360
+ document.getElementById('detail-type').innerText = "NODE DETAILS";
361
+ document.getElementById('detail-title').innerText = node.label;
362
+ container.innerHTML = `<div class="grid grid-cols-2 gap-4"><div class="bg-zinc-900/50 p-4 rounded border border-zinc-800"><div class="text-xs text-zinc-500 uppercase tracking-widest mb-1">Impact</div><div class="text-2xl font-mono text-accent">${node.data.blastRadius}%</div></div><div class="bg-zinc-900/50 p-4 rounded border border-zinc-800"><div class="text-xs text-zinc-500 uppercase tracking-widest mb-1">References</div><div class="text-2xl font-mono text-secondary">${node.data.affectedFiles}</div></div></div><div><div class="text-xs text-zinc-500 uppercase tracking-widest mb-3 flex items-center gap-2"><span class="w-1.5 h-1.5 bg-primary rounded-full animate-pulse"></span>AI Analysis</div><div class="text-sm text-zinc-400 leading-relaxed font-light">High connectivity detected. Recommended to decouple dependencies.</div></div><div><div class="text-xs text-zinc-500 uppercase tracking-widest mb-3">Location</div><div class="text-sm font-mono text-zinc-300 bg-zinc-900/50 p-2 rounded">${node.id}</div></div>`;
363
+ document.getElementById('details-panel').classList.remove('translate-x-full');
364
+ document.getElementById('details-panel').classList.remove('hidden');
365
+ }
366
+
367
+ function openFunctionDetails(fn) {
368
+ const container = document.getElementById('detail-content');
369
+ document.getElementById('detail-type').innerText = "FUNCTION DETAILS";
370
+ document.getElementById('detail-title').innerText = fn.name;
371
+ const safeCode = fn.code.replace(/</g, '&lt;').replace(/>/g, '&gt;');
372
+ const safeDoc = fn.doc || 'No documentation.';
373
+ container.innerHTML = `<div class="space-y-6"><div class="flex gap-4 border-b border-zinc-800 pb-6"><div class="flex-1"><div class="text-[10px] text-zinc-500 uppercase tracking-widest mb-1">File</div><div class="text-xs font-mono text-zinc-300 break-all">${fn.file.split('/').pop()}</div></div><div><div class="text-[10px] text-zinc-500 uppercase tracking-widest mb-1">Line</div><div class="text-xs font-mono text-accent">L${fn.line}</div></div></div><div><div class="text-[10px] text-zinc-500 uppercase tracking-widest mb-2">Parameters</div><div class="flex flex-wrap gap-2">${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>'}</div></div><div><div class="text-[10px] text-zinc-500 uppercase tracking-widest mb-2 flex items-center gap-2">Description</div><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></div><div><div class="text-[10px] text-zinc-500 uppercase tracking-widest mb-2">Source Preview</div><pre class="language-javascript text-xs max-h-[300px] overflow-auto"><code class="language-javascript">${safeCode}</code></pre></div></div>`;
374
+ Prism.highlightAllUnder(container);
375
+ document.getElementById('details-panel').classList.remove('translate-x-full');
376
+ document.getElementById('details-panel').classList.remove('hidden');
377
+ }
378
+
379
+ function closeDetails() { document.getElementById('details-panel').classList.add('translate-x-full'); network.unselectAll(); }
380
+ function resetView() { network.fit({ animation: true }); }
381
+ function searchNodes() {
382
+ const query = document.getElementById('search-input').value.toLowerCase();
383
+ if (!query) return;
384
+ const found = nodes.get().find(n => n.label.toLowerCase().includes(query));
385
+ if (found) { switchView('network'); network.focus(found.id, { scale: 1.5, animation: { duration: 1000 } }); network.selectNodes([found.id]); openNodeDetails(found); }
386
+ }
387
+
388
+ function switchView(viewName) {
389
+ document.querySelectorAll('.nav-tab').forEach(el => el.classList.remove('active'));
390
+ const btns = document.querySelectorAll('.nav-tab');
391
+ for (let btn of btns) { if (btn.getAttribute('onclick').includes(viewName)) { btn.classList.add('active'); break; } }
392
+ document.querySelectorAll('.view-panel').forEach(el => el.classList.add('hidden'));
393
+ if (viewName === 'network') document.getElementById('network-view').classList.remove('hidden');
394
+ else if (viewName === 'functions') { document.getElementById('functions-view').classList.remove('hidden'); renderFunctions(); }
395
+ else if (viewName === 'files') { document.getElementById('files-view').classList.remove('hidden'); renderFiles(); }
396
+ else if (viewName === 'dependencies') { document.getElementById('dependencies-view').classList.remove('hidden'); renderDependencies(); }
397
+ }
398
+
399
+ let functionsRendered = false;
400
+ function renderFunctions() {
401
+ if (functionsRendered) return;
402
+ const grid = document.getElementById('functions-grid');
403
+ functionsData.forEach(fn => {
404
+ const card = document.createElement('div');
405
+ 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]';
406
+ card.onclick = () => openFunctionDetails(fn);
407
+ card.innerHTML = `<div class="flex justify-between items-start"><span class="text-primary font-mono font-bold group-hover:text-white transition-colors truncate w-3/4">${fn.name}</span><span class="text-xs text-zinc-500 font-mono">L${fn.line}</span></div><div class="mt-2"><div class="text-[10px] text-zinc-500 uppercase tracking-widest mb-1">File</div><div class="text-xs text-zinc-400 truncate">${fn.file.split('/').slice(-2).join('/')}</div></div>`;
408
+ grid.appendChild(card);
409
+ });
410
+ functionsRendered = true;
411
+ }
412
+
413
+ let depsRendered = false;
414
+ function renderDependencies() {
415
+ if (depsRendered) return;
416
+ const grid = document.getElementById('deps-grid');
417
+ Object.entries(depsData).forEach(([name, version]) => {
418
+ const usages = depUsageData[name] || [];
419
+ const card = document.createElement('div');
420
+ card.className = 'bg-zinc-900/50 border border-zinc-800 p-4 rounded flex flex-col gap-2';
421
+
422
+ let usageHtml = '';
423
+ if (usages.length > 0) {
424
+ usageHtml = `
425
+ <div class="mt-2 text-xs text-zinc-500 border-t border-zinc-800 pt-2">
426
+ <span class="uppercase tracking-widest font-bold">Used by (${usages.length})</span>
427
+ <div class="mt-1 flex flex-col gap-1 max-h-32 overflow-y-auto custom-scrollbar">
428
+ ${usages.map(f => `<span class="text-zinc-400 font-mono truncate hover:text-white cursor-pointer" onclick="switchView('files'); viewFileByPath('${f}')">${f}</span>`).join('')}
429
+ </div>
430
+ </div>
431
+ `;
432
+ } else {
433
+ usageHtml = `<div class="mt-2 text-xs text-zinc-600 italic">No direct imports detected.</div>`;
434
+ }
435
+
436
+ card.innerHTML = `
437
+ <div class="flex justify-between items-center">
438
+ <span class="text-zinc-300 font-mono font-bold">${name}</span>
439
+ <span class="px-2 py-1 rounded bg-secondary/10 text-secondary text-xs border border-secondary/20">${version}</span>
440
+ </div>
441
+ ${usageHtml}
442
+ `;
443
+ grid.appendChild(card);
444
+ });
445
+ depsRendered = true;
446
+ }
447
+
448
+ let filesRendered = false;
449
+ function renderFiles() {
450
+ if (filesRendered) return;
451
+ const treeContainer = document.getElementById('file-tree');
452
+ const paths = Object.keys(filesData).sort();
453
+ const root = {};
454
+
455
+ // Build Tree
456
+ paths.forEach(p => {
457
+ const parts = p.split('/');
458
+ let current = root;
459
+ parts.forEach((part, i) => {
460
+ if (!current[part]) current[part] = (i === parts.length - 1) ? { __file: true, path: p } : {};
461
+ current = current[part];
462
+ });
463
+ });
464
+
465
+ // Render Tree
466
+ function createTreeEl(obj, depth = 0) {
467
+ const ul = document.createElement('ul');
468
+ ul.className = depth > 0 ? 'ml-4 border-l border-zinc-800 pl-2' : '';
469
+ Object.keys(obj).sort().forEach(key => {
470
+ if (key === '__file' || key === 'path') return;
471
+ const li = document.createElement('li');
472
+ const isFile = obj[key].__file;
473
+
474
+ if (isFile) {
475
+ li.innerHTML = `<div class="py-1 cursor-pointer text-zinc-400 hover:text-white flex items-center gap-2 group" onclick="viewFile('${obj[key].path}', this)"><svg class="w-3 h-3 text-zinc-600 group-hover:text-primary transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>${key}</div>`;
476
+ } else {
477
+ li.innerHTML = `<div class="py-1 text-zinc-500 text-xs font-bold uppercase tracking-wider mb-1 mt-2">${key}</div>`;
478
+ li.appendChild(createTreeEl(obj[key], depth + 1));
479
+ }
480
+ ul.appendChild(li);
481
+ });
482
+ return ul;
483
+ }
484
+ treeContainer.appendChild(createTreeEl(root));
485
+ filesRendered = true;
486
+ }
487
+
488
+ window.viewFile = (path, el) => {
489
+ document.querySelectorAll('#file-tree div').forEach(d => d.classList.remove('text-primary', 'font-bold'));
490
+ el && el.classList.add('text-primary', 'font-bold');
491
+
492
+ const fileData = filesData[path];
493
+ document.getElementById('file-placeholder').classList.add('hidden');
494
+ document.getElementById('code-container').classList.remove('hidden');
495
+ document.getElementById('current-filename').innerText = path;
496
+ const lid = fileData.language || 'javascript';
497
+ document.getElementById('current-lang').innerText = lid;
498
+
499
+ const codeEl = document.getElementById('code-content');
500
+ codeEl.className = 'language-' + lid;
501
+ codeEl.textContent = fileData.content;
502
+ Prism.highlightElement(codeEl);
503
+ }
504
+
505
+ window.viewFileByPath = (filePath) => {
506
+ const fileData = filesData[filePath];
507
+ if (fileData) {
508
+ window.viewFile(filePath, null);
509
+ }
510
+ }
511
+
512
+ document.querySelector('.nav-tab').classList.add('active');
513
+ </script>
514
+ </body>
515
+
516
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projectify-cli",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "Project Analyzer using LangChain",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -19,7 +19,7 @@
19
19
  ],
20
20
  "scripts": {
21
21
  "prepublishOnly": "npm run build",
22
- "build": "tsc && echo '#!/usr/bin/env node' | cat - dist/index.js > dist/index.js.tmp && mv dist/index.js.tmp dist/index.js && chmod +x dist/index.js",
22
+ "build": "tsc && cp src/report/template.html dist/report/ && echo '#!/usr/bin/env node' | cat - dist/index.js > dist/index.js.tmp && mv dist/index.js.tmp dist/index.js && chmod +x dist/index.js",
23
23
  "start": "node dist/index.js",
24
24
  "test": "jest",
25
25
  "dev": "ts-node src/index.ts"
@@ -36,9 +36,10 @@
36
36
  "@babel/traverse": "^7.24.1",
37
37
  "@google/generative-ai": "^0.24.1",
38
38
  "@langchain/core": "^1.1.8",
39
+ "@langchain/google-genai": "^2.1.3",
39
40
  "@langchain/langgraph": "^1.0.7",
40
41
  "@langchain/ollama": "^1.1.0",
41
- "@langchain/openai": "^0.0.28",
42
+ "@langchain/openai": "^1.2.0",
42
43
  "@types/inquirer": "^8.2.12",
43
44
  "chalk": "^4.1.2",
44
45
  "commander": "^11.1.0",
@@ -46,7 +47,7 @@
46
47
  "fs-extra": "^11.2.0",
47
48
  "graphology": "^0.25.4",
48
49
  "inquirer": "^8.2.7",
49
- "langchain": "^0.1.36",
50
+ "langchain": "^1.2.3",
50
51
  "ora": "^5.4.1",
51
52
  "simple-git": "^3.30.0",
52
53
  "type-fest": "^5.3.1"