crg-dev-kit 1.0.0 → 2.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 +51 -90
- package/bin/cli.js +14 -214
- package/lib/actions.js +127 -0
- package/lib/analytics.js +14 -11
- package/lib/paths.js +13 -0
- package/lib/roi.js +1 -9
- package/package.json +1 -1
- package/server.js +521 -240
- package/bin/tutorial.js +0 -198
package/server.js
CHANGED
|
@@ -4,9 +4,11 @@ const path = require('path');
|
|
|
4
4
|
const { execFile } = require('child_process');
|
|
5
5
|
const analytics = require('./lib/analytics');
|
|
6
6
|
const roi = require('./lib/roi');
|
|
7
|
+
const actions = require('./lib/actions');
|
|
7
8
|
|
|
8
9
|
const DEFAULT_PORT = 8742;
|
|
9
10
|
const ASSETS = path.join(__dirname, 'assets');
|
|
11
|
+
const MAX_BODY = 64 * 1024;
|
|
10
12
|
|
|
11
13
|
const DOWNLOADS = [
|
|
12
14
|
{ file: 'setup-crg.sh', label: 'Linux / macOS / WSL Setup', icon: '🐧', desc: 'Bash — auto-detects OS, installs CRG, builds graph, health check' },
|
|
@@ -16,14 +18,6 @@ const DOWNLOADS = [
|
|
|
16
18
|
{ file: 'crg-cheatsheet.pdf', label: 'Cheatsheet PDF', icon: '📄', desc: 'One-page visual reference — tools, workflows, known issues' },
|
|
17
19
|
];
|
|
18
20
|
|
|
19
|
-
const STEPS = [
|
|
20
|
-
{ n: '1', t: 'Download the setup script', d: 'Pick your OS from the Downloads section below.' },
|
|
21
|
-
{ n: '2', t: 'Run it', linux: 'bash setup-crg.sh --with-communities', win: '.\\setup-crg.ps1 -WithCommunities' },
|
|
22
|
-
{ n: '3', t: 'Drop CLAUDE.md in your project root', d: 'Your AI tool reads this file automatically.' },
|
|
23
|
-
{ n: '4', t: 'Restart your AI tool', d: 'Claude Code / Cursor / Windsurf — restart to load MCP config.' },
|
|
24
|
-
{ n: '5', t: 'Verify', linux: 'bash check-crg.sh', win: 'code-review-graph status' },
|
|
25
|
-
];
|
|
26
|
-
|
|
27
21
|
const CRG_TOOLS = [
|
|
28
22
|
{ name: 'detect_changes', label: 'Change Detection', desc: 'Risk-scored impact analysis' },
|
|
29
23
|
{ name: 'get_review_context', label: 'Review Context', desc: 'Token-optimized source snippets' },
|
|
@@ -35,7 +29,7 @@ const CRG_TOOLS = [
|
|
|
35
29
|
{ name: 'list_communities', label: 'Code Communities', desc: 'Clustered code modules' },
|
|
36
30
|
];
|
|
37
31
|
|
|
38
|
-
function esc(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
32
|
+
function esc(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
|
39
33
|
|
|
40
34
|
function mdToHtml(md) {
|
|
41
35
|
let lines = md.split('\n'), out = [], inCode = false;
|
|
@@ -72,170 +66,62 @@ function formatTokens(n) {
|
|
|
72
66
|
return `${n}`;
|
|
73
67
|
}
|
|
74
68
|
|
|
75
|
-
function
|
|
76
|
-
const
|
|
77
|
-
|
|
69
|
+
function openBrowser(url) {
|
|
70
|
+
const plat = process.platform;
|
|
71
|
+
if (plat === 'win32') execFile('cmd', ['/c', 'start', url]);
|
|
72
|
+
else if (plat === 'darwin') execFile('open', [url]);
|
|
73
|
+
else execFile('xdg-open', [url]);
|
|
74
|
+
}
|
|
78
75
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
76
|
+
function findAvailablePort(startPort, maxAttempts = 10) {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const tryPort = (port, attempt) => {
|
|
79
|
+
if (attempt > maxAttempts) {
|
|
80
|
+
reject(new Error(`No available port found after ${maxAttempts} attempts`));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const server = http.createServer();
|
|
84
|
+
server.on('error', (err) => {
|
|
85
|
+
if (err.code === 'EADDRINUSE') {
|
|
86
|
+
server.close();
|
|
87
|
+
tryPort(port + 1, attempt + 1);
|
|
88
|
+
} else {
|
|
89
|
+
reject(err);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
server.on('listening', () => {
|
|
93
|
+
server.close();
|
|
94
|
+
resolve(port);
|
|
95
|
+
});
|
|
96
|
+
server.listen(port);
|
|
97
|
+
};
|
|
98
|
+
tryPort(startPort, 1);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
86
101
|
|
|
87
|
-
|
|
88
|
-
let body = s.d ? `<p class="step-desc">${s.d}</p>` : '';
|
|
89
|
-
if (s.linux) body += `
|
|
90
|
-
<div class="step-cmds">
|
|
91
|
-
<div class="cmd-block"><span class="cmd-label">Linux / macOS</span><code>${s.linux}</code></div>
|
|
92
|
-
<div class="cmd-block"><span class="cmd-label">Windows</span><code>${s.win}</code></div>
|
|
93
|
-
</div>`;
|
|
94
|
-
return `<div class="step"><span class="step-num">${s.n}</span><div class="step-body"><h3>${s.t}</h3>${body}</div></div>`;
|
|
95
|
-
}).join('');
|
|
102
|
+
/* ── Shell page (nav + empty #content + footer + CSS + htmx) ─────────── */
|
|
96
103
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
function buildPage() {
|
|
105
|
+
const tabs = [
|
|
106
|
+
{ id: 'dashboard', label: 'Dashboard' },
|
|
107
|
+
{ id: 'install', label: 'Install' },
|
|
108
|
+
{ id: 'status', label: 'Status' },
|
|
109
|
+
{ id: 'analytics', label: 'Analytics' },
|
|
110
|
+
{ id: 'tools', label: 'Tools' },
|
|
111
|
+
{ id: 'downloads', label: 'Downloads' },
|
|
112
|
+
{ id: 'docs', label: 'Docs' },
|
|
113
|
+
];
|
|
103
114
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// ROI Section
|
|
108
|
-
try {
|
|
109
|
-
const roiData = roi.getAllProjectsROI();
|
|
110
|
-
if (roiData.installed) {
|
|
111
|
-
const verdictColor = roiData.roi.verdict === 'positive' ? '#16a34a' : roiData.roi.verdict === 'neutral' ? '#ea580c' : '#dc2626';
|
|
112
|
-
const verdictIcon = roiData.roi.verdict === 'positive' ? '✅' : roiData.roi.verdict === 'neutral' ? '➖' : '⚠️';
|
|
113
|
-
roiSection = `
|
|
114
|
-
<section id="roi">
|
|
115
|
-
<div class="st">ROI Calculator</div>
|
|
116
|
-
<div class="roi-banner">
|
|
117
|
-
<div class="roi-verdict" style="background:${verdictColor}20;border-color:${verdictColor}">
|
|
118
|
-
<span class="roi-verdict-icon">${verdictIcon}</span>
|
|
119
|
-
<span class="roi-verdict-text">${roiData.roi.verdict === 'positive' ? 'CRG is helping' : roiData.roi.verdict === 'neutral' ? 'No significant change' : 'CRG may not be helping'}</span>
|
|
120
|
-
</div>
|
|
121
|
-
<div class="roi-installed">Installed: ${new Date(roiData.installDate).toLocaleDateString()}</div>
|
|
122
|
-
</div>
|
|
123
|
-
<div class="roi-grid">
|
|
124
|
-
<div class="roi-card">
|
|
125
|
-
<div class="roi-value" style="color:${verdictColor}">${roiData.roi.tokenSavingsPercent}%</div>
|
|
126
|
-
<div class="roi-label">Token Savings</div>
|
|
127
|
-
</div>
|
|
128
|
-
<div class="roi-card">
|
|
129
|
-
<div class="roi-value">${roiData.roi.tokensSaved.toLocaleString()}</div>
|
|
130
|
-
<div class="roi-label">Tokens Saved</div>
|
|
131
|
-
</div>
|
|
132
|
-
<div class="roi-card">
|
|
133
|
-
<div class="roi-value">${roiData.roi.promptReduction}</div>
|
|
134
|
-
<div class="roi-label">Prompts Reduced</div>
|
|
135
|
-
</div>
|
|
136
|
-
<div class="roi-card">
|
|
137
|
-
<div class="roi-value">${roiData.roi.timeSavedHours}h</div>
|
|
138
|
-
<div class="roi-label">Time Saved</div>
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
|
-
<div class="roi-compare">
|
|
142
|
-
<div class="roi-compare-col">
|
|
143
|
-
<h4>Before CRG</h4>
|
|
144
|
-
<div class="roi-compare-stat">
|
|
145
|
-
<span class="roi-compare-label">Avg Tokens/Session</span>
|
|
146
|
-
<span class="roi-compare-value">${roiData.baseline.avgTokensPerSession.toLocaleString()}</span>
|
|
147
|
-
</div>
|
|
148
|
-
<div class="roi-compare-stat">
|
|
149
|
-
<span class="roi-compare-label">Total Prompts</span>
|
|
150
|
-
<span class="roi-compare-value">${roiData.baseline.totalPrompts}</span>
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
153
|
-
<div class="roi-arrow">→</div>
|
|
154
|
-
<div class="roi-compare-col">
|
|
155
|
-
<h4>After CRG</h4>
|
|
156
|
-
<div class="roi-compare-stat">
|
|
157
|
-
<span class="roi-compare-label">Avg Tokens/Session</span>
|
|
158
|
-
<span class="roi-compare-value" style="color:${verdictColor}">${roiData.post.avgTokensPerSession.toLocaleString()}</span>
|
|
159
|
-
</div>
|
|
160
|
-
<div class="roi-compare-stat">
|
|
161
|
-
<span class="roi-compare-label">Total Prompts</span>
|
|
162
|
-
<span class="roi-compare-value" style="color:${verdictColor}">${roiData.post.totalPrompts}</span>
|
|
163
|
-
</div>
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
<div class="roi-cta">
|
|
167
|
-
<code>npx crg-dev-kit roi</code> for detailed report
|
|
168
|
-
</div>
|
|
169
|
-
</section>`;
|
|
170
|
-
}
|
|
171
|
-
} catch (e) {
|
|
172
|
-
// No ROI data yet
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (analyticsData) {
|
|
176
|
-
const savingsColor = analyticsData.avgSavingsPercent >= 70 ? '#16a34a' : analyticsData.avgSavingsPercent >= 50 ? '#ea580c' : '#dc2626';
|
|
177
|
-
analyticsSection = `
|
|
178
|
-
<section id="analytics">
|
|
179
|
-
<div class="st">Token Analytics</div>
|
|
180
|
-
<div class="analytics-grid">
|
|
181
|
-
<div class="stat-card primary">
|
|
182
|
-
<div class="stat-value" style="color:${savingsColor}">${analyticsData.avgSavingsPercent}%</div>
|
|
183
|
-
<div class="stat-label">Avg Token Savings</div>
|
|
184
|
-
<div class="stat-sub">vs reading full source files</div>
|
|
185
|
-
</div>
|
|
186
|
-
<div class="stat-card">
|
|
187
|
-
<div class="stat-value">${formatTokens(analyticsData.totalTokensSaved)}</div>
|
|
188
|
-
<div class="stat-label">Total Tokens Saved</div>
|
|
189
|
-
<div class="stat-sub">${analyticsData.totalSessions} sessions across ${analyticsData.totalProjects} projects</div>
|
|
190
|
-
</div>
|
|
191
|
-
<div class="stat-card">
|
|
192
|
-
<div class="stat-value">$${analyticsData.estimatedCostSavings}</div>
|
|
193
|
-
<div class="stat-label">Estimated Cost Savings</div>
|
|
194
|
-
<div class="stat-sub">at $10/M input tokens</div>
|
|
195
|
-
</div>
|
|
196
|
-
</div>
|
|
197
|
-
${analyticsData.projects && analyticsData.projects.length > 0 ? `
|
|
198
|
-
<div class="project-table">
|
|
199
|
-
<table>
|
|
200
|
-
<thead><tr><th>Project</th><th>Sessions</th><th>Files</th><th>Savings</th><th>Tokens Saved</th></tr></thead>
|
|
201
|
-
<tbody>
|
|
202
|
-
${analyticsData.projects.map(p => `
|
|
203
|
-
<tr>
|
|
204
|
-
<td><code>${esc(p.project.split('/').pop())}</code></td>
|
|
205
|
-
<td>${p.totalSessions}</td>
|
|
206
|
-
<td>${p.totalFilesReviewed}</td>
|
|
207
|
-
<td><span class="savings-badge" style="color:${p.avgSavingsPercent >= 70 ? '#16a34a' : '#ea580c'}">${p.avgSavingsPercent}%</span></td>
|
|
208
|
-
<td>${formatTokens(p.totalTokensSaved)}</td>
|
|
209
|
-
</tr>`).join('')}
|
|
210
|
-
</tbody>
|
|
211
|
-
</table>
|
|
212
|
-
</div>` : ''}
|
|
213
|
-
${analyticsData.projects && analyticsData.projects.length > 0 && analyticsData.toolUsage ? `
|
|
214
|
-
<div class="tool-usage-section">
|
|
215
|
-
<h3>Most Used Tools</h3>
|
|
216
|
-
<div class="tool-bars">
|
|
217
|
-
${Object.entries(analyticsData.toolUsage)
|
|
218
|
-
.sort((a, b) => b[1] - a[1])
|
|
219
|
-
.slice(0, 6)
|
|
220
|
-
.map(([tool, count]) => {
|
|
221
|
-
const maxCount = Math.max(...Object.values(analyticsData.toolUsage));
|
|
222
|
-
const pct = (count / maxCount * 100).toFixed(0);
|
|
223
|
-
return `<div class="tool-bar">
|
|
224
|
-
<span class="tool-bar-name">${tool}</span>
|
|
225
|
-
<div class="tool-bar-track"><div class="tool-bar-fill" style="width:${pct}%"></div></div>
|
|
226
|
-
<span class="tool-bar-count">${count}</span>
|
|
227
|
-
</div>`;
|
|
228
|
-
}).join('')}
|
|
229
|
-
</div>
|
|
230
|
-
</div>` : ''}
|
|
231
|
-
</section>`;
|
|
232
|
-
}
|
|
115
|
+
const navLinks = tabs.map(t =>
|
|
116
|
+
`<a href="#" hx-get="/tab/${t.id}" hx-target="#content" hx-swap="innerHTML" class="${t.id === 'dashboard' ? 'active' : ''}" onclick="document.querySelectorAll('.nk a').forEach(a=>a.classList.remove('active'));this.classList.add('active')">${t.label}</a>`
|
|
117
|
+
).join('');
|
|
233
118
|
|
|
234
119
|
return `<!DOCTYPE html>
|
|
235
120
|
<html lang="en">
|
|
236
121
|
<head>
|
|
237
122
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
238
123
|
<title>code-review-graph Dev Kit</title>
|
|
124
|
+
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
|
239
125
|
<style>
|
|
240
126
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
|
|
241
127
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
@@ -245,8 +131,9 @@ nav{background:var(--s);border-bottom:1px solid var(--b);padding:0 40px;display:
|
|
|
245
131
|
.nb{display:flex;align-items:center;gap:10px;font-weight:800;font-size:15px;letter-spacing:-0.02em}
|
|
246
132
|
.nl{width:28px;height:28px;background:var(--a);border-radius:6px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:14px}
|
|
247
133
|
.nk{margin-left:auto;display:flex;gap:24px}
|
|
248
|
-
.nk a{color:var(--t3);text-decoration:none;font-size:13px;font-weight:500;transition:color .15s}
|
|
134
|
+
.nk a{color:var(--t3);text-decoration:none;font-size:13px;font-weight:500;transition:color .15s;padding-bottom:4px}
|
|
249
135
|
.nk a:hover{color:var(--a)}
|
|
136
|
+
.nk a.active{color:var(--a);border-bottom:2px solid var(--a)}
|
|
250
137
|
.badge{background:var(--al);color:var(--a);padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;border:1px solid var(--ab)}
|
|
251
138
|
.hero{text-align:center;padding:64px 40px 48px;max-width:680px;margin:0 auto}
|
|
252
139
|
.hero h1{font-size:36px;font-weight:800;letter-spacing:-0.03em;line-height:1.15;margin-bottom:12px}
|
|
@@ -338,9 +225,35 @@ footer a{color:var(--a);text-decoration:none;font-weight:600}
|
|
|
338
225
|
.tool-name{font-family:var(--m);font-size:11px;color:var(--a);font-weight:600}
|
|
339
226
|
.tool-label{font-size:12px;font-weight:700;color:var(--t)}
|
|
340
227
|
.tool-desc{font-size:11px;color:var(--t3)}
|
|
228
|
+
/* Tab navigation */
|
|
229
|
+
.nk a.active{color:var(--a);border-bottom:2px solid var(--a)}
|
|
230
|
+
/* Action buttons */
|
|
231
|
+
.btn{background:var(--a);color:#fff;padding:10px 20px;border:none;border-radius:6px;font-weight:700;font-size:13px;cursor:pointer;font-family:inherit}
|
|
232
|
+
.btn:hover{background:#c2410c}
|
|
233
|
+
.btn-danger{background:#dc2626}
|
|
234
|
+
.btn-danger:hover{background:#b91c1c}
|
|
235
|
+
.btn-secondary{background:var(--bg);color:var(--t2);border:1px solid var(--b)}
|
|
236
|
+
/* Status checks */
|
|
237
|
+
.check-pass{color:#16a34a}
|
|
238
|
+
.check-fail{color:#dc2626}
|
|
239
|
+
.check-item{display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid var(--b)}
|
|
240
|
+
.check-item:last-child{border-bottom:none}
|
|
241
|
+
/* Result messages */
|
|
242
|
+
.result-box{background:var(--s);border:1px solid var(--b);border-radius:8px;padding:20px;margin-top:16px}
|
|
243
|
+
.result-success{border-color:#16a34a;background:#f0fdf4}
|
|
244
|
+
.result-error{border-color:#dc2626;background:#fef2f2}
|
|
245
|
+
/* Input fields */
|
|
246
|
+
.input{font-family:var(--m);font-size:13px;padding:8px 12px;border:1px solid var(--b);border-radius:6px;width:100%;background:var(--s)}
|
|
247
|
+
.input:focus{outline:none;border-color:var(--a)}
|
|
248
|
+
/* htmx loading indicator */
|
|
249
|
+
.htmx-indicator{display:none}
|
|
250
|
+
.htmx-request .htmx-indicator{display:inline-block}
|
|
251
|
+
.spinner{width:16px;height:16px;border:2px solid var(--b);border-top-color:var(--a);border-radius:50%;animation:spin .6s linear infinite;display:inline-block}
|
|
252
|
+
@keyframes spin{to{transform:rotate(360deg)}}
|
|
341
253
|
@media(max-width:768px){
|
|
342
254
|
.analytics-grid{grid-template-columns:1fr}
|
|
343
255
|
.tools-grid{grid-template-columns:repeat(2,1fr)}
|
|
256
|
+
.roi-grid{grid-template-columns:repeat(2,1fr)}
|
|
344
257
|
nav{padding:0 20px}
|
|
345
258
|
.hero{padding:48px 20px 32px}
|
|
346
259
|
section{padding:0 20px 32px}
|
|
@@ -349,48 +262,312 @@ footer a{color:var(--a);text-decoration:none;font-weight:600}
|
|
|
349
262
|
</style>
|
|
350
263
|
</head>
|
|
351
264
|
<body>
|
|
352
|
-
<nav
|
|
353
|
-
<div class="
|
|
354
|
-
<
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
<section id="tools"><div class="st">Available Tools</div><div class="tools-grid">${toolCards}</div></section>
|
|
358
|
-
<section id="downloads"><div class="st">Downloads</div><div class="dl-grid">${dlCards}</div></section>
|
|
359
|
-
<section id="docs"><div class="st">Documentation</div><div class="rc">${readmeHtml}</div></section>
|
|
265
|
+
<nav>
|
|
266
|
+
<div class="nb"><div class="nl">G</div>code-review-graph</div>
|
|
267
|
+
<div class="nk">${navLinks}<span class="badge">v2.1.0</span></div>
|
|
268
|
+
</nav>
|
|
269
|
+
<main id="content" hx-get="/tab/dashboard" hx-trigger="load" hx-swap="innerHTML"></main>
|
|
360
270
|
<footer><a href="https://github.com/tirth8205/code-review-graph">github.com/tirth8205/code-review-graph</a> · MIT License · All data stays local</footer>
|
|
361
271
|
</body></html>`;
|
|
362
272
|
}
|
|
363
273
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
274
|
+
/* ── Tab fragments ────────────────────────────────────────────────────── */
|
|
275
|
+
|
|
276
|
+
function tabDashboard() {
|
|
277
|
+
return `
|
|
278
|
+
<div class="hero">
|
|
279
|
+
<h1>Ship code with a <span>knowledge graph</span></h1>
|
|
280
|
+
<p>One command to set up AI-powered code review, impact analysis, and codebase navigation. Track token savings across your team.</p>
|
|
281
|
+
<div class="hs">
|
|
282
|
+
<div><div class="sv">22</div><div class="sl">MCP Tools</div></div>
|
|
283
|
+
<div><div class="sv">19</div><div class="sl">Languages</div></div>
|
|
284
|
+
<div><div class="sv">8.2x</div><div class="sl">Token Reduction</div></div>
|
|
285
|
+
<div><div class="sv">0</div><div class="sl">Telemetry</div></div>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
<section>
|
|
289
|
+
<div class="st">Quick Status</div>
|
|
290
|
+
<div id="dashboard-status" hx-get="/api/health?fragment=1" hx-trigger="load" hx-swap="innerHTML">
|
|
291
|
+
<span class="spinner"></span> Loading status...
|
|
292
|
+
</div>
|
|
293
|
+
</section>`;
|
|
369
294
|
}
|
|
370
295
|
|
|
371
|
-
function
|
|
372
|
-
return
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
296
|
+
function tabInstall(cwd) {
|
|
297
|
+
return `
|
|
298
|
+
<section>
|
|
299
|
+
<div class="st">Install CRG Dev Kit</div>
|
|
300
|
+
<p style="color:var(--t2);font-size:14px;margin-bottom:16px">Copy setup scripts, CLAUDE.md, and health check to your project directory.</p>
|
|
301
|
+
<div style="margin-bottom:16px">
|
|
302
|
+
<label style="font-size:12px;font-weight:600;color:var(--t3);display:block;margin-bottom:6px">Target Directory</label>
|
|
303
|
+
<input class="input" id="install-dir" name="targetDir" value="${esc(cwd)}" />
|
|
304
|
+
</div>
|
|
305
|
+
<div style="display:flex;gap:12px">
|
|
306
|
+
<button class="btn"
|
|
307
|
+
hx-post="/api/install"
|
|
308
|
+
hx-target="#install-result"
|
|
309
|
+
hx-swap="innerHTML"
|
|
310
|
+
hx-include="#install-dir"
|
|
311
|
+
hx-indicator="#install-spinner">
|
|
312
|
+
Install Files <span id="install-spinner" class="htmx-indicator"><span class="spinner"></span></span>
|
|
313
|
+
</button>
|
|
314
|
+
<button class="btn btn-danger"
|
|
315
|
+
hx-post="/api/uninstall"
|
|
316
|
+
hx-target="#install-result"
|
|
317
|
+
hx-swap="innerHTML"
|
|
318
|
+
hx-include="#install-dir"
|
|
319
|
+
hx-indicator="#uninstall-spinner">
|
|
320
|
+
Uninstall <span id="uninstall-spinner" class="htmx-indicator"><span class="spinner"></span></span>
|
|
321
|
+
</button>
|
|
322
|
+
</div>
|
|
323
|
+
<div id="install-result"></div>
|
|
324
|
+
</section>`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function tabStatusFragment(statusData) {
|
|
328
|
+
const items = statusData.checks.map(c => `
|
|
329
|
+
<div class="check-item">
|
|
330
|
+
<span class="${c.exists ? 'check-pass' : 'check-fail'}">${c.exists ? '✓' : '✗'}</span>
|
|
331
|
+
<span style="font-size:13px">${esc(c.label)}</span>
|
|
332
|
+
<span style="font-family:var(--m);font-size:11px;color:var(--t4);margin-left:auto">${esc(c.file)}</span>
|
|
333
|
+
</div>`).join('');
|
|
334
|
+
|
|
335
|
+
const crgItem = `
|
|
336
|
+
<div class="check-item">
|
|
337
|
+
<span class="${statusData.crgInstalled ? 'check-pass' : 'check-fail'}">${statusData.crgInstalled ? '✓' : '✗'}</span>
|
|
338
|
+
<span style="font-size:13px">code-review-graph CLI</span>
|
|
339
|
+
<span style="font-family:var(--m);font-size:11px;color:var(--t4);margin-left:auto">${statusData.crgVersion ? esc(statusData.crgVersion) : 'not found'}</span>
|
|
340
|
+
</div>`;
|
|
341
|
+
|
|
342
|
+
const verdictColors = { ready: '#16a34a', partial: '#ea580c', not_setup: '#dc2626' };
|
|
343
|
+
const verdictLabels = { ready: 'Ready', partial: 'Partial Setup', not_setup: 'Not Set Up' };
|
|
344
|
+
const color = verdictColors[statusData.verdict] || '#dc2626';
|
|
345
|
+
const label = verdictLabels[statusData.verdict] || statusData.verdict;
|
|
346
|
+
|
|
347
|
+
return `
|
|
348
|
+
<section>
|
|
349
|
+
<div class="st">Status</div>
|
|
350
|
+
<div class="result-box" style="border-color:${color}">
|
|
351
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:16px">
|
|
352
|
+
<span style="font-size:18px;font-weight:800;color:${color}">${statusData.found}/${statusData.total}</span>
|
|
353
|
+
<span style="font-size:14px;font-weight:600;color:${color}">${label}</span>
|
|
354
|
+
</div>
|
|
355
|
+
<div style="font-family:var(--m);font-size:11px;color:var(--t4);margin-bottom:12px">${esc(statusData.targetDir)}</div>
|
|
356
|
+
${items}
|
|
357
|
+
${crgItem}
|
|
358
|
+
</div>
|
|
359
|
+
</section>`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function tabAnalytics() {
|
|
363
|
+
let roiHtml = '';
|
|
364
|
+
try {
|
|
365
|
+
const roiData = roi.getAllProjectsROI();
|
|
366
|
+
if (roiData.installed) {
|
|
367
|
+
const verdictColor = roiData.roi.verdict === 'positive' ? '#16a34a' : roiData.roi.verdict === 'neutral' ? '#ea580c' : '#dc2626';
|
|
368
|
+
const verdictIcon = roiData.roi.verdict === 'positive' ? '✓' : roiData.roi.verdict === 'neutral' ? '−' : '⚠';
|
|
369
|
+
roiHtml = `
|
|
370
|
+
<div class="st">ROI Calculator</div>
|
|
371
|
+
<div class="roi-banner">
|
|
372
|
+
<div class="roi-verdict" style="background:${verdictColor}20;border-color:${verdictColor}">
|
|
373
|
+
<span class="roi-verdict-icon">${verdictIcon}</span>
|
|
374
|
+
<span class="roi-verdict-text">${roiData.roi.verdict === 'positive' ? 'CRG is helping' : roiData.roi.verdict === 'neutral' ? 'No significant change' : 'CRG may not be helping'}</span>
|
|
375
|
+
</div>
|
|
376
|
+
<div class="roi-installed">Installed: ${new Date(roiData.installDate).toLocaleDateString()}</div>
|
|
377
|
+
</div>
|
|
378
|
+
<div class="roi-grid">
|
|
379
|
+
<div class="roi-card">
|
|
380
|
+
<div class="roi-value" style="color:${verdictColor}">${roiData.roi.tokenSavingsPercent}%</div>
|
|
381
|
+
<div class="roi-label">Token Savings</div>
|
|
382
|
+
</div>
|
|
383
|
+
<div class="roi-card">
|
|
384
|
+
<div class="roi-value">${roiData.roi.tokensSaved.toLocaleString()}</div>
|
|
385
|
+
<div class="roi-label">Tokens Saved</div>
|
|
386
|
+
</div>
|
|
387
|
+
<div class="roi-card">
|
|
388
|
+
<div class="roi-value">${roiData.roi.promptReduction}</div>
|
|
389
|
+
<div class="roi-label">Prompts Reduced</div>
|
|
390
|
+
</div>
|
|
391
|
+
<div class="roi-card">
|
|
392
|
+
<div class="roi-value">${roiData.roi.timeSavedHours}h</div>
|
|
393
|
+
<div class="roi-label">Time Saved</div>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
<div class="roi-compare">
|
|
397
|
+
<div class="roi-compare-col">
|
|
398
|
+
<h4>Before CRG</h4>
|
|
399
|
+
<div class="roi-compare-stat">
|
|
400
|
+
<span class="roi-compare-label">Avg Tokens/Session</span>
|
|
401
|
+
<span class="roi-compare-value">${roiData.baseline.avgTokensPerSession.toLocaleString()}</span>
|
|
402
|
+
</div>
|
|
403
|
+
<div class="roi-compare-stat">
|
|
404
|
+
<span class="roi-compare-label">Total Prompts</span>
|
|
405
|
+
<span class="roi-compare-value">${roiData.baseline.totalPrompts}</span>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
<div class="roi-arrow">→</div>
|
|
409
|
+
<div class="roi-compare-col">
|
|
410
|
+
<h4>After CRG</h4>
|
|
411
|
+
<div class="roi-compare-stat">
|
|
412
|
+
<span class="roi-compare-label">Avg Tokens/Session</span>
|
|
413
|
+
<span class="roi-compare-value" style="color:${verdictColor}">${roiData.post.avgTokensPerSession.toLocaleString()}</span>
|
|
414
|
+
</div>
|
|
415
|
+
<div class="roi-compare-stat">
|
|
416
|
+
<span class="roi-compare-label">Total Prompts</span>
|
|
417
|
+
<span class="roi-compare-value" style="color:${verdictColor}">${roiData.post.totalPrompts}</span>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
<div class="roi-cta">
|
|
422
|
+
<code>npx crg-dev-kit roi</code> for detailed report
|
|
423
|
+
</div>
|
|
424
|
+
<div style="margin-bottom:32px"></div>`;
|
|
425
|
+
}
|
|
426
|
+
} catch (e) {
|
|
427
|
+
// No ROI data yet
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let tokenHtml = '';
|
|
431
|
+
const analyticsData = analytics.getAllStats();
|
|
432
|
+
if (analyticsData) {
|
|
433
|
+
const savingsColor = analyticsData.avgSavingsPercent >= 70 ? '#16a34a' : analyticsData.avgSavingsPercent >= 50 ? '#ea580c' : '#dc2626';
|
|
434
|
+
tokenHtml = `
|
|
435
|
+
<div class="st">Token Analytics</div>
|
|
436
|
+
<div class="analytics-grid">
|
|
437
|
+
<div class="stat-card primary">
|
|
438
|
+
<div class="stat-value" style="color:${savingsColor}">${analyticsData.avgSavingsPercent}%</div>
|
|
439
|
+
<div class="stat-label">Avg Token Savings</div>
|
|
440
|
+
<div class="stat-sub">vs reading full source files</div>
|
|
441
|
+
</div>
|
|
442
|
+
<div class="stat-card">
|
|
443
|
+
<div class="stat-value">${formatTokens(analyticsData.totalTokensSaved)}</div>
|
|
444
|
+
<div class="stat-label">Total Tokens Saved</div>
|
|
445
|
+
<div class="stat-sub">${analyticsData.totalSessions} sessions across ${analyticsData.totalProjects} projects</div>
|
|
446
|
+
</div>
|
|
447
|
+
<div class="stat-card">
|
|
448
|
+
<div class="stat-value">$${analyticsData.estimatedCostSavings}</div>
|
|
449
|
+
<div class="stat-label">Estimated Cost Savings</div>
|
|
450
|
+
<div class="stat-sub">at $10/M input tokens</div>
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
453
|
+
${analyticsData.projects && analyticsData.projects.length > 0 ? `
|
|
454
|
+
<div class="project-table">
|
|
455
|
+
<table>
|
|
456
|
+
<thead><tr><th>Project</th><th>Sessions</th><th>Files</th><th>Savings</th><th>Tokens Saved</th></tr></thead>
|
|
457
|
+
<tbody>
|
|
458
|
+
${analyticsData.projects.map(p => `
|
|
459
|
+
<tr>
|
|
460
|
+
<td><code>${esc(p.project.split('/').pop())}</code></td>
|
|
461
|
+
<td>${p.totalSessions}</td>
|
|
462
|
+
<td>${p.totalFilesReviewed}</td>
|
|
463
|
+
<td><span class="savings-badge" style="color:${p.avgSavingsPercent >= 70 ? '#16a34a' : '#ea580c'}">${p.avgSavingsPercent}%</span></td>
|
|
464
|
+
<td>${formatTokens(p.totalTokensSaved)}</td>
|
|
465
|
+
</tr>`).join('')}
|
|
466
|
+
</tbody>
|
|
467
|
+
</table>
|
|
468
|
+
</div>` : ''}
|
|
469
|
+
${analyticsData.projects && analyticsData.projects.length > 0 && analyticsData.toolUsage ? `
|
|
470
|
+
<div class="tool-usage-section">
|
|
471
|
+
<h3>Most Used Tools</h3>
|
|
472
|
+
<div class="tool-bars">
|
|
473
|
+
${Object.entries(analyticsData.toolUsage)
|
|
474
|
+
.sort((a, b) => b[1] - a[1])
|
|
475
|
+
.slice(0, 6)
|
|
476
|
+
.map(([tool, count]) => {
|
|
477
|
+
const maxCount = Math.max(...Object.values(analyticsData.toolUsage));
|
|
478
|
+
const pct = (count / maxCount * 100).toFixed(0);
|
|
479
|
+
return `<div class="tool-bar">
|
|
480
|
+
<span class="tool-bar-name">${tool}</span>
|
|
481
|
+
<div class="tool-bar-track"><div class="tool-bar-fill" style="width:${pct}%"></div></div>
|
|
482
|
+
<span class="tool-bar-count">${count}</span>
|
|
483
|
+
</div>`;
|
|
484
|
+
}).join('')}
|
|
485
|
+
</div>
|
|
486
|
+
</div>` : ''}`;
|
|
487
|
+
} else {
|
|
488
|
+
tokenHtml = `<div class="st">Token Analytics</div><p style="color:var(--t3);font-size:14px">No analytics data yet. Use CRG tools in a project to start tracking.</p>`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return `<section>${roiHtml}${tokenHtml}</section>`;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function tabTools() {
|
|
495
|
+
const toolCards = CRG_TOOLS.map(t => `
|
|
496
|
+
<div class="tool-card">
|
|
497
|
+
<span class="tool-name">${t.name}</span>
|
|
498
|
+
<span class="tool-label">${t.label}</span>
|
|
499
|
+
<span class="tool-desc">${t.desc}</span>
|
|
500
|
+
</div>`).join('');
|
|
501
|
+
return `<section><div class="st">Available Tools</div><div class="tools-grid">${toolCards}</div></section>`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function tabDownloads() {
|
|
505
|
+
const dlCards = DOWNLOADS.map(d => `
|
|
506
|
+
<a href="/download/${d.file}" class="dl-card" download>
|
|
507
|
+
<span class="dl-icon">${d.icon}</span>
|
|
508
|
+
<div class="dl-info"><span class="dl-name">${d.label}</span><span class="dl-desc">${d.desc}</span></div>
|
|
509
|
+
<span class="dl-size">${fileSize(d.file)}</span>
|
|
510
|
+
<span class="dl-btn">Download</span>
|
|
511
|
+
</a>`).join('');
|
|
512
|
+
return `<section><div class="st">Downloads</div><div class="dl-grid">${dlCards}</div></section>`;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function tabDocs() {
|
|
516
|
+
const readmePath = path.join(ASSETS, 'README.md');
|
|
517
|
+
const readmeHtml = fs.existsSync(readmePath) ? mdToHtml(fs.readFileSync(readmePath, 'utf8')) : '<p>No README found.</p>';
|
|
518
|
+
return `<section><div class="st">Documentation</div><div class="rc">${readmeHtml}</div></section>`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/* ── API action fragments ─────────────────────────────────────────────── */
|
|
522
|
+
|
|
523
|
+
function renderInstallResult(result) {
|
|
524
|
+
const isSuccess = result.copied > 0;
|
|
525
|
+
const items = result.results.map(r => {
|
|
526
|
+
if (r.status === 'copied') return `<div class="check-item"><span class="check-pass">✓</span> ${esc(r.file)} copied</div>`;
|
|
527
|
+
if (r.status === 'skipped') return `<div class="check-item"><span style="color:var(--t4)">−</span> ${esc(r.file)} skipped (${esc(r.reason)})</div>`;
|
|
528
|
+
return `<div class="check-item"><span class="check-fail">✗</span> ${esc(r.file)} — source missing</div>`;
|
|
529
|
+
}).join('');
|
|
530
|
+
return `<div class="result-box ${isSuccess ? 'result-success' : ''}">${items}<p style="margin-top:12px;font-size:13px;color:var(--t3)">${result.copied} of ${result.total} files installed to <code>${esc(result.targetDir)}</code></p></div>`;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function renderUninstallResult(result) {
|
|
534
|
+
if (result.removed.length === 0) {
|
|
535
|
+
return `<div class="result-box"><p style="font-size:13px;color:var(--t3)">No CRG files found in <code>${esc(result.targetDir)}</code></p></div>`;
|
|
536
|
+
}
|
|
537
|
+
const items = result.removed.map(f => `<div class="check-item"><span class="check-fail">✗</span> ${esc(f)} removed</div>`).join('');
|
|
538
|
+
return `<div class="result-box result-error">${items}<p style="margin-top:12px;font-size:13px;color:var(--t3)">${result.removed.length} files removed from <code>${esc(result.targetDir)}</code></p></div>`;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function renderHealthResult(result) {
|
|
542
|
+
const items = result.results.map(r => `
|
|
543
|
+
<div class="check-item">
|
|
544
|
+
<span class="${r.pass ? 'check-pass' : 'check-fail'}">${r.pass ? '✓' : '✗'}</span>
|
|
545
|
+
<span style="font-size:13px">${esc(r.check)}</span>
|
|
546
|
+
${r.version ? `<span style="font-family:var(--m);font-size:11px;color:var(--t4);margin-left:auto">${esc(r.version)}</span>` : ''}
|
|
547
|
+
</div>`).join('');
|
|
548
|
+
return `<div class="result-box" style="border-color:${result.pass === result.total ? '#16a34a' : '#ea580c'}"><div style="font-size:14px;font-weight:600;margin-bottom:12px">${result.pass}/${result.total} checks passing</div>${items}</div>`;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/* ── Server ───────────────────────────────────────────────────────────── */
|
|
552
|
+
|
|
553
|
+
function readBody(req, res, cb) {
|
|
554
|
+
let body = '';
|
|
555
|
+
req.on('data', chunk => {
|
|
556
|
+
body += chunk;
|
|
557
|
+
if (body.length > MAX_BODY) {
|
|
558
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
559
|
+
res.end(JSON.stringify({ error: 'Payload too large' }));
|
|
560
|
+
req.destroy();
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
req.on('end', () => {
|
|
564
|
+
if (res.writableEnded) return;
|
|
565
|
+
try {
|
|
566
|
+
cb(JSON.parse(body));
|
|
567
|
+
} catch (e) {
|
|
568
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
569
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
570
|
+
}
|
|
394
571
|
});
|
|
395
572
|
}
|
|
396
573
|
|
|
@@ -399,68 +576,170 @@ function start(port, noOpen) {
|
|
|
399
576
|
noOpen = noOpen || false;
|
|
400
577
|
|
|
401
578
|
findAvailablePort(port).then(availablePort => {
|
|
402
|
-
const
|
|
579
|
+
const cwd = process.cwd();
|
|
403
580
|
|
|
404
581
|
const server = http.createServer((req, res) => {
|
|
405
582
|
const url = new URL(req.url, `http://localhost:${availablePort}`);
|
|
406
583
|
|
|
584
|
+
/* ── Shell page ─────────────────────────────────────────────── */
|
|
407
585
|
if (url.pathname === '/' || url.pathname === '') {
|
|
408
|
-
const html = buildPage(
|
|
586
|
+
const html = buildPage();
|
|
409
587
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
410
588
|
res.end(html);
|
|
589
|
+
|
|
590
|
+
/* ── Tab fragments (htmx) ───────────────────────────────────── */
|
|
591
|
+
} else if (url.pathname === '/tab/dashboard') {
|
|
592
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
593
|
+
res.end(tabDashboard());
|
|
594
|
+
|
|
595
|
+
} else if (url.pathname === '/tab/install') {
|
|
596
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
597
|
+
res.end(tabInstall(cwd));
|
|
598
|
+
|
|
599
|
+
} else if (url.pathname === '/tab/status') {
|
|
600
|
+
actions.status(cwd).then(statusData => {
|
|
601
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
602
|
+
res.end(tabStatusFragment(statusData));
|
|
603
|
+
}).catch(err => {
|
|
604
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
605
|
+
res.end(`<section><div class="result-box result-error"><p>Error: ${esc(err.message)}</p></div></section>`);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
} else if (url.pathname === '/tab/analytics') {
|
|
609
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
610
|
+
res.end(tabAnalytics());
|
|
611
|
+
|
|
612
|
+
} else if (url.pathname === '/tab/tools') {
|
|
613
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
614
|
+
res.end(tabTools());
|
|
615
|
+
|
|
616
|
+
} else if (url.pathname === '/tab/downloads') {
|
|
617
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
618
|
+
res.end(tabDownloads());
|
|
619
|
+
|
|
620
|
+
} else if (url.pathname === '/tab/docs') {
|
|
621
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
622
|
+
res.end(tabDocs());
|
|
623
|
+
|
|
624
|
+
/* ── Action API endpoints (return HTML fragments) ───────────── */
|
|
625
|
+
} else if (url.pathname === '/api/install' && req.method === 'POST') {
|
|
626
|
+
readBody(req, res, (data) => {
|
|
627
|
+
const targetDir = data.targetDir || cwd;
|
|
628
|
+
try {
|
|
629
|
+
const result = actions.install(targetDir);
|
|
630
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
631
|
+
res.end(renderInstallResult(result));
|
|
632
|
+
} catch (err) {
|
|
633
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
634
|
+
res.end(`<div class="result-box result-error"><p>Error: ${esc(err.message)}</p></div>`);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
} else if (url.pathname === '/api/uninstall' && req.method === 'POST') {
|
|
639
|
+
readBody(req, res, (data) => {
|
|
640
|
+
const targetDir = data.targetDir || cwd;
|
|
641
|
+
try {
|
|
642
|
+
const result = actions.uninstall(targetDir);
|
|
643
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
644
|
+
res.end(renderUninstallResult(result));
|
|
645
|
+
} catch (err) {
|
|
646
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
647
|
+
res.end(`<div class="result-box result-error"><p>Error: ${esc(err.message)}</p></div>`);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
} else if (url.pathname === '/api/health') {
|
|
652
|
+
actions.healthCheck(url.searchParams.get('targetDir') || cwd).then(result => {
|
|
653
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
654
|
+
res.end(renderHealthResult(result));
|
|
655
|
+
}).catch(err => {
|
|
656
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
657
|
+
res.end(`<div class="result-box result-error"><p>Error: ${esc(err.message)}</p></div>`);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
/* ── Existing JSON API endpoints ────────────────────────────── */
|
|
411
661
|
} else if (url.pathname === '/api/analytics') {
|
|
412
662
|
const data = analytics.getAllStats();
|
|
413
663
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
414
664
|
res.end(JSON.stringify(data || { message: 'No analytics data yet' }));
|
|
665
|
+
|
|
415
666
|
} else if (url.pathname === '/api/roi') {
|
|
416
667
|
const data = roi.getAllProjectsROI();
|
|
417
668
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
418
669
|
res.end(JSON.stringify(data));
|
|
670
|
+
|
|
419
671
|
} else if (url.pathname === '/api/report') {
|
|
420
672
|
const report = analytics.generateReport();
|
|
421
673
|
res.writeHead(200, { 'Content-Type': 'text/markdown' });
|
|
422
674
|
res.end(report);
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
675
|
+
|
|
676
|
+
} else if (url.pathname === '/api/session' && req.method === 'POST') {
|
|
677
|
+
readBody(req, res, (data) => {
|
|
678
|
+
const session = analytics.createSession(data.project, data.operation);
|
|
679
|
+
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
680
|
+
res.end(JSON.stringify(session));
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
} else if (url.pathname === '/api/session' && req.method !== 'POST') {
|
|
684
|
+
res.writeHead(405); res.end('Method not allowed');
|
|
685
|
+
|
|
686
|
+
} else if (url.pathname.match(/^\/api\/session\/[^/]+\/tool$/) && req.method === 'POST') {
|
|
687
|
+
const sessionId = url.pathname.split('/')[3];
|
|
688
|
+
readBody(req, res, (data) => {
|
|
689
|
+
if (!data.tool) {
|
|
690
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
691
|
+
res.end(JSON.stringify({ error: 'Missing tool field' }));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const session = analytics.logToolUsage(sessionId, data.tool, data.count);
|
|
695
|
+
if (!session) {
|
|
696
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
697
|
+
res.end(JSON.stringify({ error: 'Session not found' }));
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
701
|
+
res.end(JSON.stringify(session));
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
} else if (url.pathname.match(/^\/api\/session\/[^/]+\/end$/) && req.method === 'POST') {
|
|
705
|
+
const sessionId = url.pathname.split('/')[3];
|
|
706
|
+
readBody(req, res, (data) => {
|
|
707
|
+
if (data.filesReviewed == null) {
|
|
708
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
709
|
+
res.end(JSON.stringify({ error: 'Missing filesReviewed field' }));
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const session = analytics.endSession(sessionId, data.filesReviewed, data.avgLinesPerFile);
|
|
713
|
+
if (!session) {
|
|
714
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
715
|
+
res.end(JSON.stringify({ error: 'Session not found' }));
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
719
|
+
res.end(JSON.stringify(session));
|
|
720
|
+
});
|
|
721
|
+
|
|
441
722
|
} else if (url.pathname.startsWith('/api/session/') && req.method === 'POST') {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
req
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
} else {
|
|
457
|
-
res.writeHead(400); res.end(JSON.stringify({ error: 'Missing tool or filesReviewed' }));
|
|
458
|
-
}
|
|
459
|
-
} catch (e) {
|
|
723
|
+
// Backwards compat: old clients POST to /api/session/{id} directly
|
|
724
|
+
const sessionId = url.pathname.split('/')[3];
|
|
725
|
+
readBody(req, res, (data) => {
|
|
726
|
+
if (data.tool) {
|
|
727
|
+
const session = analytics.logToolUsage(sessionId, data.tool, data.count);
|
|
728
|
+
if (!session) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Session not found' })); return; }
|
|
729
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
730
|
+
res.end(JSON.stringify(session));
|
|
731
|
+
} else if (data.filesReviewed != null) {
|
|
732
|
+
const session = analytics.endSession(sessionId, data.filesReviewed, data.avgLinesPerFile);
|
|
733
|
+
if (!session) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Session not found' })); return; }
|
|
734
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
735
|
+
res.end(JSON.stringify(session));
|
|
736
|
+
} else {
|
|
460
737
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
461
|
-
res.end(JSON.stringify({ error:
|
|
738
|
+
res.end(JSON.stringify({ error: 'Missing tool or filesReviewed' }));
|
|
462
739
|
}
|
|
463
740
|
});
|
|
741
|
+
|
|
742
|
+
/* ── File downloads ─────────────────────────────────────────── */
|
|
464
743
|
} else if (url.pathname.startsWith('/download/')) {
|
|
465
744
|
const fname = path.basename(url.pathname);
|
|
466
745
|
const fpath = path.join(ASSETS, fname);
|
|
@@ -479,15 +758,17 @@ function start(port, noOpen) {
|
|
|
479
758
|
} else {
|
|
480
759
|
res.writeHead(404); res.end('Not found');
|
|
481
760
|
}
|
|
761
|
+
|
|
482
762
|
} else {
|
|
483
763
|
res.writeHead(404); res.end('Not found');
|
|
484
764
|
}
|
|
485
765
|
});
|
|
486
766
|
|
|
487
|
-
server.listen(availablePort, () => {
|
|
767
|
+
server.listen(availablePort, '127.0.0.1', () => {
|
|
488
768
|
const url = `http://localhost:${availablePort}`;
|
|
489
769
|
const portMsg = availablePort !== port ? ` (port ${port} was busy, using ${availablePort})` : '';
|
|
490
770
|
console.log(`\n \x1b[36mCRG Dev Kit\x1b[0m running at \x1b[1m${url}\x1b[0m${portMsg}\n`);
|
|
771
|
+
const analyticsData = analytics.getAllStats();
|
|
491
772
|
if (analyticsData) {
|
|
492
773
|
console.log(` \x1b[32m${analyticsData.avgSavingsPercent}% avg token savings\x1b[0m across \x1b[1m${analyticsData.totalSessions}\x1b[0m sessions\n`);
|
|
493
774
|
}
|