@vibecodeqa/cli 0.22.0 → 0.23.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
@@ -31,17 +31,22 @@ import { computeTrend, formatTrend } from "./trend.js";
31
31
  import { gradeFromScore } from "./types.js";
32
32
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
33
33
  const VERSION = pkg.version;
34
- const args = process.argv.slice(2);
35
- const flags = new Set(args.filter((a) => a.startsWith("--")));
36
- const cwd = resolve(args.find((a) => !a.startsWith("--")) || ".");
37
- const outputDir = join(cwd, ".vibe-check");
38
- const jsonOnly = flags.has("--json");
39
- const ciMode = flags.has("--ci");
40
- const skipTests = flags.has("--skip-tests");
41
- const watchMode = flags.has("--watch");
42
- const badgeMode = flags.has("--badge");
43
- const sarifMode = flags.has("--sarif");
44
- const uploadMode = flags.has("--upload");
34
+ function parseFlags() {
35
+ const args = process.argv.slice(2);
36
+ const flags = new Set(args.filter((a) => a.startsWith("--")));
37
+ const cwd = resolve(args.find((a) => !a.startsWith("--")) || ".");
38
+ return {
39
+ cwd,
40
+ outputDir: join(cwd, ".vibe-check"),
41
+ jsonOnly: flags.has("--json"),
42
+ ciMode: flags.has("--ci"),
43
+ skipTests: flags.has("--skip-tests"),
44
+ watchMode: flags.has("--watch"),
45
+ badgeMode: flags.has("--badge"),
46
+ sarifMode: flags.has("--sarif"),
47
+ uploadMode: flags.has("--upload"),
48
+ };
49
+ }
45
50
  function color(grade) {
46
51
  if (grade === "A")
47
52
  return "\x1b[32m";
@@ -49,23 +54,7 @@ function color(grade) {
49
54
  return "\x1b[33m";
50
55
  return "\x1b[31m";
51
56
  }
52
- async function main() {
53
- const start = Date.now();
54
- if (!jsonOnly) {
55
- console.log("");
56
- console.log(` \x1b[1m\x1b[38;5;141mvcqa\x1b[0m v${VERSION}`);
57
- console.log(` \x1b[2m${cwd}\x1b[0m`);
58
- console.log("");
59
- }
60
- const stack = detectStack(cwd);
61
- if (!jsonOnly) {
62
- const parts = [stack.language, stack.framework, stack.bundler, stack.testRunner, stack.linter, stack.packageManager].filter((v) => v !== "none" && v !== "unknown");
63
- console.log(` stack: ${parts.join(" + ")}`);
64
- console.log("");
65
- }
66
- const checks = [];
67
- const isDart = stack.language === "dart";
68
- // All runners grouped by category
57
+ function runChecks(cwd, stack, skipTests, isDart, jsonOnly) {
69
58
  const runners = [
70
59
  // Foundations
71
60
  { name: "structure", fn: () => runStructure(cwd, stack) },
@@ -97,6 +86,7 @@ async function main() {
97
86
  { name: "doc-coherence", fn: () => runDocCoherence(cwd) },
98
87
  { name: "code-coherence", fn: () => runCodeCoherence(cwd) },
99
88
  ];
89
+ const checks = [];
100
90
  for (const runner of runners) {
101
91
  if (!jsonOnly)
102
92
  process.stdout.write(` ${runner.name.padEnd(14)}`);
@@ -113,20 +103,9 @@ async function main() {
113
103
  console.log(`${c}${label.padEnd(5)}${scoreStr}\x1b[0m \x1b[2m${result.duration}ms\x1b[0m${issueStr}`);
114
104
  }
115
105
  }
116
- const score = computeScore(checks);
117
- const grade = gradeFromScore(score);
118
- const duration = Date.now() - start;
119
- const totalIssues = checks.reduce((s, c) => s + c.issues.length, 0);
120
- const report = {
121
- version: VERSION,
122
- timestamp: new Date().toISOString(),
123
- score,
124
- grade,
125
- checks,
126
- meta: { cwd, node: process.version, duration, stack, ...detectRepoUrl(cwd) },
127
- };
128
- // Trend comparison (read previous report before overwriting)
129
- const trend = computeTrend(report, outputDir);
106
+ return checks;
107
+ }
108
+ async function writeOutputs(report, outputDir, flags) {
130
109
  if (!existsSync(outputDir))
131
110
  mkdirSync(outputDir, { recursive: true });
132
111
  // Save to history before overwriting current report
@@ -159,58 +138,127 @@ async function main() {
159
138
  writeFileSync(join(reportDir, filename), html);
160
139
  }
161
140
  // Badge SVG
162
- if (badgeMode) {
141
+ if (flags.badgeMode) {
163
142
  const { buildBadge } = await import("./report/svg.js");
164
- const badgeSvg = buildBadge(score, grade);
143
+ const badgeSvg = buildBadge(report.score, report.grade);
165
144
  writeFileSync(join(outputDir, "badge.svg"), badgeSvg);
166
145
  }
167
146
  // SARIF output for GitHub Code Scanning
168
- if (sarifMode) {
147
+ if (flags.sarifMode) {
169
148
  const { generateSARIF } = await import("./report/sarif.js");
170
149
  writeFileSync(join(outputDir, "report.sarif"), generateSARIF(report));
171
150
  }
172
- // Upload to VibeCode QA dashboard
173
- if (uploadMode) {
174
- const repo = report.meta.repoUrl?.replace(/^https:\/\/github\.com\//, "") || cwd.split("/").pop() || "project";
175
- const token = process.env.VCQA_TOKEN || "";
176
- try {
177
- const res = await fetch("https://api.vibecodeqa.online/api/reports", {
178
- method: "POST",
179
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
180
- body: JSON.stringify({ repo, report }),
181
- });
182
- if (res.ok) {
183
- const data = (await res.json());
184
- if (!jsonOnly)
185
- console.log(` \x1b[32m\u2713 Uploaded to dashboard\x1b[0m \x1b[2m(${data.totalReports || 1} reports)\x1b[0m`);
186
- }
187
- else if (!jsonOnly) {
188
- console.log(` \x1b[33m\u26a0 Upload failed: ${res.status}\x1b[0m \x1b[2m(set VCQA_TOKEN env var)\x1b[0m`);
189
- }
190
- }
191
- catch {
192
- if (!jsonOnly)
193
- console.log(` \x1b[33m\u26a0 Upload failed (network error)\x1b[0m`);
194
- }
195
- }
196
- if (jsonOnly) {
151
+ }
152
+ function printResults(report, trend, flags, outputDir) {
153
+ const { score, grade, checks } = report;
154
+ const totalIssues = checks.reduce((s, c) => s + c.issues.length, 0);
155
+ if (flags.jsonOnly) {
197
156
  console.log(JSON.stringify(report));
198
157
  }
199
158
  else {
200
159
  const gc = color(grade);
201
160
  console.log("");
202
- console.log(` ${gc}\x1b[1m${grade}\x1b[0m ${gc}${score}/100\x1b[0m \x1b[2m${checks.length} checks · ${totalIssues} issues · ${duration}ms\x1b[0m`);
161
+ console.log(` ${gc}\x1b[1m${grade}\x1b[0m ${gc}${score}/100\x1b[0m \x1b[2m${checks.length} checks · ${totalIssues} issues · ${report.meta.duration}ms\x1b[0m`);
203
162
  if (trend)
204
163
  console.log(formatTrend(trend));
205
164
  console.log("");
206
165
  console.log(` \x1b[2mReport: ${join(outputDir, "report/index.html")}\x1b[0m`);
207
166
  console.log(` \x1b[2mJSON: ${join(outputDir, "report.json")}\x1b[0m`);
208
- if (badgeMode)
167
+ if (flags.badgeMode)
209
168
  console.log(` \x1b[2mBadge: ${join(outputDir, "badge.svg")}\x1b[0m`);
210
- if (sarifMode)
169
+ if (flags.sarifMode)
211
170
  console.log(` \x1b[2mSARIF: ${join(outputDir, "report.sarif")}\x1b[0m`);
212
171
  console.log("");
213
172
  }
173
+ }
174
+ async function handleUpload(report, cwd, jsonOnly) {
175
+ const repo = report.meta.repoUrl?.replace(/^https:\/\/github\.com\//, "") || cwd.split("/").pop() || "project";
176
+ const token = process.env.VCQA_TOKEN || "";
177
+ try {
178
+ const res = await fetch("https://api.vibecodeqa.online/api/reports", {
179
+ method: "POST",
180
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
181
+ body: JSON.stringify({ repo, report }),
182
+ });
183
+ if (res.ok) {
184
+ const data = (await res.json());
185
+ if (!jsonOnly)
186
+ console.log(` \x1b[32m\u2713 Uploaded to dashboard\x1b[0m \x1b[2m(${data.totalReports || 1} reports)\x1b[0m`);
187
+ }
188
+ else if (!jsonOnly) {
189
+ console.log(` \x1b[33m\u26a0 Upload failed: ${res.status}\x1b[0m \x1b[2m(set VCQA_TOKEN env var)\x1b[0m`);
190
+ }
191
+ }
192
+ catch {
193
+ if (!jsonOnly)
194
+ console.log(` \x1b[33m\u26a0 Upload failed (network error)\x1b[0m`);
195
+ }
196
+ }
197
+ async function startWatch(cwd) {
198
+ const { watch } = await import("node:fs");
199
+ const srcDirs = ["src", "web/src"].map((d) => join(cwd, d)).filter((d) => existsSync(d));
200
+ if (srcDirs.length === 0) {
201
+ console.log(" \x1b[31mNo src/ directory to watch\x1b[0m");
202
+ process.exit(1);
203
+ }
204
+ console.log(" \x1b[2mWatching for changes... (Ctrl+C to stop)\x1b[0m");
205
+ console.log("");
206
+ let debounce = null;
207
+ let running = false;
208
+ for (const dir of srcDirs) {
209
+ watch(dir, { recursive: true }, (_event, filename) => {
210
+ if (!filename || filename.includes("node_modules") || filename.includes(".vibe-check"))
211
+ return;
212
+ if (running)
213
+ return;
214
+ if (debounce)
215
+ clearTimeout(debounce);
216
+ debounce = setTimeout(async () => {
217
+ running = true;
218
+ console.log(` \x1b[2mChanged: ${filename} — re-scanning...\x1b[0m`);
219
+ await main().catch(() => { });
220
+ running = false;
221
+ }, 500);
222
+ });
223
+ }
224
+ // Keep process alive
225
+ await new Promise(() => { });
226
+ }
227
+ async function main() {
228
+ const flags = parseFlags();
229
+ const { cwd, outputDir, jsonOnly, ciMode, skipTests, watchMode } = flags;
230
+ const start = Date.now();
231
+ if (!jsonOnly) {
232
+ console.log("");
233
+ console.log(` \x1b[1m\x1b[38;5;141mvcqa\x1b[0m v${VERSION}`);
234
+ console.log(` \x1b[2m${cwd}\x1b[0m`);
235
+ console.log("");
236
+ }
237
+ const stack = detectStack(cwd);
238
+ if (!jsonOnly) {
239
+ const parts = [stack.language, stack.framework, stack.bundler, stack.testRunner, stack.linter, stack.packageManager].filter((v) => v !== "none" && v !== "unknown");
240
+ console.log(` stack: ${parts.join(" + ")}`);
241
+ console.log("");
242
+ }
243
+ const isDart = stack.language === "dart";
244
+ const checks = runChecks(cwd, stack, skipTests, isDart, jsonOnly);
245
+ const score = computeScore(checks);
246
+ const grade = gradeFromScore(score);
247
+ const duration = Date.now() - start;
248
+ const report = {
249
+ version: VERSION,
250
+ timestamp: new Date().toISOString(),
251
+ score,
252
+ grade,
253
+ checks,
254
+ meta: { cwd, node: process.version, duration, stack, ...detectRepoUrl(cwd) },
255
+ };
256
+ const trend = computeTrend(report, outputDir);
257
+ await writeOutputs(report, outputDir, flags);
258
+ printResults(report, trend, flags, outputDir);
259
+ if (flags.uploadMode) {
260
+ await handleUpload(report, cwd, jsonOnly);
261
+ }
214
262
  if (ciMode && score < 60) {
215
263
  process.exit(1);
216
264
  }
@@ -224,36 +272,8 @@ async function main() {
224
272
  /* failed to open browser */
225
273
  }
226
274
  }
227
- // Watch mode — re-run on file changes
228
275
  if (watchMode) {
229
- const { watch } = await import("node:fs");
230
- const srcDirs = ["src", "web/src"].map((d) => join(cwd, d)).filter((d) => existsSync(d));
231
- if (srcDirs.length === 0) {
232
- console.log(" \x1b[31mNo src/ directory to watch\x1b[0m");
233
- process.exit(1);
234
- }
235
- console.log(" \x1b[2mWatching for changes... (Ctrl+C to stop)\x1b[0m");
236
- console.log("");
237
- let debounce = null;
238
- let running = false;
239
- for (const dir of srcDirs) {
240
- watch(dir, { recursive: true }, (_event, filename) => {
241
- if (!filename || filename.includes("node_modules") || filename.includes(".vibe-check"))
242
- return;
243
- if (running)
244
- return; // prevent concurrent re-runs (M5)
245
- if (debounce)
246
- clearTimeout(debounce);
247
- debounce = setTimeout(async () => {
248
- running = true;
249
- console.log(` \x1b[2mChanged: ${filename} — re-scanning...\x1b[0m`);
250
- await main().catch(() => { });
251
- running = false;
252
- }, 500);
253
- });
254
- }
255
- // Keep process alive
256
- await new Promise(() => { });
276
+ await startWatch(cwd);
257
277
  }
258
278
  }
259
279
  main().catch((err) => {
@@ -13,21 +13,10 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { readDeps } from "../fs-utils.js";
15
15
  import { gradeFromScore } from "../types.js";
16
- export function runBestPractices(cwd) {
17
- const start = Date.now();
16
+ function checkCICD(cwd, has, read) {
18
17
  const issues = [];
19
18
  let practices = 0;
20
19
  let followed = 0;
21
- const has = (f) => existsSync(join(cwd, f));
22
- const read = (f) => {
23
- try {
24
- return readFileSync(join(cwd, f), "utf-8");
25
- }
26
- catch {
27
- return "";
28
- }
29
- };
30
- // ── 1. CI/CD Best Practices ──
31
20
  // Check for GitHub Actions workflows
32
21
  const hasWorkflows = has(".github/workflows");
33
22
  practices++;
@@ -99,7 +88,12 @@ export function runBestPractices(cwd) {
99
88
  rule: "no-ci",
100
89
  });
101
90
  }
102
- // ── 2. Supply Chain ──
91
+ return { practices, followed, issues };
92
+ }
93
+ function checkSupplyChain(has, read) {
94
+ const issues = [];
95
+ let practices = 0;
96
+ let followed = 0;
103
97
  // Lockfile committed
104
98
  practices++;
105
99
  const hasLockfile = has("pnpm-lock.yaml") || has("package-lock.json") || has("yarn.lock") || has("bun.lockb") || has("pubspec.lock");
@@ -136,7 +130,12 @@ export function runBestPractices(cwd) {
136
130
  });
137
131
  }
138
132
  }
139
- // ── 3. Repo Hygiene ──
133
+ return { practices, followed, issues };
134
+ }
135
+ function checkRepoHygiene(has) {
136
+ const issues = [];
137
+ let practices = 0;
138
+ let followed = 0;
140
139
  // SECURITY.md or security policy
141
140
  practices++;
142
141
  if (has("SECURITY.md") || has(".github/SECURITY.md")) {
@@ -165,7 +164,12 @@ export function runBestPractices(cwd) {
165
164
  rule: "contributing-guide",
166
165
  });
167
166
  }
168
- // ── 4. Developer Experience ──
167
+ return { practices, followed, issues };
168
+ }
169
+ function checkDevExperience(cwd, has) {
170
+ const issues = [];
171
+ let practices = 0;
172
+ let followed = 0;
169
173
  // .env.example
170
174
  practices++;
171
175
  const hasEnvFiles = has(".env") || has(".env.local") || has(".env.development");
@@ -204,7 +208,12 @@ export function runBestPractices(cwd) {
204
208
  rule: "automated-deps",
205
209
  });
206
210
  }
207
- // ── 5. Code Quality Tooling ──
211
+ return { practices, followed, issues };
212
+ }
213
+ function checkCodeQualityTooling(has, read) {
214
+ const issues = [];
215
+ let practices = 0;
216
+ let followed = 0;
208
217
  // Linter configured
209
218
  practices++;
210
219
  if (has("biome.json") ||
@@ -247,7 +256,13 @@ export function runBestPractices(cwd) {
247
256
  rule: "ts-strict-mode",
248
257
  });
249
258
  }
250
- // ── 6. Testing Best Practices ──
259
+ return { practices, followed, issues };
260
+ }
261
+ function checkTesting(has, read) {
262
+ const issues = [];
263
+ let practices = 0;
264
+ let followed = 0;
265
+ const pkg = read("package.json");
251
266
  // Test script exists
252
267
  practices++;
253
268
  if (pkg.includes('"test"') || has("pubspec.yaml")) {
@@ -268,7 +283,12 @@ export function runBestPractices(cwd) {
268
283
  rule: "coverage-config",
269
284
  });
270
285
  }
271
- // ── 7. Docker / Deployment ──
286
+ return { practices, followed, issues };
287
+ }
288
+ function checkDocker(has, read) {
289
+ const issues = [];
290
+ let practices = 0;
291
+ let followed = 0;
272
292
  // Dockerfile best practices (if Docker is used)
273
293
  if (has("Dockerfile") || has("docker-compose.yml") || has("docker-compose.yaml")) {
274
294
  practices++;
@@ -315,7 +335,13 @@ export function runBestPractices(cwd) {
315
335
  });
316
336
  }
