@vibecodeqa/cli 0.10.0 → 0.12.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 +17 -6
- package/dist/cli.js +31 -3
- package/dist/report/html.js +29 -0
- package/package.json +1 -1
- package/dist/runners/coverage.d.ts +0 -3
- package/dist/runners/coverage.js +0 -65
- package/dist/runners/tests.d.ts +0 -3
- package/dist/runners/tests.js +0 -54
package/README.md
CHANGED
|
@@ -25,6 +25,9 @@ npx @vibecodeqa/cli
|
|
|
25
25
|
# Fast mode (skip test execution)
|
|
26
26
|
npx @vibecodeqa/cli --skip-tests
|
|
27
27
|
|
|
28
|
+
# Watch mode (re-scan on file changes)
|
|
29
|
+
npx @vibecodeqa/cli --watch
|
|
30
|
+
|
|
28
31
|
# CI mode (exit code 1 if score < 60)
|
|
29
32
|
npx @vibecodeqa/cli --ci
|
|
30
33
|
|
|
@@ -36,8 +39,9 @@ npx @vibecodeqa/cli /path/to/project
|
|
|
36
39
|
```
|
|
37
40
|
|
|
38
41
|
Output goes to `.vibe-check/`:
|
|
39
|
-
- `report.html` — navigable dashboard (open in browser)
|
|
42
|
+
- `report.html` — navigable multi-page dashboard (open in browser)
|
|
40
43
|
- `report.json` — machine-readable results
|
|
44
|
+
- `history/` — last 30 reports for trend tracking
|
|
41
45
|
|
|
42
46
|
## Checks
|
|
43
47
|
|
|
@@ -113,12 +117,18 @@ Each check produces a score from 0-100. The composite score is a weighted averag
|
|
|
113
117
|
|
|
114
118
|
## Report features
|
|
115
119
|
|
|
120
|
+
The report is a multi-page navigable dashboard:
|
|
121
|
+
|
|
122
|
+
- **10 pages**: Overview, Foundations, Quality, Testing, Architecture, Security, LLM Readiness, Issues, File Map, Heatmap
|
|
123
|
+
- **Top nav + sidebar** — navigate by category and check
|
|
116
124
|
- **Radar chart** — 6-axis view of category scores
|
|
117
|
-
- **Architecture diagram** —
|
|
118
|
-
- **
|
|
119
|
-
- **
|
|
120
|
-
- **
|
|
121
|
-
- **
|
|
125
|
+
- **Architecture SVG diagram** — modules grouped by directory, import edges, node size by fan-in
|
|
126
|
+
- **Code heatmap** — colored bars showing issue density per file
|
|
127
|
+
- **Trend comparison** — score delta vs. previous run (reads previous report.json)
|
|
128
|
+
- **File map** — top files by issue count across all checks
|
|
129
|
+
- **GitHub links** — click any file:line to open in GitHub (auto-detected from git remote)
|
|
130
|
+
- **Actionable prompts** — 📋 button on every issue copies a fix prompt for Claude/Codex
|
|
131
|
+
- **Info panels** — each check has What/Risk/Fix explanations with research citations
|
|
122
132
|
- **Priority badges** — critical/high/medium/low on each check
|
|
123
133
|
|
|
124
134
|
## Trend tracking
|
|
@@ -133,6 +143,7 @@ vcqa reads the previous `.vibe-check/report.json` on each run and shows:
|
|
|
133
143
|
| Flag | Description |
|
|
134
144
|
|------|-------------|
|
|
135
145
|
| `--skip-tests` | Skip test execution and coverage (fast mode) |
|
|
146
|
+
| `--watch` | Re-scan automatically on file changes |
|
|
136
147
|
| `--ci` | Exit code 1 if composite score < 60 |
|
|
137
148
|
| `--json` | Output JSON to stdout (no HTML, no browser) |
|
|
138
149
|
|
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, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, 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,8 @@ 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
|
|
25
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
26
|
+
const VERSION = pkg.version;
|
|
26
27
|
const args = process.argv.slice(2);
|
|
27
28
|
const flags = new Set(args.filter((a) => a.startsWith("--")));
|
|
28
29
|
const cwd = resolve(args.find((a) => !a.startsWith("--")) || ".");
|
|
@@ -30,6 +31,7 @@ const outputDir = join(cwd, ".vibe-check");
|
|
|
30
31
|
const jsonOnly = flags.has("--json");
|
|
31
32
|
const ciMode = flags.has("--ci");
|
|
32
33
|
const skipTests = flags.has("--skip-tests");
|
|
34
|
+
const watchMode = flags.has("--watch");
|
|
33
35
|
function color(grade) {
|
|
34
36
|
if (grade === "A")
|
|
35
37
|
return "\x1b[32m";
|
|
@@ -141,7 +143,7 @@ async function main() {
|
|
|
141
143
|
if (ciMode && score < 60) {
|
|
142
144
|
process.exit(1);
|
|
143
145
|
}
|
|
144
|
-
if (!jsonOnly && !ciMode) {
|
|
146
|
+
if (!jsonOnly && !ciMode && !watchMode) {
|
|
145
147
|
try {
|
|
146
148
|
const { execSync } = await import("node:child_process");
|
|
147
149
|
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
@@ -149,6 +151,32 @@ async function main() {
|
|
|
149
151
|
}
|
|
150
152
|
catch { /* failed to open browser */ }
|
|
151
153
|
}
|
|
154
|
+
// Watch mode — re-run on file changes
|
|
155
|
+
if (watchMode) {
|
|
156
|
+
const { watch } = await import("node:fs");
|
|
157
|
+
const srcDirs = ["src", "web/src"].map((d) => join(cwd, d)).filter((d) => existsSync(d));
|
|
158
|
+
if (srcDirs.length === 0) {
|
|
159
|
+
console.log(" \x1b[31mNo src/ directory to watch\x1b[0m");
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
console.log(" \x1b[2mWatching for changes... (Ctrl+C to stop)\x1b[0m");
|
|
163
|
+
console.log("");
|
|
164
|
+
let debounce = null;
|
|
165
|
+
for (const dir of srcDirs) {
|
|
166
|
+
watch(dir, { recursive: true }, (_event, filename) => {
|
|
167
|
+
if (!filename || filename.includes("node_modules") || filename.includes(".vibe-check"))
|
|
168
|
+
return;
|
|
169
|
+
if (debounce)
|
|
170
|
+
clearTimeout(debounce);
|
|
171
|
+
debounce = setTimeout(() => {
|
|
172
|
+
console.log(` \x1b[2mChanged: ${filename} — re-scanning...\x1b[0m`);
|
|
173
|
+
main().catch(() => { });
|
|
174
|
+
}, 500);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
// Keep process alive
|
|
178
|
+
await new Promise(() => { });
|
|
179
|
+
}
|
|
152
180
|
}
|
|
153
181
|
main().catch((err) => {
|
|
154
182
|
console.error("vibe-check error:", err);
|
package/dist/report/html.js
CHANGED
|
@@ -78,6 +78,7 @@ export function generateHTML(report) {
|
|
|
78
78
|
...GROUPS.map((g) => ({ id: g.id, label: g.label })),
|
|
79
79
|
{ id: "issues", label: `Issues (${totalIssues})` },
|
|
80
80
|
{ id: "files", label: "File Map" },
|
|
81
|
+
{ id: "heatmap", label: "Heatmap" },
|
|
81
82
|
];
|
|
82
83
|
const topNav = topNavItems.map((t) => `<a class="tn" data-page="${t.id}" onclick="go('${t.id}')">${t.label}</a>`).join("");
|
|
83
84
|
// Overview page
|
|
@@ -189,6 +190,29 @@ ${allIssues.length > 200 ? `<p style="color:var(--muted);text-align:center;margi
|
|
|
189
190
|
<h2>File Heatmap</h2>
|
|
190
191
|
<p style="color:var(--muted);font-size:0.78rem;margin-bottom:1rem">Top ${topFiles.length} files by total issues across all checks</p>
|
|
191
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>'}
|
|
192
216
|
</div>`;
|
|
193
217
|
return `<!DOCTYPE html>
|
|
194
218
|
<html lang="en">
|
|
@@ -316,6 +340,10 @@ h3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:
|
|
|
316
340
|
.footer a{color:var(--muted)}
|
|
317
341
|
.flink{color:var(--accent);text-decoration:none;font-family:"SF Mono",monospace}.flink:hover{text-decoration:underline}
|
|
318
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}
|
|
319
347
|
.arch-svg svg{border-radius:8px}
|
|
320
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}
|
|
321
349
|
.ir:hover .cp-btn{opacity:0.6}
|
|
@@ -345,6 +373,7 @@ h3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:
|
|
|
345
373
|
${catPages}
|
|
346
374
|
${issuesPage}
|
|
347
375
|
${filesPage}
|
|
376
|
+
${heatmapPage}
|
|
348
377
|
<div class="footer">Generated by <a href="https://vibecodeqa.online">VibeCode QA</a> v${report.version} — <code>npx @vibecodeqa/cli</code></div>
|
|
349
378
|
</div>
|
|
350
379
|
|
package/package.json
CHANGED
package/dist/runners/coverage.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/** Coverage runner — runs tests with coverage and parses the summary. */
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { gradeFromScore } from "../types.js";
|
|
5
|
-
import { run } from "./exec.js";
|
|
6
|
-
export function runCoverage(cwd, stack) {
|
|
7
|
-
const start = Date.now();
|
|
8
|
-
if (stack.testRunner === "none") {
|
|
9
|
-
return {
|
|
10
|
-
name: "coverage",
|
|
11
|
-
score: 0,
|
|
12
|
-
grade: "F",
|
|
13
|
-
details: { skipped: true, reason: "no test runner" },
|
|
14
|
-
issues: [],
|
|
15
|
-
duration: Date.now() - start,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
// Run tests with coverage
|
|
19
|
-
const cmd = stack.testRunner === "vitest"
|
|
20
|
-
? "npx vitest run --coverage 2>/dev/null || true"
|
|
21
|
-
: "npx jest --coverage --coverageReporters=json-summary 2>/dev/null || true";
|
|
22
|
-
run(cmd, cwd, 120_000);
|
|
23
|
-
// Look for coverage summary
|
|
24
|
-
const searchPaths = [
|
|
25
|
-
"coverage/coverage-summary.json",
|
|
26
|
-
"test-results/coverage/coverage-summary.json",
|
|
27
|
-
];
|
|
28
|
-
let summary = null;
|
|
29
|
-
for (const p of searchPaths) {
|
|
30
|
-
const full = join(cwd, p);
|
|
31
|
-
if (existsSync(full)) {
|
|
32
|
-
try {
|
|
33
|
-
summary = JSON.parse(readFileSync(full, "utf-8"));
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
/* parse failed */
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
if (!summary?.total) {
|
|
42
|
-
return {
|
|
43
|
-
name: "coverage",
|
|
44
|
-
score: 0,
|
|
45
|
-
grade: "F",
|
|
46
|
-
details: { skipped: true, reason: "no coverage data generated" },
|
|
47
|
-
issues: [],
|
|
48
|
-
duration: Date.now() - start,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
const stmts = summary.total.statements?.pct || 0;
|
|
52
|
-
const lines = summary.total.lines?.pct || 0;
|
|
53
|
-
const branches = summary.total.branches?.pct || 0;
|
|
54
|
-
const functions = summary.total.functions?.pct || 0;
|
|
55
|
-
// Score is the average of all four metrics
|
|
56
|
-
const score = Math.round((stmts + lines + branches + functions) / 4);
|
|
57
|
-
return {
|
|
58
|
-
name: "coverage",
|
|
59
|
-
score,
|
|
60
|
-
grade: gradeFromScore(score),
|
|
61
|
-
details: { statements: stmts, lines, branches, functions },
|
|
62
|
-
issues: [],
|
|
63
|
-
duration: Date.now() - start,
|
|
64
|
-
};
|
|
65
|
-
}
|
package/dist/runners/tests.d.ts
DELETED
package/dist/runners/tests.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/** Test runner — auto-detects vitest or jest. */
|
|
2
|
-
import { gradeFromScore } from "../types.js";
|
|
3
|
-
import { run } from "./exec.js";
|
|
4
|
-
export function runTests(cwd, stack) {
|
|
5
|
-
const start = Date.now();
|
|
6
|
-
if (stack.testRunner === "none") {
|
|
7
|
-
return {
|
|
8
|
-
name: "tests",
|
|
9
|
-
score: 0,
|
|
10
|
-
grade: "F",
|
|
11
|
-
details: { skipped: true, reason: "no test runner" },
|
|
12
|
-
issues: [],
|
|
13
|
-
duration: Date.now() - start,
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
const cmd = stack.testRunner === "vitest"
|
|
17
|
-
? "npx vitest run --reporter=json 2>/dev/null || true"
|
|
18
|
-
: "npx jest --json 2>/dev/null || true";
|
|
19
|
-
const { stdout } = run(cmd, cwd, 120_000);
|
|
20
|
-
// Extract JSON from output (vitest may print other stuff before the JSON)
|
|
21
|
-
let data = null;
|
|
22
|
-
try {
|
|
23
|
-
// Find the JSON object in stdout
|
|
24
|
-
const jsonStart = stdout.indexOf("{");
|
|
25
|
-
if (jsonStart >= 0) {
|
|
26
|
-
data = JSON.parse(stdout.slice(jsonStart));
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
/* parse failed */
|
|
31
|
-
}
|
|
32
|
-
if (!data) {
|
|
33
|
-
return {
|
|
34
|
-
name: "tests",
|
|
35
|
-
score: 0,
|
|
36
|
-
grade: "F",
|
|
37
|
-
details: { error: "could not parse test output" },
|
|
38
|
-
issues: [],
|
|
39
|
-
duration: Date.now() - start,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
const passed = data.numPassedTests || 0;
|
|
43
|
-
const failed = data.numFailedTests || 0;
|
|
44
|
-
const total = data.numTotalTests || 0;
|
|
45
|
-
const score = total === 0 ? 0 : Math.round((passed / total) * 100);
|
|
46
|
-
return {
|
|
47
|
-
name: "tests",
|
|
48
|
-
score,
|
|
49
|
-
grade: gradeFromScore(score),
|
|
50
|
-
details: { passed, failed, total, runner: stack.testRunner },
|
|
51
|
-
issues: [],
|
|
52
|
-
duration: Date.now() - start,
|
|
53
|
-
};
|
|
54
|
-
}
|