@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 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** — SVG showing module relationships and import edges
118
- - **Trend comparison** — score delta vs. previous run
119
- - **File heatmap** — top files by issue count across all checks
120
- - **GitHub links** — click any file:line to open in GitHub
121
- - **Info panels** — each check explains what it measures, why it matters, and how to fix issues
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 VERSION = "0.10.0";
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);
@@ -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} &mdash; <code>npx @vibecodeqa/cli</code></div>
349
378
  </div>
350
379
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodeqa/cli",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Code health scanner for the AI coding era. 15 checks, zero config, full report.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,3 +0,0 @@
1
- /** Coverage runner — runs tests with coverage and parses the summary. */
2
- import type { CheckResult, StackInfo } from "../types.js";
3
- export declare function runCoverage(cwd: string, stack: StackInfo): CheckResult;
@@ -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
- }
@@ -1,3 +0,0 @@
1
- /** Test runner — auto-detects vitest or jest. */
2
- import type { CheckResult, StackInfo } from "../types.js";
3
- export declare function runTests(cwd: string, stack: StackInfo): CheckResult;
@@ -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
- }