@vibecodeqa/cli 0.12.1 → 0.13.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
@@ -145,9 +145,9 @@ async function main() {
145
145
  }
146
146
  if (!jsonOnly && !ciMode && !watchMode) {
147
147
  try {
148
- const { execSync } = await import("node:child_process");
148
+ const { execFileSync } = await import("node:child_process");
149
149
  const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
150
- execSync(`${openCmd} "${join(outputDir, "report.html")}"`, { stdio: "ignore" });
150
+ execFileSync(openCmd, [join(outputDir, "report.html")], { stdio: "ignore" });
151
151
  }
152
152
  catch { /* failed to open browser */ }
153
153
  }
@@ -162,15 +162,20 @@ async function main() {
162
162
  console.log(" \x1b[2mWatching for changes... (Ctrl+C to stop)\x1b[0m");
163
163
  console.log("");
164
164
  let debounce = null;
165
+ let running = false;
165
166
  for (const dir of srcDirs) {
166
167
  watch(dir, { recursive: true }, (_event, filename) => {
167
168
  if (!filename || filename.includes("node_modules") || filename.includes(".vibe-check"))
168
169
  return;
170
+ if (running)
171
+ return; // prevent concurrent re-runs (M5)
169
172
  if (debounce)
170
173
  clearTimeout(debounce);
171
- debounce = setTimeout(() => {
174
+ debounce = setTimeout(async () => {
175
+ running = true;
172
176
  console.log(` \x1b[2mChanged: ${filename} — re-scanning...\x1b[0m`);
173
- main().catch(() => { });
177
+ await main().catch(() => { });
178
+ running = false;
174
179
  }, 500);
175
180
  });
176
181
  }
package/dist/fs-utils.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Shared filesystem utilities — eliminates duplicate file-walking across runners. */
2
- import { readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { lstatSync, readdirSync, readFileSync, statSync } from "node:fs";
3
3
  import { basename, extname, join } from "node:path";
4
4
  const SKIP_DIRS = new Set(["node_modules", "dist", ".git", ".vibe-check", "coverage", "test-results", "__pycache__"]);
5
5
  const CODE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx"]);
@@ -53,6 +53,9 @@ function walk(dir, cwd, out, exts) {
53
53
  if (SKIP_DIRS.has(entry))
54
54
  continue;
55
55
  const full = join(dir, entry);
56
+ // Skip symlinks to prevent traversal attacks (H3)
57
+ if (lstatSync(full).isSymbolicLink())
58
+ continue;
56
59
  if (statSync(full).isDirectory()) {
57
60
  walk(full, cwd, out, exts);
58
61
  }
@@ -60,6 +63,9 @@ function walk(dir, cwd, out, exts) {
60
63
  const ext = extname(entry);
61
64
  if (!exts.has(ext))
62
65
  continue;
66
+ // Skip files over 1MB to prevent memory issues (M1)
67
+ if (statSync(full).size > 1_000_000)
68
+ continue;
63
69
  const content = readFileSync(full, "utf-8");
64
70
  const relPath = full.replace(cwd + "/", "");
65
71
  const isTest = entry.includes(".test.") || entry.includes(".spec.") || relPath.includes("__tests__");
@@ -11,12 +11,12 @@
11
11
  import { getCheckMeta } from "../check-meta.js";
12
12
  import { generateArchSVG } from "../runners/architecture.js";
13
13
  function e(s) {
14
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
14
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
15
15
  }
16
16
  /** Make a file path a clickable GitHub link if repoUrl is available. */
17
17
  function fileLink(path, line, repoUrl, branch) {
18
- const clean = path.split(":")[0]; // strip :line from composite paths
19
- if (!repoUrl)
18
+ const clean = path.split(":")[0];
19
+ if (!repoUrl || !/^https?:\/\//.test(repoUrl))
20
20
  return e(path);
21
21
  const href = `${repoUrl}/blob/${branch}/${clean}${line ? "#L" + line : ""}`;
22
22
  return `<a href="${e(href)}" target="_blank" rel="noopener" class="flink">${e(path)}</a>`;
@@ -140,9 +140,8 @@ export function generateHTML(report) {
140
140
  for (const [file, issues] of byFile) {
141
141
  issuesHtml += `<div class="fg"><div class="fn">${fl(file)} <span class="fc">${issues.length}</span></div>`;
142
142
  for (const iss of issues) {
143
- const promptText = `Fix this issue in ${e(file)}${iss.line ? ":" + iss.line : ""}\n${iss.severity}: ${e(iss.message)}${iss.rule ? " (" + e(iss.rule) + ")" : ""}\nCheck: ${e(c.name)}`;
144
- const safePrompt = promptText.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n");
145
- 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('${safePrompt}');this.textContent='✓';setTimeout(()=>this.textContent='📋',1000)" title="Copy fix prompt">📋</button></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" data-prompt="${e(prompt)}" title="Copy fix prompt">📋</button></div>`;
146
145
  }
147
146
  issuesHtml += `</div>`;
148
147
  }
@@ -390,6 +389,13 @@ function sub(el,cat){
390
389
  el.classList.add('active');
391
390
  document.querySelectorAll('#p-'+cat+' .sp').forEach(s=>{s.classList.toggle('active',s.dataset.sub===id)});
392
391
  }
392
+ // Copy-prompt buttons — read from data-attribute (no inline JS with user data)
393
+ document.addEventListener('click',function(ev){
394
+ var btn=ev.target.closest('.cp-btn');
395
+ if(!btn)return;
396
+ navigator.clipboard.writeText(btn.dataset.prompt||'');
397
+ btn.textContent='\\u2713';setTimeout(function(){btn.textContent='\\ud83d\\udccb'},1000);
398
+ });
393
399
  // Init: show overview
394
400
  document.querySelector('.tn').classList.add('active');
395
401
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodeqa/cli",
3
- "version": "0.12.1",
3
+ "version": "0.13.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": {