@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 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.9.1";
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);
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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 details = Object.entries(c.details).filter(([k]) => k !== "skipped" && k !== "reason").map(([k, v]) => {
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
- 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>` : ""}</div>`;
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
- ${details ? `<div class="kvs">${details}</div>` : ""}
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} &mdash; <code>npx @vibecodeqa/cli</code></div>
342
378
  </div>
343
379
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodeqa/cli",
3
- "version": "0.9.1",
3
+ "version": "0.11.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": {