317
337
  }
318
- // ── 8. Git Practices ──
338
+ return { practices, followed, issues };
339
+ }
340
+ function checkGitPractices(cwd, has, read) {
341
+ const issues = [];
342
+ let practices = 0;
343
+ let followed = 0;
344
+ const deps = readDeps(cwd);
319
345
  // .gitignore is comprehensive
320
346
  practices++;
321
347
  const gitignore = read(".gitignore");
@@ -344,7 +370,13 @@ export function runBestPractices(cwd) {
344
370
  rule: "conventional-commits",
345
371
  });
346
372
  }
347
- // ── 9. Monitoring & Observability ──
373
+ return { practices, followed, issues };
374
+ }
375
+ function checkMonitoring(cwd) {
376
+ const issues = [];
377
+ let practices = 0;
378
+ let followed = 0;
379
+ const deps = readDeps(cwd);
348
380
  // Error tracking (Sentry, Bugsnag, etc.) — only for apps/servers, not CLI tools
349
381
  const isApp = deps.react || deps.vue || deps.svelte || deps.express || deps.fastify || deps.hono || deps.next || deps.nuxt;
350
382
  if (isApp) {
@@ -360,7 +392,14 @@ export function runBestPractices(cwd) {
360
392
  });
361
393
  }
362
394
  }
