@vibecodeqa/cli 0.9.1 → 0.11.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/dist/cli.js +46 -3
- package/dist/report/html.js +39 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/** vibe-check — code health scanner for the AI coding era. */
|
|
3
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { detectRepoUrl, detectStack } from "./detect.js";
|
|
6
6
|
import { generateHTML } from "./report/html.js";
|
|
@@ -22,7 +22,7 @@ import { runTypeSafety } from "./runners/type-safety.js";
|
|
|
22
22
|
import { computeScore } from "./score.js";
|
|
23
23
|
import { computeTrend, formatTrend } from "./trend.js";
|
|
24
24
|
import { gradeFromScore } from "./types.js";
|
|
25
|
-
const VERSION = "0.
|
|
25
|
+
const VERSION = "0.11.0";
|
|
26
26
|
const args = process.argv.slice(2);
|
|
27
27
|
const flags = new Set(args.filter((a) => a.startsWith("--")));
|
|
28
28
|
const cwd = resolve(args.find((a) => !a.startsWith("--")) || ".");
|
|
@@ -30,6 +30,7 @@ const outputDir = join(cwd, ".vibe-check");
|
|
|
30
30
|
const jsonOnly = flags.has("--json");
|
|
31
31
|
const ciMode = flags.has("--ci");
|
|
32
32
|
const skipTests = flags.has("--skip-tests");
|
|
33
|
+
const watchMode = flags.has("--watch");
|
|
33
34
|
function color(grade) {
|
|
34
35
|
if (grade === "A")
|
|
35
36
|
return "\x1b[32m";
|
|
@@ -106,6 +107,22 @@ async function main() {
|
|
|
106
107
|
const trend = computeTrend(report, outputDir);
|
|
107
108
|
if (!existsSync(outputDir))
|
|
108
109
|
mkdirSync(outputDir, { recursive: true });
|
|
110
|
+
// Save to history before overwriting current report
|
|
111
|
+
const historyDir = join(outputDir, "history");
|
|
112
|
+
if (!existsSync(historyDir))
|
|
113
|
+
mkdirSync(historyDir, { recursive: true });
|
|
114
|
+
const historyFile = join(historyDir, `${report.timestamp.replace(/[:.]/g, "-")}.json`);
|
|
115
|
+
writeFileSync(historyFile, JSON.stringify(report, null, 2));
|
|
116
|
+
// Keep only last 30 history entries
|
|
117
|
+
const historyFiles = readdirSync(historyDir).filter((f) => f.endsWith(".json")).sort();
|
|
118
|
+
if (historyFiles.length > 30) {
|
|
119
|
+
for (const old of historyFiles.slice(0, historyFiles.length - 30)) {
|
|
120
|
+
try {
|
|
121
|
+
unlinkSync(join(historyDir, old));
|
|
122
|
+
}
|
|
123
|
+
catch { /* ignore */ }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
109
126
|
writeFileSync(join(outputDir, "report.json"), JSON.stringify(report, null, 2));
|
|
110
127
|
writeFileSync(join(outputDir, "report.html"), generateHTML(report));
|
|
111
128
|
if (jsonOnly) {
|
|
@@ -125,7 +142,7 @@ async function main() {
|
|
|
125
142
|
if (ciMode && score < 60) {
|
|
126
143
|
process.exit(1);
|
|
127
144
|
}
|
|
128
|
-
if (!jsonOnly && !ciMode) {
|
|
145
|
+
if (!jsonOnly && !ciMode && !watchMode) {
|
|
129
146
|
try {
|
|
130
147
|
const { execSync } = await import("node:child_process");
|
|
131
148
|
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
@@ -133,6 +150,32 @@ async function main() {
|
|
|
133
150
|
}
|
|
134
151
|
catch { /* failed to open browser */ }
|
|
135
152
|
}
|
|
153
|
+
// Watch mode — re-run on file changes
|
|
154
|
+
if (watchMode) {
|
|
155
|
+
const { watch } = await import("node:fs");
|
|
156
|
+
const srcDirs = ["src", "web/src"].map((d) => join(cwd, d)).filter((d) => existsSync(d));
|
|
157
|
+
if (srcDirs.length === 0) {
|
|
158
|
+
console.log(" \x1b[31mNo src/ directory to watch\x1b[0m");
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
console.log(" \x1b[2mWatching for changes... (Ctrl+C to stop)\x1b[0m");
|
|
162
|
+
console.log("");
|
|
163
|
+
let debounce = null;
|
|
164
|
+
for (const dir of srcDirs) {
|
|
165
|
+
watch(dir, { recursive: true }, (_event, filename) => {
|
|
166
|
+
if (!filename || filename.includes("node_modules") || filename.includes(".vibe-check"))
|
|
167
|
+
return;
|
|
168
|
+
if (debounce)
|
|
169
|
+
clearTimeout(debounce);
|
|
170
|
+
debounce = setTimeout(() => {
|
|
171
|
+
console.log(` \x1b[2mChanged: ${filename} — re-scanning...\x1b[0m`);
|
|
172
|
+
main().catch(() => { });
|
|
173
|
+
}, 500);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// Keep process alive
|
|
177
|
+
await new Promise(() => { });
|
|
178
|
+
}
|
|
136
179
|
}
|
|
137
180
|
main().catch((err) => {
|
|
138
181
|
console.error("vibe-check error:", err);
|
package/dist/report/html.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* All in one self-contained HTML file using hash routing + show/hide.
|
|
10
10
|
*/
|
|
11
11
|
import { getCheckMeta } from "../check-meta.js";
|
|
12
|
+
import { generateArchSVG } from "../runners/architecture.js";
|
|
12
13
|
function e(s) {
|
|
13
14
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
14
15
|
}
|
|
@@ -77,6 +78,7 @@ export function generateHTML(report) {
|
|
|
77
78
|
...GROUPS.map((g) => ({ id: g.id, label: g.label })),
|
|
78
79
|
{ id: "issues", label: `Issues (${totalIssues})` },
|
|
79
80
|
{ id: "files", label: "File Map" },
|
|
81
|
+
{ id: "heatmap", label: "Heatmap" },
|
|
80
82
|
];
|
|
81
83
|
const topNav = topNavItems.map((t) => `<a class="tn" data-page="${t.id}" onclick="go('${t.id}')">${t.label}</a>`).join("");
|
|
82
84
|
// Overview page
|
|
@@ -116,7 +118,7 @@ export function generateHTML(report) {
|
|
|
116
118
|
const subPages = cs.checks.map((c, i) => {
|
|
117
119
|
const meta = getCheckMeta(c.name);
|
|
118
120
|
const sk = c.details.skipped;
|
|
119
|
-
const
|
|
121
|
+
const detailsFiltered = Object.entries(c.details).filter(([k]) => k !== "skipped" && k !== "reason" && k !== "graph").map(([k, v]) => {
|
|
120
122
|
const d = Array.isArray(v) ? v.join(", ") : typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
121
123
|
return `<div class="kv"><span class="k">${e(k)}</span><span class="v">${e(d)}</span></div>`;
|
|
122
124
|
}).join("");
|
|
@@ -138,7 +140,8 @@ export function generateHTML(report) {
|
|
|
138
140
|
for (const [file, issues] of byFile) {
|
|
139
141
|
issuesHtml += `<div class="fg"><div class="fn">${fl(file)} <span class="fc">${issues.length}</span></div>`;
|
|
140
142
|
for (const iss of issues) {
|
|
141
|
-
|
|
143
|
+
const prompt = `Fix this issue in ${file}${iss.line ? ":" + iss.line : ""}\\n${iss.severity}: ${iss.message}${iss.rule ? " (" + iss.rule + ")" : ""}\\nCheck: ${c.name}`;
|
|
144
|
+
issuesHtml += `<div class="ir ${iss.severity}"><span class="is">${iss.severity[0].toUpperCase()}</span>${iss.line ? `<span class="il">${iss.line}</span>` : ""}<span class="im">${e(iss.message)}</span>${iss.rule ? `<span class="iru">${e(iss.rule)}</span>` : ""}<button class="cp-btn" onclick="navigator.clipboard.writeText('${prompt.replace(/'/g, "\\'")}');this.textContent='✓';setTimeout(()=>this.textContent='📋',1000)" title="Copy fix prompt">📋</button></div>`;
|
|
142
145
|
}
|
|
143
146
|
issuesHtml += `</div>`;
|
|
144
147
|
}
|
|
@@ -153,7 +156,8 @@ export function generateHTML(report) {
|
|
|
153
156
|
<div class="ch-head"><span class="ch-g" style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "—" : c.grade}</span><div><b>${e(meta.label)}</b><span class="ch-s">${sk ? "skipped" : c.score + "/100"} · weight ${meta.weight}% · ${c.duration}ms · ${c.issues.length} issues</span></div><span class="pri" style="color:${pc(meta.priority)}">${meta.priority}</span></div>
|
|
154
157
|
${meta.description ? `<div class="info-panel"><div class="ip-row"><span class="ip-label">What</span><span>${e(meta.description)}</span></div><div class="ip-row"><span class="ip-label">Risk</span><span>${e(meta.risk)}</span></div><div class="ip-row"><span class="ip-label">Fix</span><span>${e(meta.recommendation)}</span></div></div>` : ""}
|
|
155
158
|
${sk ? `<p class="skip-r">${e(c.details.reason || "skipped")}</p>` : ""}
|
|
156
|
-
${
|
|
159
|
+
${c.name === "architecture" && !sk ? `<div class="arch-svg">${generateArchSVG(c.details)}</div>` : ""}
|
|
160
|
+
${detailsFiltered ? `<div class="kvs">${detailsFiltered}</div>` : ""}
|
|
157
161
|
${issuesHtml ? `<div class="iss-list">${issuesHtml}</div>` : '<p style="color:var(--muted);font-size:0.8rem;margin-top:1rem">No issues found.</p>'}
|
|
158
162
|
</div>`;
|
|
159
163
|
}).join("");
|
|
@@ -186,6 +190,29 @@ ${allIssues.length > 200 ? `<p style="color:var(--muted);text-align:center;margi
|
|
|
186
190
|
<h2>File Heatmap</h2>
|
|
187
191
|
<p style="color:var(--muted);font-size:0.78rem;margin-bottom:1rem">Top ${topFiles.length} files by total issues across all checks</p>
|
|
188
192
|
${fileRows || '<p style="color:var(--muted)">No file-level issues found.</p>'}
|
|
193
|
+
</div>`;
|
|
194
|
+
// Codebase heatmap — each file = row of pixels, color = issue density
|
|
195
|
+
const heatmapFiles = [...fileIssues.entries()]
|
|
196
|
+
.sort((a, b) => b[1].errors + b[1].warnings - a[1].errors - a[1].warnings)
|
|
197
|
+
.slice(0, 30);
|
|
198
|
+
let heatmapHtml = "";
|
|
199
|
+
if (heatmapFiles.length > 0) {
|
|
200
|
+
const maxIssues = Math.max(...heatmapFiles.map(([, d]) => d.errors + d.warnings));
|
|
201
|
+
heatmapHtml = heatmapFiles.map(([file, d]) => {
|
|
202
|
+
const total = d.errors + d.warnings;
|
|
203
|
+
const intensity = maxIssues > 0 ? total / maxIssues : 0;
|
|
204
|
+
const r = Math.round(239 * intensity); // red channel
|
|
205
|
+
const g = Math.round(68 * (1 - intensity) + 197 * (d.errors === 0 ? 0.3 : 0)); // green
|
|
206
|
+
const color = `rgb(${r},${g},30)`;
|
|
207
|
+
const barW = Math.max(4, Math.round(intensity * 200));
|
|
208
|
+
const checks = [...d.checks].join(", ");
|
|
209
|
+
return `<div class="hm-row"><span class="hm-name">${fl(file)}</span><div class="hm-bar" style="width:${barW}px;background:${color}" title="${total} issues (${checks})"></div><span class="hm-count">${d.errors}E ${d.warnings}W</span></div>`;
|
|
210
|
+
}).join("");
|
|
211
|
+
}
|
|
212
|
+
const heatmapPage = `<div id="p-heatmap" class="page">
|
|
213
|
+
<h2>Code Heatmap</h2>
|
|
214
|
+
<p style="color:var(--muted);font-size:0.78rem;margin-bottom:1rem">Visual density of issues per file. Red = errors, orange = warnings. Bar width = relative issue count.</p>
|
|
215
|
+
${heatmapHtml || '<p style="color:var(--muted)">No issues to visualize.</p>'}
|
|
189
216
|
</div>`;
|
|
190
217
|
return `<!DOCTYPE html>
|
|
191
218
|
<html lang="en">
|
|
@@ -312,6 +339,14 @@ h3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:
|
|
|
312
339
|
.footer{text-align:center;color:var(--muted);font-size:0.58rem;margin-top:2rem;padding:0.8rem 0;border-top:1px solid var(--border)}
|
|
313
340
|
.footer a{color:var(--muted)}
|
|
314
341
|
.flink{color:var(--accent);text-decoration:none;font-family:"SF Mono",monospace}.flink:hover{text-decoration:underline}
|
|
342
|
+
.arch-svg{margin:1rem 0;overflow-x:auto}
|
|
343
|
+
.hm-row{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.2rem;font-size:0.7rem}
|
|
344
|
+
.hm-name{width:200px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:"SF Mono",monospace;font-size:0.65rem}
|
|
345
|
+
.hm-bar{height:14px;border-radius:3px;min-width:4px}
|
|
346
|
+
.hm-count{color:var(--muted);font-size:0.65rem;flex-shrink:0}
|
|
347
|
+
.arch-svg svg{border-radius:8px}
|
|
348
|
+
.cp-btn{background:none;border:none;cursor:pointer;font-size:0.6rem;opacity:0.3;padding:0 0.2rem;flex-shrink:0}.cp-btn:hover{opacity:1}
|
|
349
|
+
.ir:hover .cp-btn{opacity:0.6}
|
|
315
350
|
@media(max-width:768px){.side{display:none}.content{margin-left:0;padding:1rem}.cats{grid-template-columns:1fr 1fr}.dash{flex-direction:column}}
|
|
316
351
|
</style>
|
|
317
352
|
</head>
|
|
@@ -338,6 +373,7 @@ h3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:
|
|
|
338
373
|
${catPages}
|
|
339
374
|
${issuesPage}
|
|
340
375
|
${filesPage}
|
|
376
|
+
${heatmapPage}
|
|
341
377
|
<div class="footer">Generated by <a href="https://vibecodeqa.online">VibeCode QA</a> v${report.version} — <code>npx @vibecodeqa/cli</code></div>
|
|
342
378
|
</div>
|
|
343
379
|
|