crg-dev-kit 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 +111 -0
- package/assets/CLAUDE.md +48 -0
- package/assets/README.md +72 -0
- package/assets/check-crg.sh +34 -0
- package/assets/crg-cheatsheet.pdf +0 -0
- package/assets/setup-crg.ps1 +182 -0
- package/assets/setup-crg.sh +242 -0
- package/bin/cli.js +250 -0
- package/bin/tutorial.js +198 -0
- package/lib/analytics.js +250 -0
- package/lib/roi.js +250 -0
- package/package.json +38 -0
- package/server.js +502 -0
package/server.js
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { execFile } = require('child_process');
|
|
5
|
+
const analytics = require('./lib/analytics');
|
|
6
|
+
const roi = require('./lib/roi');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_PORT = 8742;
|
|
9
|
+
const ASSETS = path.join(__dirname, 'assets');
|
|
10
|
+
|
|
11
|
+
const DOWNLOADS = [
|
|
12
|
+
{ file: 'setup-crg.sh', label: 'Linux / macOS / WSL Setup', icon: '🐧', desc: 'Bash — auto-detects OS, installs CRG, builds graph, health check' },
|
|
13
|
+
{ file: 'setup-crg.ps1', label: 'Windows Setup', icon: '🪟', desc: 'PowerShell — pip install, MCP config, graph build, health check' },
|
|
14
|
+
{ file: 'check-crg.sh', label: 'Health Check', icon: '🩺', desc: 'Quick verification — run anytime to confirm setup' },
|
|
15
|
+
{ file: 'CLAUDE.md', label: 'CLAUDE.md (AI Config)', icon: '🤖', desc: 'Drop in project root — tells AI which tools work/broken' },
|
|
16
|
+
{ file: 'crg-cheatsheet.pdf', label: 'Cheatsheet PDF', icon: '📄', desc: 'One-page visual reference — tools, workflows, known issues' },
|
|
17
|
+
];
|
|
18
|
+
|
|
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
|
+
const CRG_TOOLS = [
|
|
28
|
+
{ name: 'detect_changes', label: 'Change Detection', desc: 'Risk-scored impact analysis' },
|
|
29
|
+
{ name: 'get_review_context', label: 'Review Context', desc: 'Token-optimized source snippets' },
|
|
30
|
+
{ name: 'get_impact_radius', label: 'Impact Radius', desc: 'Blast radius of changes' },
|
|
31
|
+
{ name: 'query_graph', label: 'Graph Queries', desc: 'Callers, callees, tests, imports' },
|
|
32
|
+
{ name: 'semantic_search', label: 'Semantic Search', desc: 'Find code by meaning' },
|
|
33
|
+
{ name: 'get_architecture_overview', label: 'Architecture', desc: 'High-level structure from communities' },
|
|
34
|
+
{ name: 'list_flows', label: 'Execution Flows', desc: 'Critical call chains' },
|
|
35
|
+
{ name: 'list_communities', label: 'Code Communities', desc: 'Clustered code modules' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function esc(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
39
|
+
|
|
40
|
+
function mdToHtml(md) {
|
|
41
|
+
let lines = md.split('\n'), out = [], inCode = false;
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
if (line.trim().startsWith('```')) { out.push(inCode ? '</code></pre>' : '<pre><code>'); inCode = !inCode; continue; }
|
|
44
|
+
if (inCode) { out.push(esc(line)); continue; }
|
|
45
|
+
if (line.startsWith('# ')) { out.push(`<h1>${esc(line.slice(2))}</h1>`); continue; }
|
|
46
|
+
if (line.startsWith('## ')) { out.push(`<h2>${esc(line.slice(3))}</h2>`); continue; }
|
|
47
|
+
if (line.startsWith('### ')) { out.push(`<h3>${esc(line.slice(4))}</h3>`); continue; }
|
|
48
|
+
if (line.startsWith('- ')) { out.push(`<li>${esc(line.slice(2)).replace(/`([^`]+)`/g,'<code>$1</code>').replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')}</li>`); continue; }
|
|
49
|
+
if (line.startsWith('|') && line.includes('|')) {
|
|
50
|
+
const cells = line.split('|').slice(1,-1);
|
|
51
|
+
if (cells.every(c => /^[\s\-:]+$/.test(c))) continue;
|
|
52
|
+
const tag = out.filter(l=>l.includes('<th')).length === 0 ? 'th' : 'td';
|
|
53
|
+
out.push('<tr>' + cells.map(c => `<${tag}>${esc(c.trim()).replace(/`([^`]+)`/g,'<code>$1</code>')}</${tag}>`).join('') + '</tr>');
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (line.trim() === '') { out.push(''); continue; }
|
|
57
|
+
out.push(`<p>${esc(line).replace(/`([^`]+)`/g,'<code>$1</code>').replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')}</p>`);
|
|
58
|
+
}
|
|
59
|
+
let html = out.join('\n');
|
|
60
|
+
html = html.replace(/(<tr>.*?<\/tr>\n?)+/gs, m => `<table>${m}</table>`);
|
|
61
|
+
return html;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fileSize(f) {
|
|
65
|
+
try { const s = fs.statSync(path.join(ASSETS, f)).size; return s > 1024 ? `${(s/1024).toFixed(0)} KB` : `${s} B`; }
|
|
66
|
+
catch { return '—'; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatTokens(n) {
|
|
70
|
+
if (n >= 1000000) return `${(n/1000000).toFixed(1)}M`;
|
|
71
|
+
if (n >= 1000) return `${(n/1000).toFixed(1)}K`;
|
|
72
|
+
return `${n}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildPage(port, analyticsData) {
|
|
76
|
+
const readmePath = path.join(ASSETS, 'README.md');
|
|
77
|
+
const readmeHtml = fs.existsSync(readmePath) ? mdToHtml(fs.readFileSync(readmePath, 'utf8')) : '<p>No README found.</p>';
|
|
78
|
+
|
|
79
|
+
const dlCards = DOWNLOADS.map(d => `
|
|
80
|
+
<a href="/download/${d.file}" class="dl-card" download>
|
|
81
|
+
<span class="dl-icon">${d.icon}</span>
|
|
82
|
+
<div class="dl-info"><span class="dl-name">${d.label}</span><span class="dl-desc">${d.desc}</span></div>
|
|
83
|
+
<span class="dl-size">${fileSize(d.file)}</span>
|
|
84
|
+
<span class="dl-btn">Download</span>
|
|
85
|
+
</a>`).join('');
|
|
86
|
+
|
|
87
|
+
const stepCards = STEPS.map(s => {
|
|
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('');
|
|
96
|
+
|
|
97
|
+
const toolCards = CRG_TOOLS.map(t => `
|
|
98
|
+
<div class="tool-card">
|
|
99
|
+
<span class="tool-name">${t.name}</span>
|
|
100
|
+
<span class="tool-label">${t.label}</span>
|
|
101
|
+
<span class="tool-desc">${t.desc}</span>
|
|
102
|
+
</div>`).join('');
|
|
103
|
+
|
|
104
|
+
let analyticsSection = '';
|
|
105
|
+
let roiSection = '';
|
|
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
|
+
}
|
|
233
|
+
|
|
234
|
+
return `<!DOCTYPE html>
|
|
235
|
+
<html lang="en">
|
|
236
|
+
<head>
|
|
237
|
+
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
238
|
+
<title>code-review-graph Dev Kit</title>
|
|
239
|
+
<style>
|
|
240
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
|
|
241
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
242
|
+
:root{--bg:#fafaf9;--s:#fff;--b:#e7e5e4;--t:#1c1917;--t2:#44403c;--t3:#78716c;--t4:#a8a29e;--a:#ea580c;--al:#fff7ed;--ab:#fed7aa;--m:'JetBrains Mono',monospace}
|
|
243
|
+
body{font-family:'Inter',-apple-system,sans-serif;background:var(--bg);color:var(--t);line-height:1.6;-webkit-font-smoothing:antialiased}
|
|
244
|
+
nav{background:var(--s);border-bottom:1px solid var(--b);padding:0 40px;display:flex;align-items:center;height:56px;position:sticky;top:0;z-index:100}
|
|
245
|
+
.nb{display:flex;align-items:center;gap:10px;font-weight:800;font-size:15px;letter-spacing:-0.02em}
|
|
246
|
+
.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
|
+
.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}
|
|
249
|
+
.nk a:hover{color:var(--a)}
|
|
250
|
+
.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
|
+
.hero{text-align:center;padding:64px 40px 48px;max-width:680px;margin:0 auto}
|
|
252
|
+
.hero h1{font-size:36px;font-weight:800;letter-spacing:-0.03em;line-height:1.15;margin-bottom:12px}
|
|
253
|
+
.hero h1 span{color:var(--a)}
|
|
254
|
+
.hero p{font-size:16px;color:var(--t3);max-width:520px;margin:0 auto}
|
|
255
|
+
.hs{display:flex;justify-content:center;gap:32px;margin-top:28px}
|
|
256
|
+
.sv{font-size:24px;font-weight:800;letter-spacing:-0.02em}
|
|
257
|
+
.sl{font-size:11px;color:var(--t4);text-transform:uppercase;letter-spacing:.06em;font-weight:600;margin-top:2px}
|
|
258
|
+
section{max-width:780px;margin:0 auto;padding:0 40px 48px}
|
|
259
|
+
.st{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:var(--a);margin-bottom:16px;padding-bottom:8px;border-bottom:2px solid var(--a);display:inline-block}
|
|
260
|
+
.step{display:flex;gap:16px;padding:20px 0;border-bottom:1px solid var(--b)}.step:last-child{border-bottom:none}
|
|
261
|
+
.step-num{width:32px;height:32px;background:var(--a);color:#fff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:14px;flex-shrink:0}
|
|
262
|
+
.step-body h3{font-size:15px;font-weight:700;margin-bottom:4px}
|
|
263
|
+
.step-desc{font-size:13px;color:var(--t3)}
|
|
264
|
+
.step-cmds{display:flex;gap:12px;margin-top:8px;flex-wrap:wrap}
|
|
265
|
+
.cmd-block{background:#1c1917;border-radius:6px;padding:8px 12px;flex:1;min-width:240px}
|
|
266
|
+
.cmd-label{font-size:10px;font-weight:600;color:#a8a29e;text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:4px}
|
|
267
|
+
.cmd-block code{font-family:var(--m);font-size:12px;color:#fafaf9}
|
|
268
|
+
.dl-grid{display:flex;flex-direction:column;gap:8px}
|
|
269
|
+
.dl-card{display:flex;align-items:center;gap:12px;padding:14px 16px;background:var(--s);border:1px solid var(--b);border-radius:8px;text-decoration:none;color:var(--t);transition:border-color .15s,box-shadow .15s}
|
|
270
|
+
.dl-card:hover{border-color:var(--a);box-shadow:0 2px 8px rgba(234,88,12,.08)}
|
|
271
|
+
.dl-icon{font-size:22px;flex-shrink:0}.dl-info{flex:1}
|
|
272
|
+
.dl-name{font-weight:700;font-size:14px;display:block}
|
|
273
|
+
.dl-desc{font-size:12px;color:var(--t3);display:block;margin-top:1px}
|
|
274
|
+
.dl-size{font-family:var(--m);font-size:11px;color:var(--t4);flex-shrink:0}
|
|
275
|
+
.dl-btn{background:var(--a);color:#fff;padding:6px 14px;border-radius:5px;font-size:12px;font-weight:700;flex-shrink:0;transition:background .15s}
|
|
276
|
+
.dl-card:hover .dl-btn{background:#c2410c}
|
|
277
|
+
.rc{background:var(--s);border:1px solid var(--b);border-radius:8px;padding:32px}
|
|
278
|
+
.rc h1{font-size:22px;font-weight:800;margin-bottom:16px;letter-spacing:-0.02em}
|
|
279
|
+
.rc h2{font-size:17px;font-weight:700;margin:24px 0 8px}
|
|
280
|
+
.rc h3{font-size:14px;font-weight:700;margin:16px 0 6px}
|
|
281
|
+
.rc p{font-size:14px;color:var(--t2);margin-bottom:8px}
|
|
282
|
+
.rc code{font-family:var(--m);font-size:12px;background:var(--bg);padding:1px 5px;border-radius:3px;border:1px solid var(--b)}
|
|
283
|
+
.rc pre{background:#1c1917;color:#fafaf9;padding:14px 16px;border-radius:6px;overflow-x:auto;margin:8px 0 16px;font-size:12px;line-height:1.5}
|
|
284
|
+
.rc pre code{background:none;border:none;padding:0;color:inherit}
|
|
285
|
+
.rc table{width:100%;border-collapse:collapse;margin:8px 0 16px;font-size:13px}
|
|
286
|
+
.rc th{text-align:left;font-weight:700;padding:8px 12px;border-bottom:2px solid var(--b);font-size:12px;color:var(--t3);text-transform:uppercase;letter-spacing:.04em}
|
|
287
|
+
.rc td{padding:8px 12px;border-bottom:1px solid var(--b);color:var(--t2)}
|
|
288
|
+
.rc li{font-size:14px;color:var(--t2);margin-bottom:4px;margin-left:20px}
|
|
289
|
+
.rc a{color:var(--a)}
|
|
290
|
+
footer{text-align:center;padding:32px 40px;font-size:12px;color:var(--t4);border-top:1px solid var(--b);margin-top:24px}
|
|
291
|
+
footer a{color:var(--a);text-decoration:none;font-weight:600}
|
|
292
|
+
.npx-banner{background:#1c1917;border-radius:8px;padding:16px 20px;margin-bottom:24px;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
|
|
293
|
+
.npx-banner code{font-family:var(--m);font-size:15px;color:#a6e3a1;font-weight:600}
|
|
294
|
+
.npx-banner .dim{color:#a8a29e;font-size:12px}
|
|
295
|
+
.npx-banner .alt{font-family:var(--m);font-size:13px;color:#cdd6f4}
|
|
296
|
+
.analytics-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:32px}
|
|
297
|
+
.roi-banner{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px;flex-wrap:wrap;gap:12px}
|
|
298
|
+
.roi-verdict{display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:8px;border:2px solid;font-weight:600;font-size:14px}
|
|
299
|
+
.roi-verdict-icon{font-size:18px}
|
|
300
|
+
.roi-verdict-text{color:inherit}
|
|
301
|
+
.roi-installed{font-size:13px;color:var(--t3)}
|
|
302
|
+
.roi-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:24px}
|
|
303
|
+
.roi-card{background:var(--s);border:1px solid var(--b);border-radius:8px;padding:20px;text-align:center}
|
|
304
|
+
.roi-value{font-size:28px;font-weight:800;letter-spacing:-0.02em}
|
|
305
|
+
.roi-label{font-size:11px;color:var(--t3);margin-top:4px;font-weight:600;text-transform:uppercase;letter-spacing:.05em}
|
|
306
|
+
.roi-compare{display:flex;align-items:center;gap:24px;background:var(--s);border:1px solid var(--b);border-radius:8px;padding:24px;margin-bottom:16px}
|
|
307
|
+
.roi-compare-col{flex:1}
|
|
308
|
+
.roi-compare-col h4{font-size:12px;color:var(--t4);text-transform:uppercase;letter-spacing:.06em;margin-bottom:12px}
|
|
309
|
+
.roi-compare-stat{display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--b)}
|
|
310
|
+
.roi-compare-stat:last-child{border-bottom:none}
|
|
311
|
+
.roi-compare-label{font-size:13px;color:var(--t3)}
|
|
312
|
+
.roi-compare-value{font-family:var(--m);font-size:13px;font-weight:600}
|
|
313
|
+
.roi-arrow{font-size:24px;color:var(--t4)}
|
|
314
|
+
.roi-cta{text-align:center;font-size:13px;color:var(--t3)}
|
|
315
|
+
.roi-cta code{background:var(--bg);padding:4px 8px;border-radius:4px;font-size:12px}
|
|
316
|
+
.stat-card{background:var(--s);border:1px solid var(--b);border-radius:8px;padding:24px;text-align:center}
|
|
317
|
+
.stat-card.primary{border-color:var(--a);background:var(--al)}
|
|
318
|
+
.stat-value{font-size:32px;font-weight:800;letter-spacing:-0.03em}
|
|
319
|
+
.stat-label{font-size:12px;color:var(--t3);margin-top:4px;font-weight:600}
|
|
320
|
+
.stat-sub{font-size:11px;color:var(--t4);margin-top:2px}
|
|
321
|
+
.project-table{margin-bottom:24px}
|
|
322
|
+
.project-table table{width:100%;border-collapse:collapse;background:var(--s);border:1px solid var(--b);border-radius:8px;overflow:hidden}
|
|
323
|
+
.project-table th{text-align:left;padding:12px 16px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--t4);background:var(--bg);border-bottom:1px solid var(--b)}
|
|
324
|
+
.project-table td{padding:12px 16px;font-size:13px;color:var(--t2);border-bottom:1px solid var(--b)}
|
|
325
|
+
.project-table tr:last-child td{border-bottom:none}
|
|
326
|
+
.project-table code{font-family:var(--m);font-size:12px;background:var(--bg);padding:2px 6px;border-radius:3px}
|
|
327
|
+
.savings-badge{font-weight:700;font-size:13px}
|
|
328
|
+
.tool-usage-section{background:var(--s);border:1px solid var(--b);border-radius:8px;padding:24px}
|
|
329
|
+
.tool-usage-section h3{font-size:14px;font-weight:700;margin-bottom:16px}
|
|
330
|
+
.tool-bars{display:flex;flex-direction:column;gap:12px}
|
|
331
|
+
.tool-bar{display:flex;align-items:center;gap:12px}
|
|
332
|
+
.tool-bar-name{font-family:var(--m);font-size:11px;color:var(--t3);width:180px;flex-shrink:0;text-align:right}
|
|
333
|
+
.tool-bar-track{flex:1;height:8px;background:var(--bg);border-radius:4px;overflow:hidden}
|
|
334
|
+
.tool-bar-fill{height:100%;background:var(--a);border-radius:4px;transition:width .3s}
|
|
335
|
+
.tool-bar-count{font-family:var(--m);font-size:11px;color:var(--t4);width:30px;flex-shrink:0}
|
|
336
|
+
.tools-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
|
|
337
|
+
.tool-card{background:var(--s);border:1px solid var(--b);border-radius:6px;padding:12px;display:flex;flex-direction:column;gap:4px}
|
|
338
|
+
.tool-name{font-family:var(--m);font-size:11px;color:var(--a);font-weight:600}
|
|
339
|
+
.tool-label{font-size:12px;font-weight:700;color:var(--t)}
|
|
340
|
+
.tool-desc{font-size:11px;color:var(--t3)}
|
|
341
|
+
@media(max-width:768px){
|
|
342
|
+
.analytics-grid{grid-template-columns:1fr}
|
|
343
|
+
.tools-grid{grid-template-columns:repeat(2,1fr)}
|
|
344
|
+
nav{padding:0 20px}
|
|
345
|
+
.hero{padding:48px 20px 32px}
|
|
346
|
+
section{padding:0 20px 32px}
|
|
347
|
+
.hs{gap:20px}
|
|
348
|
+
}
|
|
349
|
+
</style>
|
|
350
|
+
</head>
|
|
351
|
+
<body>
|
|
352
|
+
<nav><div class="nb"><div class="nl">G</div>code-review-graph</div><div class="nk"><a href="#steps">Setup</a><a href="#roi">ROI</a><a href="#analytics">Analytics</a><a href="#tools">Tools</a><a href="#downloads">Downloads</a><a href="#docs">Docs</a><span class="badge">v2.1.0</span></div></nav>
|
|
353
|
+
<div class="hero"><h1>Ship code with a <span>knowledge graph</span></h1><p>One command to set up AI-powered code review, impact analysis, and codebase navigation. Track token savings across your team.</p><div class="hs"><div><div class="sv">22</div><div class="sl">MCP Tools</div></div><div><div class="sv">19</div><div class="sl">Languages</div></div><div><div class="sv">8.2x</div><div class="sl">Token Reduction</div></div><div><div class="sv">0</div><div class="sl">Telemetry</div></div></div></div>
|
|
354
|
+
<section id="steps"><div class="st">Integration Steps</div><div class="npx-banner"><code>npx crg-dev-kit</code><span class="dim">opens this page</span><span style="color:#a8a29e">|</span><span class="alt">npx crg-dev-kit install</span><span class="dim">copies scripts to your project</span></div><div class="steps">${stepCards}</div></section>
|
|
355
|
+
${roiSection}
|
|
356
|
+
${analyticsSection}
|
|
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>
|
|
360
|
+
<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
|
+
</body></html>`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function openBrowser(url) {
|
|
365
|
+
const plat = process.platform;
|
|
366
|
+
if (plat === 'win32') execFile('cmd', ['/c', 'start', url]);
|
|
367
|
+
else if (plat === 'darwin') execFile('open', [url]);
|
|
368
|
+
else execFile('xdg-open', [url]);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function findAvailablePort(startPort, maxAttempts = 10) {
|
|
372
|
+
return new Promise((resolve, reject) => {
|
|
373
|
+
const tryPort = (port, attempt) => {
|
|
374
|
+
if (attempt > maxAttempts) {
|
|
375
|
+
reject(new Error(`No available port found after ${maxAttempts} attempts`));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const server = http.createServer();
|
|
379
|
+
server.on('error', (err) => {
|
|
380
|
+
if (err.code === 'EADDRINUSE') {
|
|
381
|
+
server.close();
|
|
382
|
+
tryPort(port + 1, attempt + 1);
|
|
383
|
+
} else {
|
|
384
|
+
reject(err);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
server.on('listening', () => {
|
|
388
|
+
server.close();
|
|
389
|
+
resolve(port);
|
|
390
|
+
});
|
|
391
|
+
server.listen(port);
|
|
392
|
+
};
|
|
393
|
+
tryPort(startPort, 1);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function start(port, noOpen) {
|
|
398
|
+
port = port || DEFAULT_PORT;
|
|
399
|
+
noOpen = noOpen || false;
|
|
400
|
+
|
|
401
|
+
findAvailablePort(port).then(availablePort => {
|
|
402
|
+
const analyticsData = analytics.getAllStats();
|
|
403
|
+
|
|
404
|
+
const server = http.createServer((req, res) => {
|
|
405
|
+
const url = new URL(req.url, `http://localhost:${availablePort}`);
|
|
406
|
+
|
|
407
|
+
if (url.pathname === '/' || url.pathname === '') {
|
|
408
|
+
const html = buildPage(availablePort, analyticsData);
|
|
409
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
410
|
+
res.end(html);
|
|
411
|
+
} else if (url.pathname === '/api/analytics') {
|
|
412
|
+
const data = analytics.getAllStats();
|
|
413
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
414
|
+
res.end(JSON.stringify(data || { message: 'No analytics data yet' }));
|
|
415
|
+
} else if (url.pathname === '/api/roi') {
|
|
416
|
+
const data = roi.getAllProjectsROI();
|
|
417
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
418
|
+
res.end(JSON.stringify(data));
|
|
419
|
+
} else if (url.pathname === '/api/report') {
|
|
420
|
+
const report = analytics.generateReport();
|
|
421
|
+
res.writeHead(200, { 'Content-Type': 'text/markdown' });
|
|
422
|
+
res.end(report);
|
|
423
|
+
} else if (url.pathname === '/api/session') {
|
|
424
|
+
if (req.method === 'POST') {
|
|
425
|
+
let body = '';
|
|
426
|
+
req.on('data', chunk => { body += chunk; });
|
|
427
|
+
req.on('end', () => {
|
|
428
|
+
try {
|
|
429
|
+
const data = JSON.parse(body);
|
|
430
|
+
const session = analytics.createSession(data.project, data.operation);
|
|
431
|
+
res.writeHead(201, { 'Content-Type': 'application/json' });
|
|
432
|
+
res.end(JSON.stringify(session));
|
|
433
|
+
} catch (e) {
|
|
434
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
435
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
} else {
|
|
439
|
+
res.writeHead(405); res.end('Method not allowed');
|
|
440
|
+
}
|
|
441
|
+
} else if (url.pathname.startsWith('/api/session/') && req.method === 'POST') {
|
|
442
|
+
const sessionId = url.pathname.split('/').pop();
|
|
443
|
+
let body = '';
|
|
444
|
+
req.on('data', chunk => { body += chunk; });
|
|
445
|
+
req.on('end', () => {
|
|
446
|
+
try {
|
|
447
|
+
const data = JSON.parse(body);
|
|
448
|
+
if (data.tool) {
|
|
449
|
+
const session = analytics.logToolUsage(sessionId, data.tool, data.count);
|
|
450
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
451
|
+
res.end(JSON.stringify(session));
|
|
452
|
+
} else if (data.filesReviewed) {
|
|
453
|
+
const session = analytics.endSession(sessionId, data.filesReviewed, data.avgLinesPerFile);
|
|
454
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
455
|
+
res.end(JSON.stringify(session));
|
|
456
|
+
} else {
|
|
457
|
+
res.writeHead(400); res.end(JSON.stringify({ error: 'Missing tool or filesReviewed' }));
|
|
458
|
+
}
|
|
459
|
+
} catch (e) {
|
|
460
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
461
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
} else if (url.pathname.startsWith('/download/')) {
|
|
465
|
+
const fname = path.basename(url.pathname);
|
|
466
|
+
const fpath = path.join(ASSETS, fname);
|
|
467
|
+
if (!path.resolve(fpath).startsWith(path.resolve(ASSETS))) {
|
|
468
|
+
res.writeHead(403); res.end('Forbidden'); return;
|
|
469
|
+
}
|
|
470
|
+
if (fs.existsSync(fpath)) {
|
|
471
|
+
const ct = fname.endsWith('.pdf') ? 'application/pdf' : 'application/octet-stream';
|
|
472
|
+
const data = fs.readFileSync(fpath);
|
|
473
|
+
res.writeHead(200, {
|
|
474
|
+
'Content-Type': ct,
|
|
475
|
+
'Content-Disposition': `attachment; filename="${fname}"`,
|
|
476
|
+
'Content-Length': data.length
|
|
477
|
+
});
|
|
478
|
+
res.end(data);
|
|
479
|
+
} else {
|
|
480
|
+
res.writeHead(404); res.end('Not found');
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
res.writeHead(404); res.end('Not found');
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
server.listen(availablePort, () => {
|
|
488
|
+
const url = `http://localhost:${availablePort}`;
|
|
489
|
+
const portMsg = availablePort !== port ? ` (port ${port} was busy, using ${availablePort})` : '';
|
|
490
|
+
console.log(`\n \x1b[36mCRG Dev Kit\x1b[0m running at \x1b[1m${url}\x1b[0m${portMsg}\n`);
|
|
491
|
+
if (analyticsData) {
|
|
492
|
+
console.log(` \x1b[32m${analyticsData.avgSavingsPercent}% avg token savings\x1b[0m across \x1b[1m${analyticsData.totalSessions}\x1b[0m sessions\n`);
|
|
493
|
+
}
|
|
494
|
+
if (!noOpen) openBrowser(url);
|
|
495
|
+
});
|
|
496
|
+
}).catch(err => {
|
|
497
|
+
console.error(`\x1b[31mError: ${err.message}\x1b[0m`);
|
|
498
|
+
process.exit(1);
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
module.exports = { start };
|