363
- // ── 10. API & Configuration ──
395
+ return { practices, followed, issues };
396
+ }
397
+ function checkAPIConfig(cwd, read) {
398
+ const issues = [];
399
+ let practices = 0;
400
+ let followed = 0;
401
+ const deps = readDeps(cwd);
402
+ const pkg = read("package.json");
364
403
  // Environment validation (zod, joi, envalid)
365
404
  practices++;
366
405
  if (deps.zod || deps.joi || deps.envalid || deps["@t3-oss/env-core"] || deps["@t3-oss/env-nextjs"]) {
@@ -379,6 +418,39 @@ export function runBestPractices(cwd) {
379
418
  followed++;
380
419
  }
381
420
  }
421
+ return { practices, followed, issues };
422
+ }
423
+ export function runBestPractices(cwd) {
424
+ const start = Date.now();
425
+ const has = (f) => existsSync(join(cwd, f));
426
+ const read = (f) => {
427
+ try {
428
+ return readFileSync(join(cwd, f), "utf-8");
429
+ }
430
+ catch {
431
+ return "";
432
+ }
433
+ };
434
+ const categories = [
435
+ checkCICD(cwd, has, read),
436
+ checkSupplyChain(has, read),
437
+ checkRepoHygiene(has),
438
+ checkDevExperience(cwd, has),
439
+ checkCodeQualityTooling(has, read),
440
+ checkTesting(has, read),
441
+ checkDocker(has, read),
442
+ checkGitPractices(cwd, has, read),
443
+ checkMonitoring(cwd),
444
+ checkAPIConfig(cwd, read),
445
+ ];
446
+ let practices = 0;
447
+ let followed = 0;
448
+ const issues = [];
449
+ for (const cat of categories) {
450
+ practices += cat.practices;
451
+ followed += cat.followed;
452
+ issues.push(...cat.issues);
453
+ }
382
454
  // ── Score ──
383
455
  const pct = practices > 0 ? Math.round((followed / practices) * 100) : 100;
384
456
  const score = pct;
@@ -28,7 +28,7 @@ const CODE_SMELLS = [
28
28
  message: "dangerouslySetInnerHTML bypasses React's XSS protection",
29
29
  },
30
30
  { name: "document.write", pattern: /document\.write\s*\(/, severity: "error", message: "document.write blocks rendering" },
31
- { name: "http:// URL", pattern: /['"]http:\/\/(?!localhost|127\.0\.0\.1)/, severity: "warning", message: "Non-HTTPS URL — use https://" },
31
+ { name: "http:// URL", pattern: /['"]http:\/\/(?!localhost|127\.0\.0\.1|www\.w3\.org|schemas?\.)/, severity: "warning", message: "Non-HTTPS URL — use https://" },
32
32
  { name: "TODO/FIXME", pattern: /\b(TODO|FIXME|HACK|XXX)\b/, severity: "warning", message: "Unresolved TODO/FIXME comment" },
33
33
  {
34
34
  name: "magic number",
@@ -96,6 +96,9 @@ export function runStandards(cwd, stack) {
96
96
  continue;
97
97
  if (/\bpattern\s*:|name:\s*["']|message:\s*["']|description:\s*["']|risk:\s*["']|recommendation:\s*["']/.test(trimmed))
98
98
  continue;
99
+ // Skip string-only lines (check-meta descriptions, inline scripts)
100
+ if (/^\s*["'`].*["'`][,;]?\s*$/.test(line))
101
+ continue;
99
102
  for (const check of CODE_SMELLS) {
100
103
  // Skip console.log in CLI entry points (intentional output)
101
104
  if (check.name === "console.log" && (f.path.includes("cli.") || f.path.includes("bin/")))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodeqa/cli",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Code health scanner for the AI coding era. 21 checks, zero config, full report.",
5
5
  "type": "module",
6
6
  "bin": {