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.
- package/README.md +94 -0
- package/dist/ai/index.js +100 -0
- package/dist/ai/providers/base.js +2 -0
- package/dist/ai/providers/gemini.js +56 -0
- package/dist/ai/providers/ollama.js +50 -0
- package/dist/ai/providers/openai.js +72 -0
- package/dist/analyzer/index.js +34 -0
- package/dist/analyzer/parsers/javascript.js +137 -0
- package/dist/analyzer/parsers/python.js +44 -0
- package/dist/graph/index.js +126 -0
- package/dist/index.js +136 -0
- package/dist/report/htmlGenerator.js +582 -0
- package/dist/scanner/index.js +29 -0
- package/dist/utils/fileUtils.js +24 -0
- package/dist/utils/gitUtils.js +47 -0
- package/functions.png +0 -0
- package/graph.png +0 -0
- package/package.json +57 -0
|
@@ -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>></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, '<').replace(/>/g, '>');
|
|
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
|
+
}
|