cc-starter 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/LICENSE +21 -0
- package/README.md +149 -0
- package/bin/cc-starter.js +55 -0
- package/lib/constants.js +32 -0
- package/lib/detect.js +109 -0
- package/lib/plugins.js +74 -0
- package/lib/scaffold.js +261 -0
- package/lib/wizard.js +99 -0
- package/package.json +30 -0
- package/template/CLAUDE.md.hbs +44 -0
- package/template/claude/commands/kickstart.md +16 -0
- package/template/claude/memory/MEMORY.md +11 -0
- package/template/claude/project/README.md +7 -0
- package/template/claude/reference/README.md +7 -0
- package/template/claude/rules/01-general.md +36 -0
- package/template/claude/rules/02-code-standards.md +23 -0
- package/template/claude/rules/03-dev-ops.md +20 -0
- package/template/claude/settings.json +4 -0
- package/template/scripts/stats/cocomo.js +178 -0
- package/template/scripts/stats/project-report.js +640 -0
- package/template/scripts/stats/vibe-code.js +533 -0
- package/template/scripts/stats/vibe-stats.js +249 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cc-starter Project Report — generates a visual HTML report of project statistics.
|
|
3
|
+
// Two modes: "minimal" (zero deps, CSS bar charts) and "fancy" (Chart.js via CDN).
|
|
4
|
+
// CommonJS, zero npm dependencies.
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const { execSync } = require("child_process");
|
|
9
|
+
|
|
10
|
+
// ── Configuration (shared with cocomo.js) ────────────────────
|
|
11
|
+
|
|
12
|
+
const SKIP_DIRS = new Set([
|
|
13
|
+
"node_modules", ".git", ".next", "dist", "build",
|
|
14
|
+
".cache", "__pycache__", ".venv", "target", "vendor", ".claude",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const SKIP_FILES = new Set([
|
|
18
|
+
"package-lock.json", "yarn.lock", "pnpm-lock.yaml",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const EXT_MAP = {
|
|
22
|
+
".ts": "TypeScript",
|
|
23
|
+
".tsx": "TypeScript",
|
|
24
|
+
".js": "JavaScript",
|
|
25
|
+
".jsx": "JavaScript",
|
|
26
|
+
".py": "Python",
|
|
27
|
+
".go": "Go",
|
|
28
|
+
".rs": "Rust",
|
|
29
|
+
".java": "Java",
|
|
30
|
+
".cs": "C#",
|
|
31
|
+
".rb": "Ruby",
|
|
32
|
+
".php": "PHP",
|
|
33
|
+
".css": "CSS",
|
|
34
|
+
".scss": "CSS",
|
|
35
|
+
".html": "HTML",
|
|
36
|
+
".json": "JSON",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const HOURS_PER_MONTH = 168;
|
|
40
|
+
const DEFAULT_HOURLY_RATE = 80;
|
|
41
|
+
|
|
42
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function readConfig(dir) {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(fs.readFileSync(path.join(dir, ".cc-starter.json"), "utf-8"));
|
|
47
|
+
} catch {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function countLines(filePath) {
|
|
53
|
+
const buf = fs.readFileSync(filePath);
|
|
54
|
+
if (buf.length === 0) return 0;
|
|
55
|
+
let count = 1;
|
|
56
|
+
for (let i = 0; i < buf.length; i++) {
|
|
57
|
+
if (buf[i] === 0x0a) count++;
|
|
58
|
+
}
|
|
59
|
+
if (buf[buf.length - 1] === 0x0a) count--;
|
|
60
|
+
return count;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function fmt(n) {
|
|
64
|
+
return Number(n).toLocaleString("en-US");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Data Collection ──────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
function collectData(root) {
|
|
70
|
+
const langCounts = {};
|
|
71
|
+
const folderCounts = {};
|
|
72
|
+
const fileList = []; // { path, lines, lang }
|
|
73
|
+
|
|
74
|
+
function walk(dir) {
|
|
75
|
+
let entries;
|
|
76
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
77
|
+
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
const name = entry.name;
|
|
80
|
+
if (entry.isDirectory()) {
|
|
81
|
+
if (!SKIP_DIRS.has(name)) walk(path.join(dir, name));
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!entry.isFile()) continue;
|
|
85
|
+
if (name.endsWith(".min.js") || name.endsWith(".min.css")) continue;
|
|
86
|
+
if (SKIP_FILES.has(name)) continue;
|
|
87
|
+
|
|
88
|
+
const ext = path.extname(name).toLowerCase();
|
|
89
|
+
const lang = EXT_MAP[ext];
|
|
90
|
+
if (!lang) continue;
|
|
91
|
+
|
|
92
|
+
const fullPath = path.join(dir, name);
|
|
93
|
+
const lines = countLines(fullPath);
|
|
94
|
+
|
|
95
|
+
if (lang === "JSON" && lines >= 1000) continue;
|
|
96
|
+
|
|
97
|
+
langCounts[lang] = (langCounts[lang] || 0) + lines;
|
|
98
|
+
fileList.push({ path: path.relative(root, fullPath), lines, lang });
|
|
99
|
+
|
|
100
|
+
// Top-level folder
|
|
101
|
+
const rel = path.relative(root, fullPath);
|
|
102
|
+
const topFolder = rel.includes(path.sep) ? rel.split(path.sep)[0] : "(root)";
|
|
103
|
+
folderCounts[topFolder] = (folderCounts[topFolder] || 0) + lines;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
walk(root);
|
|
108
|
+
|
|
109
|
+
// Sort
|
|
110
|
+
const languages = Object.entries(langCounts).sort((a, b) => b[1] - a[1]);
|
|
111
|
+
const folders = Object.entries(folderCounts).sort((a, b) => b[1] - a[1]);
|
|
112
|
+
const largestFiles = fileList.sort((a, b) => b.lines - a.lines).slice(0, 10);
|
|
113
|
+
const totalLines = languages.reduce((s, [, c]) => s + c, 0);
|
|
114
|
+
|
|
115
|
+
return { languages, folders, largestFiles, totalLines };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function cocomoEstimate(totalLines, hourlyRate) {
|
|
119
|
+
const kloc = totalLines / 1000;
|
|
120
|
+
const effort = 3.0 * Math.pow(kloc, 1.12);
|
|
121
|
+
const schedule = 2.5 * Math.pow(effort, 0.35);
|
|
122
|
+
const teamSize = effort / schedule;
|
|
123
|
+
const cost = effort * HOURS_PER_MONTH * hourlyRate;
|
|
124
|
+
return { kloc, effort, schedule, teamSize, cost };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function loadVibeStats(root) {
|
|
128
|
+
try {
|
|
129
|
+
const data = JSON.parse(fs.readFileSync(path.join(root, ".vibe-stats.json"), "utf-8"));
|
|
130
|
+
if (data.operationsCount > 0) return data;
|
|
131
|
+
} catch {}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getGitStats(root) {
|
|
136
|
+
try {
|
|
137
|
+
const opts = { cwd: root, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] };
|
|
138
|
+
const totalCommits = parseInt(execSync("git rev-list --count HEAD", opts).trim(), 10);
|
|
139
|
+
const lastCommitDate = execSync('git log -1 --format=%ci', opts).trim();
|
|
140
|
+
// Active days: unique dates of commits
|
|
141
|
+
const logOutput = execSync('git log --format=%cd --date=short', opts).trim();
|
|
142
|
+
const uniqueDays = new Set(logOutput.split("\n").filter(Boolean));
|
|
143
|
+
const firstCommitDate = execSync('git log --reverse --format=%cd --date=short -1', opts).trim();
|
|
144
|
+
return {
|
|
145
|
+
totalCommits,
|
|
146
|
+
lastCommitDate: lastCommitDate.slice(0, 10),
|
|
147
|
+
firstCommitDate,
|
|
148
|
+
activeDays: uniqueDays.size,
|
|
149
|
+
};
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Escape HTML ──────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
function esc(s) {
|
|
158
|
+
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Color palette ────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
const PALETTE = [
|
|
164
|
+
"#58a6ff", // blue
|
|
165
|
+
"#3fb950", // green
|
|
166
|
+
"#bc8cff", // purple
|
|
167
|
+
"#79c0ff", // light blue
|
|
168
|
+
"#d2a8ff", // lavender
|
|
169
|
+
"#56d4dd", // teal
|
|
170
|
+
"#f778ba", // pink
|
|
171
|
+
"#ffa657", // orange
|
|
172
|
+
"#ff7b72", // red
|
|
173
|
+
"#e3b341", // gold
|
|
174
|
+
"#7ee787", // lime
|
|
175
|
+
"#a5d6ff", // sky
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
function colorFor(i) {
|
|
179
|
+
return PALETTE[i % PALETTE.length];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── HTML Generation ──────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
function generateMinimal(projectName, data, cocomo, vibeStats, gitStats) {
|
|
185
|
+
const { languages, folders, largestFiles, totalLines } = data;
|
|
186
|
+
const genDate = new Date().toISOString().slice(0, 10);
|
|
187
|
+
const maxLangLines = languages.length > 0 ? languages[0][1] : 1;
|
|
188
|
+
const maxFolderLines = folders.length > 0 ? folders[0][1] : 1;
|
|
189
|
+
|
|
190
|
+
let html = `<!DOCTYPE html>
|
|
191
|
+
<html lang="en">
|
|
192
|
+
<head>
|
|
193
|
+
<meta charset="UTF-8">
|
|
194
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
195
|
+
<title>Project Report — ${esc(projectName)}</title>
|
|
196
|
+
<style>
|
|
197
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
198
|
+
body {
|
|
199
|
+
background: #0d1117; color: #e6edf3;
|
|
200
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
201
|
+
line-height: 1.6; padding: 2rem 1rem;
|
|
202
|
+
}
|
|
203
|
+
.container { max-width: 860px; margin: 0 auto; }
|
|
204
|
+
h1 { font-size: 1.75rem; font-weight: 600; margin-bottom: 0.25rem; }
|
|
205
|
+
.subtitle { color: #8b949e; font-size: 0.9rem; margin-bottom: 2rem; }
|
|
206
|
+
.section { margin-bottom: 2.5rem; }
|
|
207
|
+
.section-title {
|
|
208
|
+
font-size: 1.1rem; font-weight: 600; margin-bottom: 1rem;
|
|
209
|
+
padding-bottom: 0.5rem; border-bottom: 1px solid #21262d;
|
|
210
|
+
}
|
|
211
|
+
.bar-row { display: flex; align-items: center; margin-bottom: 0.5rem; gap: 0.75rem; }
|
|
212
|
+
.bar-label { width: 120px; flex-shrink: 0; font-size: 0.85rem; text-align: right; color: #c9d1d9; }
|
|
213
|
+
.bar-track { flex: 1; height: 22px; background: #161b22; border-radius: 4px; overflow: hidden; }
|
|
214
|
+
.bar { height: 100%; border-radius: 4px; min-width: 2px; transition: width 0.3s; }
|
|
215
|
+
.bar-value { width: 90px; flex-shrink: 0; font-size: 0.8rem; color: #8b949e; text-align: right; font-variant-numeric: tabular-nums; }
|
|
216
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; }
|
|
217
|
+
.card {
|
|
218
|
+
background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 1.25rem;
|
|
219
|
+
}
|
|
220
|
+
.card-title { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; color: #8b949e; margin-bottom: 0.5rem; }
|
|
221
|
+
.card-value { font-size: 1.5rem; font-weight: 600; color: #58a6ff; }
|
|
222
|
+
.card-detail { font-size: 0.8rem; color: #8b949e; margin-top: 0.25rem; }
|
|
223
|
+
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
|
224
|
+
th { text-align: left; color: #8b949e; font-weight: 500; padding: 0.5rem 0.75rem; border-bottom: 1px solid #21262d; }
|
|
225
|
+
td { padding: 0.5rem 0.75rem; border-bottom: 1px solid #161b22; }
|
|
226
|
+
tr:hover td { background: #161b22; }
|
|
227
|
+
.mono { font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 0.8rem; }
|
|
228
|
+
.text-right { text-align: right; }
|
|
229
|
+
.footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid #21262d; font-size: 0.75rem; color: #484f58; text-align: center; }
|
|
230
|
+
</style>
|
|
231
|
+
</head>
|
|
232
|
+
<body>
|
|
233
|
+
<div class="container">
|
|
234
|
+
<h1>${esc(projectName)}</h1>
|
|
235
|
+
<div class="subtitle">Project Report · Generated ${esc(genDate)} · ${fmt(totalLines)} total lines of code</div>
|
|
236
|
+
|
|
237
|
+
<div class="section">
|
|
238
|
+
<div class="section-title">Lines of Code by Language</div>
|
|
239
|
+
`;
|
|
240
|
+
|
|
241
|
+
for (let i = 0; i < languages.length; i++) {
|
|
242
|
+
const [lang, count] = languages[i];
|
|
243
|
+
const pct = Math.max(1, Math.round((count / maxLangLines) * 100));
|
|
244
|
+
html += ` <div class="bar-row">
|
|
245
|
+
<span class="bar-label">${esc(lang)}</span>
|
|
246
|
+
<div class="bar-track"><div class="bar" style="width:${pct}%;background:${colorFor(i)}"></div></div>
|
|
247
|
+
<span class="bar-value">${fmt(count)}</span>
|
|
248
|
+
</div>\n`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
html += ` </div>
|
|
252
|
+
|
|
253
|
+
<div class="section">
|
|
254
|
+
<div class="section-title">Lines of Code by Folder</div>
|
|
255
|
+
`;
|
|
256
|
+
|
|
257
|
+
const foldersToShow = folders.slice(0, 15);
|
|
258
|
+
for (let i = 0; i < foldersToShow.length; i++) {
|
|
259
|
+
const [folder, count] = foldersToShow[i];
|
|
260
|
+
const pct = Math.max(1, Math.round((count / maxFolderLines) * 100));
|
|
261
|
+
html += ` <div class="bar-row">
|
|
262
|
+
<span class="bar-label">${esc(folder)}/</span>
|
|
263
|
+
<div class="bar-track"><div class="bar" style="width:${pct}%;background:${colorFor(i + 3)}"></div></div>
|
|
264
|
+
<span class="bar-value">${fmt(count)}</span>
|
|
265
|
+
</div>\n`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
html += ` </div>
|
|
269
|
+
|
|
270
|
+
<div class="section">
|
|
271
|
+
<div class="section-title">Largest Files</div>
|
|
272
|
+
<table>
|
|
273
|
+
<thead><tr><th>#</th><th>File</th><th>Language</th><th class="text-right">Lines</th></tr></thead>
|
|
274
|
+
<tbody>
|
|
275
|
+
`;
|
|
276
|
+
|
|
277
|
+
for (let i = 0; i < largestFiles.length; i++) {
|
|
278
|
+
const f = largestFiles[i];
|
|
279
|
+
html += ` <tr><td>${i + 1}</td><td class="mono">${esc(f.path.replace(/\\/g, "/"))}</td><td>${esc(f.lang)}</td><td class="text-right">${fmt(f.lines)}</td></tr>\n`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
html += ` </tbody>
|
|
283
|
+
</table>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<div class="section">
|
|
287
|
+
<div class="section-title">Estimation</div>
|
|
288
|
+
<div class="cards">
|
|
289
|
+
<div class="card">
|
|
290
|
+
<div class="card-title">COCOMO-II Effort</div>
|
|
291
|
+
<div class="card-value">${cocomo.effort.toFixed(1)} PM</div>
|
|
292
|
+
<div class="card-detail">Person-Months (Semi-Detached)</div>
|
|
293
|
+
</div>
|
|
294
|
+
<div class="card">
|
|
295
|
+
<div class="card-title">Schedule</div>
|
|
296
|
+
<div class="card-value">${cocomo.schedule.toFixed(1)} mo</div>
|
|
297
|
+
<div class="card-detail">${cocomo.teamSize.toFixed(1)} developers</div>
|
|
298
|
+
</div>
|
|
299
|
+
<div class="card">
|
|
300
|
+
<div class="card-title">Estimated Cost</div>
|
|
301
|
+
<div class="card-value">€${fmt(Math.round(cocomo.cost))}</div>
|
|
302
|
+
<div class="card-detail">${fmt(cocomo.kloc.toFixed(1))} KLOC at €${fmt(cocomo.hourlyRate)}/h</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
`;
|
|
307
|
+
|
|
308
|
+
if (vibeStats) {
|
|
309
|
+
html += `
|
|
310
|
+
<div class="section">
|
|
311
|
+
<div class="section-title">Token Savings</div>
|
|
312
|
+
<div class="cards">
|
|
313
|
+
<div class="card">
|
|
314
|
+
<div class="card-title">Tokens Saved</div>
|
|
315
|
+
<div class="card-value">${fmt(vibeStats.totalSaved)}</div>
|
|
316
|
+
<div class="card-detail">across ${fmt(vibeStats.operationsCount)} operations</div>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (gitStats) {
|
|
324
|
+
html += `
|
|
325
|
+
<div class="section">
|
|
326
|
+
<div class="section-title">Git Statistics</div>
|
|
327
|
+
<div class="cards">
|
|
328
|
+
<div class="card">
|
|
329
|
+
<div class="card-title">Total Commits</div>
|
|
330
|
+
<div class="card-value">${fmt(gitStats.totalCommits)}</div>
|
|
331
|
+
<div class="card-detail">since ${esc(gitStats.firstCommitDate)}</div>
|
|
332
|
+
</div>
|
|
333
|
+
<div class="card">
|
|
334
|
+
<div class="card-title">Active Days</div>
|
|
335
|
+
<div class="card-value">${fmt(gitStats.activeDays)}</div>
|
|
336
|
+
<div class="card-detail">days with at least one commit</div>
|
|
337
|
+
</div>
|
|
338
|
+
<div class="card">
|
|
339
|
+
<div class="card-title">Last Commit</div>
|
|
340
|
+
<div class="card-value">${esc(gitStats.lastCommitDate)}</div>
|
|
341
|
+
<div class="card-detail"> </div>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
html += `
|
|
349
|
+
<div class="footer">Generated by cc-starter · project-report.js</div>
|
|
350
|
+
</div>
|
|
351
|
+
</body>
|
|
352
|
+
</html>`;
|
|
353
|
+
|
|
354
|
+
return html;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function generateFancy(projectName, data, cocomo, vibeStats, gitStats) {
|
|
358
|
+
const { languages, folders, largestFiles, totalLines } = data;
|
|
359
|
+
const genDate = new Date().toISOString().slice(0, 10);
|
|
360
|
+
const maxFolderLines = folders.length > 0 ? folders[0][1] : 1;
|
|
361
|
+
|
|
362
|
+
// Prepare chart data
|
|
363
|
+
const langLabels = JSON.stringify(languages.map(([l]) => l));
|
|
364
|
+
const langValues = JSON.stringify(languages.map(([, c]) => c));
|
|
365
|
+
const langColors = JSON.stringify(languages.map((_, i) => colorFor(i)));
|
|
366
|
+
|
|
367
|
+
const foldersToShow = folders.slice(0, 15);
|
|
368
|
+
const folderLabels = JSON.stringify(foldersToShow.map(([f]) => f + "/"));
|
|
369
|
+
const folderValues = JSON.stringify(foldersToShow.map(([, c]) => c));
|
|
370
|
+
const folderColors = JSON.stringify(foldersToShow.map((_, i) => colorFor(i + 3)));
|
|
371
|
+
|
|
372
|
+
let html = `<!DOCTYPE html>
|
|
373
|
+
<html lang="en">
|
|
374
|
+
<head>
|
|
375
|
+
<meta charset="UTF-8">
|
|
376
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
377
|
+
<title>Project Report — ${esc(projectName)}</title>
|
|
378
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
379
|
+
<style>
|
|
380
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
381
|
+
body {
|
|
382
|
+
background: #0d1117; color: #e6edf3;
|
|
383
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
384
|
+
line-height: 1.6; padding: 2rem 1rem;
|
|
385
|
+
}
|
|
386
|
+
.container { max-width: 920px; margin: 0 auto; }
|
|
387
|
+
h1 { font-size: 1.75rem; font-weight: 600; margin-bottom: 0.25rem; }
|
|
388
|
+
.subtitle { color: #8b949e; font-size: 0.9rem; margin-bottom: 2rem; }
|
|
389
|
+
.section { margin-bottom: 2.5rem; }
|
|
390
|
+
.section-title {
|
|
391
|
+
font-size: 1.1rem; font-weight: 600; margin-bottom: 1rem;
|
|
392
|
+
padding-bottom: 0.5rem; border-bottom: 1px solid #21262d;
|
|
393
|
+
}
|
|
394
|
+
.chart-row { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; align-items: start; }
|
|
395
|
+
@media (max-width: 700px) { .chart-row { grid-template-columns: 1fr; } }
|
|
396
|
+
.chart-wrap { position: relative; background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 1.25rem; }
|
|
397
|
+
.chart-wrap canvas { width: 100% !important; }
|
|
398
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; }
|
|
399
|
+
.card {
|
|
400
|
+
background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 1.25rem;
|
|
401
|
+
}
|
|
402
|
+
.card-title { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; color: #8b949e; margin-bottom: 0.5rem; }
|
|
403
|
+
.card-value { font-size: 1.5rem; font-weight: 600; color: #58a6ff; }
|
|
404
|
+
.card-detail { font-size: 0.8rem; color: #8b949e; margin-top: 0.25rem; }
|
|
405
|
+
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
|
406
|
+
th { text-align: left; color: #8b949e; font-weight: 500; padding: 0.5rem 0.75rem; border-bottom: 1px solid #21262d; }
|
|
407
|
+
td { padding: 0.5rem 0.75rem; border-bottom: 1px solid #161b22; }
|
|
408
|
+
tr:hover td { background: #161b22; }
|
|
409
|
+
.mono { font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 0.8rem; }
|
|
410
|
+
.text-right { text-align: right; }
|
|
411
|
+
.footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid #21262d; font-size: 0.75rem; color: #484f58; text-align: center; }
|
|
412
|
+
</style>
|
|
413
|
+
</head>
|
|
414
|
+
<body>
|
|
415
|
+
<div class="container">
|
|
416
|
+
<h1>${esc(projectName)}</h1>
|
|
417
|
+
<div class="subtitle">Project Report · Generated ${esc(genDate)} · ${fmt(totalLines)} total lines of code</div>
|
|
418
|
+
|
|
419
|
+
<div class="section">
|
|
420
|
+
<div class="section-title">Code Distribution</div>
|
|
421
|
+
<div class="chart-row">
|
|
422
|
+
<div class="chart-wrap">
|
|
423
|
+
<canvas id="langChart"></canvas>
|
|
424
|
+
</div>
|
|
425
|
+
<div class="chart-wrap">
|
|
426
|
+
<canvas id="folderChart"></canvas>
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<div class="section">
|
|
432
|
+
<div class="section-title">Largest Files</div>
|
|
433
|
+
<table>
|
|
434
|
+
<thead><tr><th>#</th><th>File</th><th>Language</th><th class="text-right">Lines</th></tr></thead>
|
|
435
|
+
<tbody>
|
|
436
|
+
`;
|
|
437
|
+
|
|
438
|
+
for (let i = 0; i < largestFiles.length; i++) {
|
|
439
|
+
const f = largestFiles[i];
|
|
440
|
+
html += ` <tr><td>${i + 1}</td><td class="mono">${esc(f.path.replace(/\\/g, "/"))}</td><td>${esc(f.lang)}</td><td class="text-right">${fmt(f.lines)}</td></tr>\n`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
html += ` </tbody>
|
|
444
|
+
</table>
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
<div class="section">
|
|
448
|
+
<div class="section-title">Estimation</div>
|
|
449
|
+
<div class="cards">
|
|
450
|
+
<div class="card">
|
|
451
|
+
<div class="card-title">COCOMO-II Effort</div>
|
|
452
|
+
<div class="card-value">${cocomo.effort.toFixed(1)} PM</div>
|
|
453
|
+
<div class="card-detail">Person-Months (Semi-Detached)</div>
|
|
454
|
+
</div>
|
|
455
|
+
<div class="card">
|
|
456
|
+
<div class="card-title">Schedule</div>
|
|
457
|
+
<div class="card-value">${cocomo.schedule.toFixed(1)} mo</div>
|
|
458
|
+
<div class="card-detail">${cocomo.teamSize.toFixed(1)} developers</div>
|
|
459
|
+
</div>
|
|
460
|
+
<div class="card">
|
|
461
|
+
<div class="card-title">Estimated Cost</div>
|
|
462
|
+
<div class="card-value">€${fmt(Math.round(cocomo.cost))}</div>
|
|
463
|
+
<div class="card-detail">${fmt(cocomo.kloc.toFixed(1))} KLOC at €${fmt(cocomo.hourlyRate)}/h</div>
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
`;
|
|
468
|
+
|
|
469
|
+
if (vibeStats) {
|
|
470
|
+
html += `
|
|
471
|
+
<div class="section">
|
|
472
|
+
<div class="section-title">Token Savings</div>
|
|
473
|
+
<div class="cards">
|
|
474
|
+
<div class="card">
|
|
475
|
+
<div class="card-title">Tokens Saved</div>
|
|
476
|
+
<div class="card-value">${fmt(vibeStats.totalSaved)}</div>
|
|
477
|
+
<div class="card-detail">across ${fmt(vibeStats.operationsCount)} operations</div>
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
`;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (gitStats) {
|
|
485
|
+
html += `
|
|
486
|
+
<div class="section">
|
|
487
|
+
<div class="section-title">Git Statistics</div>
|
|
488
|
+
<div class="cards">
|
|
489
|
+
<div class="card">
|
|
490
|
+
<div class="card-title">Total Commits</div>
|
|
491
|
+
<div class="card-value">${fmt(gitStats.totalCommits)}</div>
|
|
492
|
+
<div class="card-detail">since ${esc(gitStats.firstCommitDate)}</div>
|
|
493
|
+
</div>
|
|
494
|
+
<div class="card">
|
|
495
|
+
<div class="card-title">Active Days</div>
|
|
496
|
+
<div class="card-value">${fmt(gitStats.activeDays)}</div>
|
|
497
|
+
<div class="card-detail">days with at least one commit</div>
|
|
498
|
+
</div>
|
|
499
|
+
<div class="card">
|
|
500
|
+
<div class="card-title">Last Commit</div>
|
|
501
|
+
<div class="card-value">${esc(gitStats.lastCommitDate)}</div>
|
|
502
|
+
<div class="card-detail"> </div>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
html += `
|
|
510
|
+
<div class="footer">Generated by cc-starter · project-report.js</div>
|
|
511
|
+
</div>
|
|
512
|
+
|
|
513
|
+
<script>
|
|
514
|
+
Chart.defaults.color = '#8b949e';
|
|
515
|
+
Chart.defaults.borderColor = '#21262d';
|
|
516
|
+
|
|
517
|
+
new Chart(document.getElementById('langChart'), {
|
|
518
|
+
type: 'doughnut',
|
|
519
|
+
data: {
|
|
520
|
+
labels: ${langLabels},
|
|
521
|
+
datasets: [{
|
|
522
|
+
data: ${langValues},
|
|
523
|
+
backgroundColor: ${langColors},
|
|
524
|
+
borderWidth: 0,
|
|
525
|
+
hoverOffset: 6
|
|
526
|
+
}]
|
|
527
|
+
},
|
|
528
|
+
options: {
|
|
529
|
+
responsive: true,
|
|
530
|
+
plugins: {
|
|
531
|
+
title: { display: true, text: 'By Language', color: '#e6edf3', font: { size: 14, weight: '600' } },
|
|
532
|
+
legend: { position: 'bottom', labels: { padding: 12, usePointStyle: true, pointStyleWidth: 8 } },
|
|
533
|
+
tooltip: {
|
|
534
|
+
callbacks: {
|
|
535
|
+
label: function(ctx) {
|
|
536
|
+
const total = ctx.dataset.data.reduce((a, b) => a + b, 0);
|
|
537
|
+
const pct = ((ctx.raw / total) * 100).toFixed(1);
|
|
538
|
+
return ctx.label + ': ' + ctx.raw.toLocaleString() + ' lines (' + pct + '%)';
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
new Chart(document.getElementById('folderChart'), {
|
|
547
|
+
type: 'bar',
|
|
548
|
+
data: {
|
|
549
|
+
labels: ${folderLabels},
|
|
550
|
+
datasets: [{
|
|
551
|
+
label: 'Lines of Code',
|
|
552
|
+
data: ${folderValues},
|
|
553
|
+
backgroundColor: ${folderColors},
|
|
554
|
+
borderWidth: 0,
|
|
555
|
+
borderRadius: 3
|
|
556
|
+
}]
|
|
557
|
+
},
|
|
558
|
+
options: {
|
|
559
|
+
indexAxis: 'y',
|
|
560
|
+
responsive: true,
|
|
561
|
+
plugins: {
|
|
562
|
+
title: { display: true, text: 'By Folder', color: '#e6edf3', font: { size: 14, weight: '600' } },
|
|
563
|
+
legend: { display: false },
|
|
564
|
+
tooltip: {
|
|
565
|
+
callbacks: {
|
|
566
|
+
label: function(ctx) { return ctx.raw.toLocaleString() + ' lines'; }
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
scales: {
|
|
571
|
+
x: { grid: { color: '#161b22' }, ticks: { color: '#8b949e' } },
|
|
572
|
+
y: { grid: { display: false }, ticks: { color: '#c9d1d9', font: { family: '"SFMono-Regular", Consolas, monospace', size: 11 } } }
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
</script>
|
|
577
|
+
</body>
|
|
578
|
+
</html>`;
|
|
579
|
+
|
|
580
|
+
return html;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ── Main ─────────────────────────────────────────────────────
|
|
584
|
+
|
|
585
|
+
function main() {
|
|
586
|
+
const root = process.cwd();
|
|
587
|
+
const config = readConfig(root);
|
|
588
|
+
const hourlyRate = config.hourlyRate || DEFAULT_HOURLY_RATE;
|
|
589
|
+
const reportStyle = config.reportStyle || "minimal";
|
|
590
|
+
|
|
591
|
+
// Determine project name from package.json or folder name
|
|
592
|
+
let projectName = path.basename(root);
|
|
593
|
+
try {
|
|
594
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf-8"));
|
|
595
|
+
if (pkg.name) projectName = pkg.name;
|
|
596
|
+
} catch {}
|
|
597
|
+
|
|
598
|
+
// Collect data
|
|
599
|
+
const data = collectData(root);
|
|
600
|
+
|
|
601
|
+
if (data.totalLines === 0) {
|
|
602
|
+
console.log("\n No source files found in the current directory.\n");
|
|
603
|
+
process.exit(0);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const est = cocomoEstimate(data.totalLines, hourlyRate);
|
|
607
|
+
est.hourlyRate = hourlyRate;
|
|
608
|
+
|
|
609
|
+
const vibeStats = loadVibeStats(root);
|
|
610
|
+
const gitStats = getGitStats(root);
|
|
611
|
+
|
|
612
|
+
// Generate HTML
|
|
613
|
+
const html = reportStyle === "fancy"
|
|
614
|
+
? generateFancy(projectName, data, est, vibeStats, gitStats)
|
|
615
|
+
: generateMinimal(projectName, data, est, vibeStats, gitStats);
|
|
616
|
+
|
|
617
|
+
// Write output
|
|
618
|
+
const outDir = path.join(root, "stats");
|
|
619
|
+
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
|
620
|
+
const outPath = path.join(outDir, "report.html");
|
|
621
|
+
fs.writeFileSync(outPath, html, "utf-8");
|
|
622
|
+
|
|
623
|
+
console.log(`\n Report generated: ${outPath}\n`);
|
|
624
|
+
|
|
625
|
+
// Try to open in browser
|
|
626
|
+
try {
|
|
627
|
+
const platform = process.platform;
|
|
628
|
+
if (platform === "win32") {
|
|
629
|
+
execSync(`start "" "${outPath}"`, { stdio: "ignore", shell: true });
|
|
630
|
+
} else if (platform === "darwin") {
|
|
631
|
+
execSync(`open "${outPath}"`, { stdio: "ignore" });
|
|
632
|
+
} else {
|
|
633
|
+
execSync(`xdg-open "${outPath}"`, { stdio: "ignore" });
|
|
634
|
+
}
|
|
635
|
+
} catch {
|
|
636
|
+
// Silently ignore if browser can't be opened
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
main();
|