@vibecodeqa/cli 0.42.0 → 0.43.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 +130 -165
- package/dist/check-meta.js +59 -6
- package/dist/cli.js +268 -761
- package/dist/commands/explain.d.ts +2 -0
- package/dist/commands/explain.js +33 -0
- package/dist/commands/fix.d.ts +6 -0
- package/dist/commands/fix.js +131 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +96 -0
- package/dist/commands/shared.d.ts +4 -0
- package/dist/commands/shared.js +80 -0
- package/dist/core.js +10 -0
- package/dist/runners/accessibility.js +4 -1
- package/dist/runners/design-consistency.d.ts +12 -0
- package/dist/runners/design-consistency.js +125 -0
- package/dist/runners/error-handling.js +18 -2
- package/dist/runners/file-cohesion.d.ts +17 -0
- package/dist/runners/file-cohesion.js +177 -0
- package/dist/runners/frontend-health.d.ts +14 -0
- package/dist/runners/frontend-health.js +206 -0
- package/dist/runners/html-quality.d.ts +8 -0
- package/dist/runners/html-quality.js +203 -0
- package/dist/runners/react.js +1 -0
- package/dist/runners/secrets.js +7 -2
- package/dist/runners/security.js +7 -1
- package/dist/runners/standards.js +29 -9
- package/dist/runners/styling.d.ts +15 -0
- package/dist/runners/styling.js +280 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,52 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/** vibe-check — code health scanner for the AI coding era. */
|
|
3
|
-
import { existsSync, mkdirSync,
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
5
|
+
import { runExplain } from "./commands/explain.js";
|
|
6
|
+
import { runFix } from "./commands/fix.js";
|
|
7
|
+
import { runInit } from "./commands/init.js";
|
|
8
|
+
import { validateCwd } from "./commands/shared.js";
|
|
9
|
+
import { loadConfig } from "./config.js";
|
|
10
|
+
import { scan } from "./core.js";
|
|
11
|
+
import { detectStack, detectWorkspace } from "./detect.js";
|
|
10
12
|
import { postPRComment } from "./pr-comment.js";
|
|
11
13
|
import { generatePages } from "./report/html.js";
|
|
12
|
-
import { runAccessibility } from "./runners/accessibility.js";
|
|
13
|
-
import { runArchitecture } from "./runners/architecture.js";
|
|
14
|
-
import { runBestPractices } from "./runners/best-practices.js";
|
|
15
|
-
import { runCodeCoherence } from "./runners/code-coherence.js";
|
|
16
|
-
import { runCommentStaleness } from "./runners/comment-staleness.js";
|
|
17
|
-
import { runComplexity } from "./runners/complexity.js";
|
|
18
|
-
import { runDeadPatterns } from "./runners/dead-patterns.js";
|
|
19
|
-
import { runTestAudit } from "./runners/test-audit.js";
|
|
20
|
-
import { runConfusion } from "./runners/confusion.js";
|
|
21
|
-
import { runContainerHealth } from "./runners/container-health.js";
|
|
22
|
-
import { runContext } from "./runners/context.js";
|
|
23
|
-
import { runDependencies } from "./runners/dependencies.js";
|
|
24
|
-
import { runEnvValidation } from "./runners/env-validation.js";
|
|
25
|
-
import { runGitHygiene } from "./runners/git-hygiene.js";
|
|
26
|
-
import { runDocCoherence } from "./runners/doc-coherence.js";
|
|
27
|
-
import { runDocs } from "./runners/docs.js";
|
|
28
|
-
import { runDuplication } from "./runners/duplication.js";
|
|
29
|
-
import { runErrorHandling } from "./runners/error-handling.js";
|
|
30
|
-
import { runLint } from "./runners/lint.js";
|
|
31
|
-
import { runMemorySafety } from "./runners/memory-safety.js";
|
|
32
|
-
import { runPerformance } from "./runners/performance.js";
|
|
33
|
-
import { runReact } from "./runners/react.js";
|
|
34
|
-
import { runSecrets } from "./runners/secrets.js";
|
|
35
|
-
import { runSecurity } from "./runners/security.js";
|
|
36
|
-
import { runStandards } from "./runners/standards.js";
|
|
37
|
-
import { runStructure } from "./runners/structure.js";
|
|
38
|
-
import { runTesting } from "./runners/testing.js";
|
|
39
|
-
import { runTypeSafety } from "./runners/type-safety.js";
|
|
40
|
-
import { runTypeCheck } from "./runners/types-check.js";
|
|
41
|
-
import { computeScore } from "./score.js";
|
|
42
14
|
import { computeTrend, formatTrend } from "./trend.js";
|
|
43
|
-
import { gradeFromScore } from "./types.js";
|
|
44
15
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
45
16
|
const VERSION = pkg.version;
|
|
46
17
|
function parseFlags() {
|
|
47
18
|
const args = process.argv.slice(2);
|
|
48
19
|
const flags = new Set(args.filter((a) => a.startsWith("--")));
|
|
49
|
-
// Parse flags with value arguments
|
|
50
20
|
const valueArgIndices = new Set();
|
|
51
21
|
function parseValueFlag(flag, fallback) {
|
|
52
22
|
const idx = args.indexOf(flag);
|
|
@@ -58,14 +28,12 @@ function parseFlags() {
|
|
|
58
28
|
valueArgIndices.add(idx + 1);
|
|
59
29
|
return parseInt(next, 10);
|
|
60
30
|
}
|
|
61
|
-
// Non-numeric value after flag — consume it to prevent misuse as cwd
|
|
62
31
|
valueArgIndices.add(idx + 1);
|
|
63
32
|
}
|
|
64
33
|
return fallback ?? null;
|
|
65
34
|
}
|
|
66
35
|
const topN = parseValueFlag("--top", 5) ?? 0;
|
|
67
36
|
const failUnder = parseValueFlag("--fail-under");
|
|
68
|
-
// --diff [base] — only show issues in changed files
|
|
69
37
|
let diffBase = null;
|
|
70
38
|
const diffIdx = args.indexOf("--diff");
|
|
71
39
|
if (diffIdx !== -1) {
|
|
@@ -75,7 +43,7 @@ function parseFlags() {
|
|
|
75
43
|
valueArgIndices.add(diffIdx + 1);
|
|
76
44
|
}
|
|
77
45
|
else {
|
|
78
|
-
diffBase = "HEAD";
|
|
46
|
+
diffBase = "HEAD";
|
|
79
47
|
}
|
|
80
48
|
}
|
|
81
49
|
const cwd = resolve(args.find((a, i) => !a.startsWith("--") && !valueArgIndices.has(i)) || ".");
|
|
@@ -97,6 +65,7 @@ function parseFlags() {
|
|
|
97
65
|
annotations: flags.has("--annotations"),
|
|
98
66
|
};
|
|
99
67
|
}
|
|
68
|
+
// ── Output helpers ──
|
|
100
69
|
function color(grade) {
|
|
101
70
|
if (grade === "A")
|
|
102
71
|
return "\x1b[32m";
|
|
@@ -104,136 +73,114 @@ function color(grade) {
|
|
|
104
73
|
return "\x1b[33m";
|
|
105
74
|
return "\x1b[31m";
|
|
106
75
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
if (!jsonOnly)
|
|
164
|
-
process.stdout.write(` ${runner.name.padEnd(14)}`);
|
|
165
|
-
let result;
|
|
166
|
-
try {
|
|
167
|
-
const maybeResult = runner.fn();
|
|
168
|
-
result = maybeResult instanceof Promise ? await maybeResult : maybeResult;
|
|
169
|
-
}
|
|
170
|
-
catch (err) {
|
|
171
|
-
// Runner crashed — record as errored, don't kill the scan
|
|
172
|
-
result = {
|
|
173
|
-
name: runner.name,
|
|
174
|
-
score: 0,
|
|
175
|
-
grade: "F",
|
|
176
|
-
details: { skipped: true, reason: `runner error: ${err instanceof Error ? err.message : "unknown"}` },
|
|
177
|
-
issues: [],
|
|
178
|
-
duration: 0,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
checks.push(result);
|
|
182
|
-
if (!jsonOnly) {
|
|
183
|
-
const det = result.details;
|
|
184
|
-
const skipped = det.skipped;
|
|
185
|
-
const premium = det.comingSoon;
|
|
186
|
-
const c = premium ? "\x1b[2m" : skipped ? "\x1b[2m" : color(result.grade);
|
|
187
|
-
const label = premium ? "soon" : skipped ? "skip" : result.grade;
|
|
188
|
-
const scoreStr = premium ? "PRO" : skipped ? "—" : `${result.score}/100`;
|
|
189
|
-
const issueStr = result.issues.length > 0 ? ` \x1b[2m${result.issues.length} issues\x1b[0m` : "";
|
|
190
|
-
console.log(`${c}${label.padEnd(5)}${scoreStr}\x1b[0m \x1b[2m${result.duration}ms\x1b[0m${issueStr}`);
|
|
76
|
+
function printHelp() {
|
|
77
|
+
console.log(`
|
|
78
|
+
\x1b[1m\x1b[38;5;141mvcqa\x1b[0m v${VERSION} — code health scanner
|
|
79
|
+
|
|
80
|
+
\x1b[1mUsage:\x1b[0m npx @vibecodeqa/cli [command] [path] [flags]
|
|
81
|
+
|
|
82
|
+
\x1b[1mCommands:\x1b[0m
|
|
83
|
+
init [path] Set up CI workflow + recommended configs
|
|
84
|
+
fix [path] Auto-fix (.gitignore, strict mode, biome/eslint, suggestions)
|
|
85
|
+
--ai Use Claude to fix remaining issues (needs ANTHROPIC_API_KEY)
|
|
86
|
+
--check NAME Only fix issues from a specific check (e.g. --check security)
|
|
87
|
+
--dry-run Show what AI would fix without applying changes
|
|
88
|
+
explain [check] Deep-dive explanation of a check (what/risk/fix)
|
|
89
|
+
monitor [path] Live quality control panel — re-scans on file changes
|
|
90
|
+
|
|
91
|
+
\x1b[1mFlags:\x1b[0m
|
|
92
|
+
--skip-tests Skip test execution (faster scan)
|
|
93
|
+
--ci CI mode (exit 1 if score < 60)
|
|
94
|
+
--fail-under N Exit 1 if score below N (e.g. --fail-under 80)
|
|
95
|
+
--json Output JSON only (no terminal UI)
|
|
96
|
+
--badge Generate SVG badge
|
|
97
|
+
--sarif Generate SARIF for GitHub Code Scanning
|
|
98
|
+
--upload Upload report to app.vibecodeqa.online
|
|
99
|
+
--top [N] Show top N issues to fix (default: 5)
|
|
100
|
+
--diff [base] Only show issues in changed files (vs HEAD or branch)
|
|
101
|
+
--markdown Output markdown summary (pipe to file or clipboard)
|
|
102
|
+
--pr-comment Post score as GitHub PR comment (needs GITHUB_TOKEN)
|
|
103
|
+
--annotations Emit GitHub Actions ::warning/::error annotations
|
|
104
|
+
--watch Re-scan on file changes
|
|
105
|
+
-v, --version Print version
|
|
106
|
+
-h, --help Show this help
|
|
107
|
+
|
|
108
|
+
\x1b[1mExamples:\x1b[0m
|
|
109
|
+
npx @vibecodeqa/cli # scan current directory
|
|
110
|
+
npx @vibecodeqa/cli init # set up CI + configs
|
|
111
|
+
npx @vibecodeqa/cli fix # auto-fix what's fixable
|
|
112
|
+
npx @vibecodeqa/cli fix --ai # AI-powered fix (uses Claude)
|
|
113
|
+
npx @vibecodeqa/cli fix --ai --check security # fix only security issues
|
|
114
|
+
npx @vibecodeqa/cli fix --ai --dry-run # preview AI fixes without applying
|
|
115
|
+
npx @vibecodeqa/cli --skip-tests --top # fast scan with top issues
|
|
116
|
+
npx @vibecodeqa/cli --ci --fail-under 80 # CI with quality gate
|
|
117
|
+
`);
|
|
118
|
+
}
|
|
119
|
+
function printHeader(cwd, stack, workspace) {
|
|
120
|
+
console.log("");
|
|
121
|
+
console.log(` \x1b[1m\x1b[38;5;141mvcqa\x1b[0m v${VERSION}`);
|
|
122
|
+
console.log(` \x1b[2m${cwd}\x1b[0m`);
|
|
123
|
+
console.log("");
|
|
124
|
+
const parts = [stack.language, stack.framework, stack.bundler, stack.testRunner, stack.linter, stack.packageManager].filter((v) => v !== "none" && v !== "unknown");
|
|
125
|
+
console.log(` stack: ${parts.join(" + ")}`);
|
|
126
|
+
if (workspace.isMonorepo) {
|
|
127
|
+
console.log(` workspace: ${workspace.tool} monorepo — ${workspace.packages.length} packages`);
|
|
128
|
+
for (const p of workspace.packages.slice(0, 8)) {
|
|
129
|
+
const f = [p.hasSrc && "src", p.hasTests && "tests", p.hasLinter && "linter"].filter(Boolean).join(", ");
|
|
130
|
+
console.log(` \x1b[2m${p.path}\x1b[0m (${f || "empty"})`);
|
|
191
131
|
}
|
|
132
|
+
if (workspace.packages.length > 8)
|
|
133
|
+
console.log(` \x1b[2m...and ${workspace.packages.length - 8} more\x1b[0m`);
|
|
192
134
|
}
|
|
193
|
-
|
|
135
|
+
console.log("");
|
|
194
136
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
.sort();
|
|
208
|
-
if (historyFiles.length > 30) {
|
|
209
|
-
for (const old of historyFiles.slice(0, historyFiles.length - 30)) {
|
|
210
|
-
try {
|
|
211
|
-
unlinkSync(join(historyDir, old));
|
|
212
|
-
}
|
|
213
|
-
catch {
|
|
214
|
-
/* ignore */
|
|
215
|
-
}
|
|
216
|
-
}
|
|
137
|
+
function generateMarkdown(report, trend) {
|
|
138
|
+
const { score, grade, checks } = report;
|
|
139
|
+
const gradeEmoji = grade === "A" ? "🟢" : grade === "B" ? "🟡" : grade === "C" ? "🟠" : "🔴";
|
|
140
|
+
let md = `# ${gradeEmoji} VibeCode QA: ${grade} ${score}/100\n\n`;
|
|
141
|
+
if (trend) {
|
|
142
|
+
const arrow = trend.scoreDelta > 0 ? "📈" : trend.scoreDelta < 0 ? "📉" : "➡️";
|
|
143
|
+
md += `${arrow} **${trend.scoreDelta > 0 ? "+" : ""}${trend.scoreDelta}** vs previous`;
|
|
144
|
+
if (trend.fixedIssues > 0)
|
|
145
|
+
md += ` · ${trend.fixedIssues} fixed`;
|
|
146
|
+
if (trend.newIssues > 0)
|
|
147
|
+
md += ` · ${trend.newIssues} new`;
|
|
148
|
+
md += "\n\n";
|
|
217
149
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
writeFileSync(join(reportDir, filename), html);
|
|
150
|
+
md += "| Check | Score | Grade |\n|-------|-------|-------|\n";
|
|
151
|
+
for (const c of checks) {
|
|
152
|
+
const det = c.details;
|
|
153
|
+
if (det.skipped || det.comingSoon)
|
|
154
|
+
continue;
|
|
155
|
+
const emoji = c.score >= 90 ? "🟢" : c.score >= 75 ? "🟡" : c.score >= 60 ? "🟠" : "🔴";
|
|
156
|
+
md += `| ${emoji} ${c.name} | ${c.score}/100 | ${c.grade} |\n`;
|
|
226
157
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
158
|
+
const errors = checks.flatMap((c) => c.issues.filter((i) => i.severity === "error"));
|
|
159
|
+
const warnings = checks.flatMap((c) => c.issues.filter((i) => i.severity === "warning"));
|
|
160
|
+
if (errors.length + warnings.length > 0) {
|
|
161
|
+
md += `\n## Issues (${errors.length} errors, ${warnings.length} warnings)\n\n`;
|
|
162
|
+
for (const i of [...errors, ...warnings].slice(0, 15)) {
|
|
163
|
+
const loc = i.file ? ` \`${i.file}${i.line ? `:${i.line}` : ""}\`` : "";
|
|
164
|
+
md += `- ${i.severity === "error" ? "❌" : "⚠️"} ${i.message}${loc}\n`;
|
|
165
|
+
}
|
|
166
|
+
const remaining = errors.length + warnings.length - 15;
|
|
167
|
+
if (remaining > 0)
|
|
168
|
+
md += `\n*...and ${remaining} more*\n`;
|
|
232
169
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
170
|
+
md += `\n---\n*vcqa v${report.version} · ${report.meta.duration}ms*\n`;
|
|
171
|
+
return md;
|
|
172
|
+
}
|
|
173
|
+
function emitAnnotations(report) {
|
|
174
|
+
for (const c of report.checks) {
|
|
175
|
+
for (const i of c.issues) {
|
|
176
|
+
if (i.severity === "info")
|
|
177
|
+
continue;
|
|
178
|
+
const level = i.severity === "error" ? "error" : "warning";
|
|
179
|
+
const file = i.file && typeof i.file === "string" ? i.file : "";
|
|
180
|
+
const line = i.line || "";
|
|
181
|
+
const loc = file ? ` file=${file}${line ? `,line=${line}` : ""}` : "";
|
|
182
|
+
console.log(`::${level}${loc ? loc : ""}::${c.name}: ${i.message}`);
|
|
183
|
+
}
|
|
237
184
|
}
|
|
238
185
|
}
|
|
239
186
|
async function printResults(report, trend, flags, outputDir, interactive) {
|
|
@@ -247,7 +194,6 @@ async function printResults(report, trend, flags, outputDir, interactive) {
|
|
|
247
194
|
console.log("");
|
|
248
195
|
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`);
|
|
249
196
|
if (trend) {
|
|
250
|
-
// Load history for sparkline
|
|
251
197
|
const historyDir = join(outputDir, "history");
|
|
252
198
|
const { loadHistory } = await import("./history.js");
|
|
253
199
|
const history = loadHistory(historyDir);
|
|
@@ -257,12 +203,10 @@ async function printResults(report, trend, flags, outputDir, interactive) {
|
|
|
257
203
|
console.log(formatTrend(trend, scores));
|
|
258
204
|
}
|
|
259
205
|
console.log("");
|
|
260
|
-
|
|
261
|
-
// in an interactive terminal so the scan never dead-ends at a file path.
|
|
206
|
+
const { getCheckMeta } = await import("./check-meta.js");
|
|
262
207
|
const effectiveTopN = flags.topN > 0 ? flags.topN : interactive ? 3 : 0;
|
|
263
208
|
if (effectiveTopN > 0) {
|
|
264
209
|
const allIssues = checks.flatMap((c) => c.issues.map((iss) => ({ check: c.name, weight: getCheckMeta(c.name).weight, ...iss })));
|
|
265
|
-
// Sort by: errors first, then by check weight (highest-impact first)
|
|
266
210
|
allIssues.sort((a, b) => {
|
|
267
211
|
const sevOrder = { error: 0, warning: 1, info: 2 };
|
|
268
212
|
const sevDiff = (sevOrder[a.severity] ?? 2) - (sevOrder[b.severity] ?? 2);
|
|
@@ -284,27 +228,21 @@ async function printResults(report, trend, flags, outputDir, interactive) {
|
|
|
284
228
|
console.log("");
|
|
285
229
|
}
|
|
286
230
|
}
|
|
287
|
-
// Next steps: surface the weakest scored dimensions and how to dig into each.
|
|
288
|
-
// Skipped in CI (clean machine-readable-ish output) — interactive runs get the on-ramp.
|
|
289
231
|
if (!flags.ciMode) {
|
|
290
232
|
const weakest = checks
|
|
291
|
-
.filter((c) => {
|
|
292
|
-
const det = c.details;
|
|
293
|
-
return !det.skipped && !det.comingSoon && c.score < 70;
|
|
294
|
-
})
|
|
233
|
+
.filter((c) => { const det = c.details; return !det.skipped && !det.comingSoon && c.score < 70; })
|
|
295
234
|
.sort((a, b) => a.score - b.score)
|
|
296
235
|
.slice(0, 3);
|
|
297
236
|
if (weakest.length > 0) {
|
|
298
237
|
console.log(" \x1b[1mWeakest areas:\x1b[0m");
|
|
299
238
|
for (const c of weakest) {
|
|
300
|
-
const
|
|
239
|
+
const gc2 = color(c.grade);
|
|
301
240
|
const label = getCheckMeta(c.name).label || c.name;
|
|
302
|
-
console.log(` ${
|
|
241
|
+
console.log(` ${gc2}${c.grade}\x1b[0m \x1b[2m${String(c.score).padStart(3)}\x1b[0m ${label.padEnd(18)}\x1b[2m→ vcqa explain ${c.name}\x1b[0m`);
|
|
303
242
|
}
|
|
304
243
|
console.log("");
|
|
305
244
|
}
|
|
306
245
|
}
|
|
307
|
-
// Report paths + the interactive on-ramp.
|
|
308
246
|
console.log(` \x1b[2mReport: ${join(outputDir, "report/index.html")}\x1b[0m`);
|
|
309
247
|
console.log(` \x1b[2mJSON: ${join(outputDir, "report.json")}\x1b[0m`);
|
|
310
248
|
if (flags.badgeMode)
|
|
@@ -317,18 +255,76 @@ async function printResults(report, trend, flags, outputDir, interactive) {
|
|
|
317
255
|
console.log("");
|
|
318
256
|
}
|
|
319
257
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
258
|
+
function getChangedFiles(cwd, base) {
|
|
259
|
+
try {
|
|
260
|
+
const { execSync } = require("node:child_process");
|
|
261
|
+
const cmd = base === "HEAD" ? "git diff --name-only" : `git diff --name-only ${base}...HEAD`;
|
|
262
|
+
const stdout = execSync(cmd, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
263
|
+
if (!stdout)
|
|
264
|
+
return new Set();
|
|
265
|
+
return new Set(stdout.split("\n").filter((f) => f.length > 0));
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// ── Report output ──
|
|
272
|
+
async function writeOutputs(report, outputDir, flags) {
|
|
273
|
+
mkdirSync(outputDir, { recursive: true });
|
|
274
|
+
// Always write JSON
|
|
275
|
+
writeFileSync(join(outputDir, "report.json"), JSON.stringify(report, null, 2));
|
|
276
|
+
// Save history
|
|
277
|
+
const historyDir = join(outputDir, "history");
|
|
278
|
+
mkdirSync(historyDir, { recursive: true });
|
|
279
|
+
writeFileSync(join(historyDir, `${report.timestamp}.json`), JSON.stringify({ score: report.score, grade: report.grade, timestamp: report.timestamp, checks: report.checks.map((c) => ({ name: c.name, score: c.score, issues: c.issues.length })) }));
|
|
280
|
+
const historyFiles = readdirSync(historyDir).sort();
|
|
281
|
+
for (const old of historyFiles.slice(0, historyFiles.length - 30)) {
|
|
282
|
+
try {
|
|
283
|
+
unlinkSync(join(historyDir, old));
|
|
284
|
+
}
|
|
285
|
+
catch { /* ignore */ }
|
|
286
|
+
}
|
|
287
|
+
// HTML report
|
|
288
|
+
if (!flags.jsonOnly) {
|
|
289
|
+
const reportDir = join(outputDir, "report");
|
|
290
|
+
mkdirSync(reportDir, { recursive: true });
|
|
291
|
+
const historyDir = join(outputDir, "history");
|
|
292
|
+
const pages = generatePages(report, historyDir);
|
|
293
|
+
for (const [filename, html] of pages) {
|
|
294
|
+
writeFileSync(join(reportDir, filename), html);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Badge
|
|
298
|
+
if (flags.badgeMode) {
|
|
299
|
+
const { buildBadge } = await import("./report/svg.js");
|
|
300
|
+
writeFileSync(join(outputDir, "badge.svg"), buildBadge(report.score, report.grade));
|
|
301
|
+
}
|
|
302
|
+
// SARIF
|
|
303
|
+
if (flags.sarifMode) {
|
|
304
|
+
const { generateSARIF } = await import("./report/sarif.js");
|
|
305
|
+
writeFileSync(join(outputDir, "report.sarif"), generateSARIF(report));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// ── Upload ──
|
|
309
|
+
async function handleUpload(report, cwd, quietMode) {
|
|
310
|
+
const token = process.env.VCQA_TOKEN || process.env.GITHUB_TOKEN;
|
|
311
|
+
if (!token) {
|
|
312
|
+
if (!quietMode)
|
|
313
|
+
console.log(" \x1b[33m\u26a0 Set VCQA_TOKEN to enable upload\x1b[0m");
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const repo = report.meta.repoUrl?.replace(/^https?:\/\/github\.com\//, "")?.replace(/\.git$/, "") || "";
|
|
317
|
+
if (!repo) {
|
|
318
|
+
if (!quietMode)
|
|
319
|
+
console.log(" \x1b[33m\u26a0 No git remote — can't upload\x1b[0m");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
324
322
|
let sha;
|
|
325
323
|
try {
|
|
326
324
|
const { execSync } = await import("node:child_process");
|
|
327
325
|
sha = execSync("git rev-parse HEAD", { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
328
326
|
}
|
|
329
|
-
catch {
|
|
330
|
-
/* not a git repo */
|
|
331
|
-
}
|
|
327
|
+
catch { /* not a git repo */ }
|
|
332
328
|
try {
|
|
333
329
|
const res = await fetch("https://api.vibecodeqa.online/api/reports", {
|
|
334
330
|
method: "POST",
|
|
@@ -337,34 +333,33 @@ async function handleUpload(report, cwd, jsonOnly) {
|
|
|
337
333
|
});
|
|
338
334
|
if (res.ok) {
|
|
339
335
|
const data = (await res.json());
|
|
340
|
-
if (!
|
|
336
|
+
if (!quietMode)
|
|
341
337
|
console.log(` \x1b[32m\u2713 Uploaded to dashboard\x1b[0m \x1b[2m(${data.totalReports || 1} reports)\x1b[0m`);
|
|
342
338
|
}
|
|
343
|
-
else if (!
|
|
344
|
-
console.log(` \x1b[33m\u26a0 Upload failed: ${res.status}\x1b[0m
|
|
339
|
+
else if (!quietMode) {
|
|
340
|
+
console.log(` \x1b[33m\u26a0 Upload failed: ${res.status}\x1b[0m`);
|
|
345
341
|
}
|
|
346
342
|
}
|
|
347
343
|
catch {
|
|
348
|
-
if (!
|
|
344
|
+
if (!quietMode)
|
|
349
345
|
console.log(` \x1b[33m\u26a0 Upload failed (network error)\x1b[0m`);
|
|
350
346
|
}
|
|
351
347
|
}
|
|
348
|
+
// ── Watch mode ──
|
|
352
349
|
async function startWatch(cwd) {
|
|
353
350
|
const { watch } = await import("node:fs");
|
|
354
351
|
const workspace = detectWorkspace(cwd);
|
|
355
352
|
const watchDirs = workspace.isMonorepo
|
|
356
353
|
? workspace.srcRoots.map((d) => join(cwd, d)).filter((d) => existsSync(d))
|
|
357
354
|
: ["src", "web/src"].map((d) => join(cwd, d)).filter((d) => existsSync(d));
|
|
358
|
-
|
|
359
|
-
if (srcDirs.length === 0) {
|
|
355
|
+
if (watchDirs.length === 0) {
|
|
360
356
|
console.log(" \x1b[31mNo src/ directory to watch\x1b[0m");
|
|
361
357
|
process.exit(1);
|
|
362
358
|
}
|
|
363
|
-
console.log(" \x1b[2mWatching for changes... (Ctrl+C to stop)\x1b[0m");
|
|
364
|
-
console.log("");
|
|
359
|
+
console.log(" \x1b[2mWatching for changes... (Ctrl+C to stop)\x1b[0m\n");
|
|
365
360
|
let debounce = null;
|
|
366
361
|
let running = false;
|
|
367
|
-
for (const dir of
|
|
362
|
+
for (const dir of watchDirs) {
|
|
368
363
|
watch(dir, { recursive: true }, (_event, filename) => {
|
|
369
364
|
if (!filename || filename.includes("node_modules") || filename.includes(".vibe-check"))
|
|
370
365
|
return;
|
|
@@ -376,7 +371,7 @@ async function startWatch(cwd) {
|
|
|
376
371
|
running = true;
|
|
377
372
|
try {
|
|
378
373
|
console.log(` \x1b[2mChanged: ${filename} — re-scanning...\x1b[0m`);
|
|
379
|
-
await main().catch(() => { });
|
|
374
|
+
await main().catch((err) => { console.error("scan error:", err); });
|
|
380
375
|
}
|
|
381
376
|
finally {
|
|
382
377
|
running = false;
|
|
@@ -384,471 +379,77 @@ async function startWatch(cwd) {
|
|
|
384
379
|
}, 500);
|
|
385
380
|
});
|
|
386
381
|
}
|
|
387
|
-
// Keep process alive
|
|
388
382
|
await new Promise(() => { });
|
|
389
383
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
monitor [path] Live quality control panel — re-scans on file changes
|
|
404
|
-
|
|
405
|
-
\x1b[1mFlags:\x1b[0m
|
|
406
|
-
--skip-tests Skip test execution (faster scan)
|
|
407
|
-
--ci CI mode (exit 1 if score < 60)
|
|
408
|
-
--fail-under N Exit 1 if score below N (e.g. --fail-under 80)
|
|
409
|
-
--json Output JSON only (no terminal UI)
|
|
410
|
-
--badge Generate SVG badge
|
|
411
|
-
--sarif Generate SARIF for GitHub Code Scanning
|
|
412
|
-
--upload Upload report to app.vibecodeqa.online
|
|
413
|
-
--top [N] Show top N issues to fix (default: 5)
|
|
414
|
-
--diff [base] Only show issues in changed files (vs HEAD or branch)
|
|
415
|
-
--markdown Output markdown summary (pipe to file or clipboard)
|
|
416
|
-
--pr-comment Post score as GitHub PR comment (needs GITHUB_TOKEN)
|
|
417
|
-
--annotations Emit GitHub Actions ::warning/::error annotations
|
|
418
|
-
--watch Re-scan on file changes
|
|
419
|
-
-v, --version Print version
|
|
420
|
-
-h, --help Show this help
|
|
421
|
-
|
|
422
|
-
\x1b[1mExamples:\x1b[0m
|
|
423
|
-
npx @vibecodeqa/cli # scan current directory
|
|
424
|
-
npx @vibecodeqa/cli init # set up CI + configs
|
|
425
|
-
npx @vibecodeqa/cli fix # auto-fix what's fixable
|
|
426
|
-
npx @vibecodeqa/cli fix --ai # AI-powered fix (uses Claude)
|
|
427
|
-
npx @vibecodeqa/cli fix --ai --check security # fix only security issues
|
|
428
|
-
npx @vibecodeqa/cli fix --ai --dry-run # preview AI fixes without applying
|
|
429
|
-
npx @vibecodeqa/cli --skip-tests --top # fast scan with top issues
|
|
430
|
-
npx @vibecodeqa/cli --ci --fail-under 80 # CI with quality gate
|
|
431
|
-
`);
|
|
432
|
-
}
|
|
433
|
-
// ── init command ──
|
|
434
|
-
async function runInit(cwd) {
|
|
435
|
-
console.log("");
|
|
436
|
-
console.log(` \x1b[1m\x1b[38;5;141mvcqa init\x1b[0m`);
|
|
437
|
-
console.log(` \x1b[2m${cwd}\x1b[0m`);
|
|
438
|
-
console.log("");
|
|
439
|
-
validateCwd(cwd);
|
|
440
|
-
const stack = detectStack(cwd);
|
|
441
|
-
let created = 0;
|
|
442
|
-
// 1. GitHub Actions workflow
|
|
443
|
-
const workflowDir = join(cwd, ".github", "workflows");
|
|
444
|
-
const workflowPath = join(workflowDir, "vibecodeqa.yml");
|
|
445
|
-
if (!existsSync(workflowPath)) {
|
|
446
|
-
try {
|
|
447
|
-
mkdirSync(workflowDir, { recursive: true });
|
|
448
|
-
writeFileSync(workflowPath, `name: VibeCode QA
|
|
449
|
-
on: [pull_request]
|
|
450
|
-
permissions: { contents: read }
|
|
451
|
-
jobs:
|
|
452
|
-
scan:
|
|
453
|
-
runs-on: ubuntu-latest
|
|
454
|
-
steps:
|
|
455
|
-
- uses: actions/checkout@v4
|
|
456
|
-
- run: npx @vibecodeqa/cli --ci --fail-under 70 --sarif --badge
|
|
457
|
-
- uses: github/codeql-action/upload-sarif@v3
|
|
458
|
-
if: always()
|
|
459
|
-
with:
|
|
460
|
-
sarif_file: .vibe-check/report.sarif
|
|
461
|
-
`);
|
|
462
|
-
console.log(` \x1b[32m+\x1b[0m .github/workflows/vibecodeqa.yml`);
|
|
463
|
-
created++;
|
|
464
|
-
}
|
|
465
|
-
catch {
|
|
466
|
-
console.log(` \x1b[31m!\x1b[0m .github/workflows/vibecodeqa.yml (write failed — check permissions)`);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
else {
|
|
470
|
-
console.log(` \x1b[2m=\x1b[0m .github/workflows/vibecodeqa.yml (exists)`);
|
|
471
|
-
}
|
|
472
|
-
// 2. Biome config (if biome is a dep but no config exists)
|
|
473
|
-
if ((stack.linter === "biome" || existsSync(join(cwd, "node_modules", "@biomejs", "biome"))) &&
|
|
474
|
-
!existsSync(join(cwd, "biome.json")) &&
|
|
475
|
-
!existsSync(join(cwd, "biome.jsonc"))) {
|
|
476
|
-
writeFileSync(join(cwd, "biome.json"), JSON.stringify({
|
|
477
|
-
$schema: "https://biomejs.dev/schemas/2.0.0/schema.json",
|
|
478
|
-
formatter: { indentStyle: "tab", lineWidth: 120 },
|
|
479
|
-
linter: { enabled: true, rules: { recommended: true } },
|
|
480
|
-
organizeImports: { enabled: true },
|
|
481
|
-
}, null, "\t") + "\n");
|
|
482
|
-
console.log(` \x1b[32m+\x1b[0m biome.json`);
|
|
483
|
-
created++;
|
|
484
|
-
}
|
|
485
|
-
// 3. Create .vcqa.json if not present
|
|
486
|
-
const vcqaConfigPath = join(cwd, ".vcqa.json");
|
|
487
|
-
if (!existsSync(vcqaConfigPath)) {
|
|
488
|
-
const { CHECK_META } = await import("./check-meta.js");
|
|
489
|
-
const checksConfig = {};
|
|
490
|
-
for (const name of Object.keys(CHECK_META)) {
|
|
491
|
-
checksConfig[name] = {};
|
|
492
|
-
}
|
|
493
|
-
const config = {
|
|
494
|
-
_comment: "vcqa config — docs: https://vibecodeqa.online/skills",
|
|
495
|
-
checks: checksConfig,
|
|
496
|
-
_checks_help: "Set { \"enabled\": false } to disable. Add \"ignore\": [\"generated/**\"] to skip files per-check.",
|
|
497
|
-
ignore: [],
|
|
498
|
-
_ignore_help: "Global file patterns to skip: [\"vendor/**\", \"*.generated.ts\", \"proto/**\"]",
|
|
499
|
-
failUnder: 60,
|
|
500
|
-
_failUnder_help: "Exit with code 1 if score below this. Overridden by --fail-under flag.",
|
|
501
|
-
};
|
|
502
|
-
writeFileSync(vcqaConfigPath, JSON.stringify(config, null, 2) + "\n");
|
|
503
|
-
console.log(` \x1b[32m+\x1b[0m .vcqa.json`);
|
|
504
|
-
created++;
|
|
505
|
-
}
|
|
506
|
-
// 4. Add .vibe-check to .gitignore
|
|
507
|
-
const gitignorePath = join(cwd, ".gitignore");
|
|
508
|
-
if (existsSync(gitignorePath)) {
|
|
509
|
-
const content = readFileSync(gitignorePath, "utf-8");
|
|
510
|
-
if (!content.includes(".vibe-check")) {
|
|
511
|
-
writeFileSync(gitignorePath, content.trimEnd() + "\n.vibe-check/\n");
|
|
512
|
-
console.log(` \x1b[32m+\x1b[0m .gitignore (added .vibe-check/)`);
|
|
513
|
-
created++;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
console.log("");
|
|
517
|
-
if (created > 0) {
|
|
518
|
-
console.log(` \x1b[32mCreated ${created} file(s).\x1b[0m Run \x1b[1mnpx @vibecodeqa/cli\x1b[0m to scan.`);
|
|
519
|
-
}
|
|
520
|
-
else {
|
|
521
|
-
console.log(` \x1b[2mAlready set up. Run npx @vibecodeqa/cli to scan.\x1b[0m`);
|
|
522
|
-
}
|
|
523
|
-
console.log("");
|
|
524
|
-
}
|
|
525
|
-
// ── explain command ──
|
|
526
|
-
async function runExplain(checkName) {
|
|
527
|
-
if (!checkName) {
|
|
528
|
-
console.log("\n \x1b[1mUsage:\x1b[0m vcqa explain <check>\n");
|
|
529
|
-
console.log(" Available checks:");
|
|
530
|
-
const { CHECK_META } = await import("./check-meta.js");
|
|
531
|
-
for (const [name, meta] of Object.entries(CHECK_META)) {
|
|
532
|
-
console.log(` \x1b[1m${name.padEnd(16)}\x1b[0m ${meta.label} (${meta.category}, ${meta.weight}%)`);
|
|
533
|
-
}
|
|
534
|
-
console.log("");
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
const meta = getCheckMeta(checkName);
|
|
538
|
-
if (!meta.description || meta.description.length < 20) {
|
|
539
|
-
console.log(`\n \x1b[31mUnknown check: ${checkName}\x1b[0m`);
|
|
540
|
-
console.log(" Run \x1b[1mvcqa explain\x1b[0m to see available checks.\n");
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
console.log("");
|
|
544
|
-
console.log(` \x1b[1m\x1b[38;5;141m${meta.label}\x1b[0m \x1b[2m${meta.category} · ${meta.priority} priority · ${meta.weight}% weight\x1b[0m`);
|
|
545
|
-
console.log("");
|
|
546
|
-
console.log(` \x1b[1mWhat:\x1b[0m ${meta.description}`);
|
|
547
|
-
console.log("");
|
|
548
|
-
console.log(` \x1b[1mRisk:\x1b[0m ${meta.risk}`);
|
|
549
|
-
console.log("");
|
|
550
|
-
console.log(` \x1b[1mFix:\x1b[0m ${meta.recommendation}`);
|
|
551
|
-
if (meta.deeperTools?.length) {
|
|
552
|
-
console.log("");
|
|
553
|
-
console.log(` \x1b[1mGo deeper:\x1b[0m ${meta.deeperTools.join(", ")}`);
|
|
554
|
-
}
|
|
555
|
-
console.log("");
|
|
384
|
+
// ── Interactive prompt ──
|
|
385
|
+
function readKey() {
|
|
386
|
+
return new Promise((resolve) => {
|
|
387
|
+
const stdin = process.stdin;
|
|
388
|
+
const wasRaw = stdin.isRaw;
|
|
389
|
+
stdin.setRawMode?.(true);
|
|
390
|
+
stdin.resume();
|
|
391
|
+
stdin.once("data", (buf) => {
|
|
392
|
+
stdin.setRawMode?.(wasRaw ?? false);
|
|
393
|
+
stdin.pause();
|
|
394
|
+
resolve(buf.toString("utf-8"));
|
|
395
|
+
});
|
|
396
|
+
});
|
|
556
397
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
console.log(` \x1b[1m\x1b[38;5;141mvcqa fix${opts.ai ? " --ai" : ""}${opts.dryRun ? " --dry-run" : ""}${opts.checkFilter ? ` --check ${opts.checkFilter}` : ""}\x1b[0m`);
|
|
561
|
-
console.log(` \x1b[2m${cwd}\x1b[0m`);
|
|
562
|
-
console.log("");
|
|
563
|
-
validateCwd(cwd);
|
|
564
|
-
const stack = detectStack(cwd);
|
|
565
|
-
let fixed = 0;
|
|
566
|
-
// 0. Auto-fix structure issues (missing files)
|
|
567
|
-
if (!existsSync(join(cwd, ".gitignore"))) {
|
|
568
|
-
writeFileSync(join(cwd, ".gitignore"), "node_modules\ndist\n.vibe-check\ncoverage\n.env\n.env.local\n");
|
|
569
|
-
console.log(" \x1b[32m\u2713 Created .gitignore\x1b[0m");
|
|
570
|
-
fixed++;
|
|
571
|
-
}
|
|
572
|
-
// Add .vibe-check to existing .gitignore if missing
|
|
573
|
-
if (existsSync(join(cwd, ".gitignore"))) {
|
|
574
|
-
const gi = readFileSync(join(cwd, ".gitignore"), "utf-8");
|
|
575
|
-
if (!gi.includes(".vibe-check")) {
|
|
576
|
-
writeFileSync(join(cwd, ".gitignore"), gi.trimEnd() + "\n.vibe-check/\n");
|
|
577
|
-
console.log(" \x1b[32m\u2713 Added .vibe-check/ to .gitignore\x1b[0m");
|
|
578
|
-
fixed++;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
// Enable strict mode if tsconfig exists without it
|
|
582
|
-
const tsconfigPath = join(cwd, "tsconfig.json");
|
|
583
|
-
if (existsSync(tsconfigPath)) {
|
|
584
|
-
try {
|
|
585
|
-
const raw = readFileSync(tsconfigPath, "utf-8");
|
|
586
|
-
const tsconfig = JSON.parse(raw);
|
|
587
|
-
if (!tsconfig.compilerOptions?.strict) {
|
|
588
|
-
tsconfig.compilerOptions = { ...tsconfig.compilerOptions, strict: true };
|
|
589
|
-
writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
|
|
590
|
-
console.log(' \x1b[32m\u2713 Enabled "strict": true in tsconfig.json\x1b[0m');
|
|
591
|
-
fixed++;
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
catch {
|
|
595
|
-
/* can't parse tsconfig */
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
// 1. Run biome format (auto-fixable lint + format issues)
|
|
599
|
-
if (stack.linter === "biome") {
|
|
600
|
-
console.log(" \x1b[1mFormatting with Biome...\x1b[0m");
|
|
601
|
-
const { execSync } = await import("node:child_process");
|
|
602
|
-
try {
|
|
603
|
-
execSync("npx biome check --write .", { cwd, stdio: "inherit", timeout: 30_000 });
|
|
604
|
-
fixed++;
|
|
605
|
-
}
|
|
606
|
-
catch {
|
|
607
|
-
console.log(" \x1b[33mBiome had issues (some may be unfixable)\x1b[0m");
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
else if (stack.linter === "eslint") {
|
|
611
|
-
console.log(" \x1b[1mFixing with ESLint...\x1b[0m");
|
|
612
|
-
const { execSync } = await import("node:child_process");
|
|
398
|
+
function openPath(target) {
|
|
399
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
400
|
+
import("node:child_process").then(({ spawn }) => {
|
|
613
401
|
try {
|
|
614
|
-
|
|
615
|
-
fixed++;
|
|
616
|
-
}
|
|
617
|
-
catch {
|
|
618
|
-
console.log(" \x1b[33mESLint had issues (some may be unfixable)\x1b[0m");
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
// 2. Scan to find remaining issues and generate fix suggestions
|
|
622
|
-
console.log("");
|
|
623
|
-
console.log(" \x1b[1mScanning for remaining issues...\x1b[0m");
|
|
624
|
-
console.log("");
|
|
625
|
-
const workspace = detectWorkspace(cwd);
|
|
626
|
-
setGlobalSrcRoots(workspace.isMonorepo ? workspace.srcRoots : undefined);
|
|
627
|
-
const enrichedStack = detectStack(cwd, workspace);
|
|
628
|
-
const isDart = enrichedStack.language === "dart";
|
|
629
|
-
const checks = await runChecks(cwd, enrichedStack, workspace, true, isDart, true);
|
|
630
|
-
const score = computeScore(checks);
|
|
631
|
-
// AI-powered fix mode
|
|
632
|
-
if (opts.ai) {
|
|
633
|
-
const aiIssues = collectFixableIssues(checks, suggestFix, opts.checkFilter);
|
|
634
|
-
if (aiIssues.length === 0) {
|
|
635
|
-
console.log(" \x1b[2mNo fixable issues found.\x1b[0m");
|
|
636
|
-
}
|
|
637
|
-
else {
|
|
638
|
-
console.log(` \x1b[1mAI fixing ${Math.min(aiIssues.length, 10)} issues${opts.dryRun ? " (dry run)" : ""}...\x1b[0m`);
|
|
639
|
-
console.log("");
|
|
640
|
-
const results = await aiFixIssues(cwd, aiIssues, { dryRun: opts.dryRun || false });
|
|
641
|
-
const applied = results.filter((r) => r.applied).length;
|
|
642
|
-
if (applied > 0) {
|
|
643
|
-
// Re-scan to show new score
|
|
644
|
-
console.log("");
|
|
645
|
-
console.log(" \x1b[1mRe-scanning...\x1b[0m");
|
|
646
|
-
const reChecks = await runChecks(cwd, enrichedStack, workspace, true, isDart, true);
|
|
647
|
-
const newScore = computeScore(reChecks);
|
|
648
|
-
const newGrade = gradeFromScore(newScore);
|
|
649
|
-
const delta = newScore - score;
|
|
650
|
-
console.log(` Score: \x1b[${newScore >= 75 ? "32" : newScore >= 60 ? "33" : "31"}m${newGrade} ${newScore}/100\x1b[0m${delta > 0 ? ` \x1b[32m(+${delta})\x1b[0m` : ""}`);
|
|
651
|
-
console.log(` \x1b[32m${applied} AI fix(es) applied.\x1b[0m Re-run \x1b[1mnpx @vibecodeqa/cli\x1b[0m for full report.`);
|
|
652
|
-
}
|
|
653
|
-
else {
|
|
654
|
-
const grade = gradeFromScore(score);
|
|
655
|
-
console.log(`\n Score: \x1b[${score >= 75 ? "32" : score >= 60 ? "33" : "31"}m${grade} ${score}/100\x1b[0m`);
|
|
656
|
-
if (opts.dryRun)
|
|
657
|
-
console.log(" \x1b[2mDry run — no files modified. Remove --dry-run to apply.\x1b[0m");
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
console.log("");
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
// Collect actionable issues with fix suggestions (non-AI mode)
|
|
664
|
-
const fixable = [];
|
|
665
|
-
for (const c of checks) {
|
|
666
|
-
for (const iss of c.issues) {
|
|
667
|
-
if (!iss.file || typeof iss.file !== "string" || !iss.line)
|
|
668
|
-
continue;
|
|
669
|
-
const fix = suggestFix(c.name, iss.rule || "", iss.message);
|
|
670
|
-
if (fix)
|
|
671
|
-
fixable.push({ check: c.name, file: iss.file, line: iss.line, message: iss.message, fix });
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
// Print top fixable issues
|
|
675
|
-
const top = fixable.slice(0, 10);
|
|
676
|
-
if (top.length > 0) {
|
|
677
|
-
console.log(` \x1b[1m${top.length} issues with fix suggestions:\x1b[0m`);
|
|
678
|
-
console.log("");
|
|
679
|
-
for (const f of top) {
|
|
680
|
-
console.log(` \x1b[2m${f.file}:${f.line}\x1b[0m`);
|
|
681
|
-
console.log(` ${f.message}`);
|
|
682
|
-
console.log(` \x1b[32mFix: ${f.fix}\x1b[0m`);
|
|
683
|
-
console.log("");
|
|
402
|
+
spawn(cmd, [target], { detached: true, stdio: "ignore", shell: process.platform === "win32" }).unref();
|
|
684
403
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
console.log(` Score after fix: \x1b[${score >= 75 ? "32" : score >= 60 ? "33" : "31"}m${grade} ${score}/100\x1b[0m`);
|
|
688
|
-
if (fixed > 0)
|
|
689
|
-
console.log(` \x1b[32m${fixed} auto-fix(es) applied.\x1b[0m Re-run \x1b[1mnpx @vibecodeqa/cli\x1b[0m for full report.`);
|
|
690
|
-
console.log("");
|
|
691
|
-
}
|
|
692
|
-
function suggestFix(check, rule, message) {
|
|
693
|
-
// Map common issues to actionable fixes
|
|
694
|
-
if (rule === "empty-catch")
|
|
695
|
-
return "Add error logging: catch(e) { console.error(e); }";
|
|
696
|
-
if (rule === "throw-string")
|
|
697
|
-
return 'Replace throw "msg" with throw new Error("msg")';
|
|
698
|
-
if (rule === "swallowed-promise")
|
|
699
|
-
return "Add logging: .catch((e) => { console.error(e); })";
|
|
700
|
-
if (rule === "floating-promise")
|
|
701
|
-
return "Add await or .catch() to handle the promise";
|
|
702
|
-
if (rule === "unsafe-json-parse")
|
|
703
|
-
return "Wrap in try-catch: try { JSON.parse(x) } catch { /* handle */ }";
|
|
704
|
-
if (rule === "no-error-boundary")
|
|
705
|
-
return "Add <ErrorBoundary> wrapper in your React app root";
|
|
706
|
-
if (rule === "img-alt")
|
|
707
|
-
return 'Add alt attribute: <img alt="description" ...>';
|
|
708
|
-
if (rule === "click-events")
|
|
709
|
-
return 'Add role="button" and onKeyDown handler';
|
|
710
|
-
if (rule === "vue-v-for-key")
|
|
711
|
-
return 'Add :key="item.id" to the v-for element';
|
|
712
|
-
if (rule === "missing-key")
|
|
713
|
-
return "Add key={item.id} to the JSX element in .map()";
|
|
714
|
-
if (rule === "index-key")
|
|
715
|
-
return "Use a stable unique ID instead of array index for key";
|
|
716
|
-
if (rule === "conditional-hook")
|
|
717
|
-
return "Move the hook call before any conditional (if/switch)";
|
|
718
|
-
if (rule === "no-tests")
|
|
719
|
-
return "Create a test file: src/__tests__/example.test.ts";
|
|
720
|
-
if (rule === "no-readme")
|
|
721
|
-
return "Create README.md with: project description, install, usage";
|
|
722
|
-
if (rule === "no-changelog")
|
|
723
|
-
return "Create CHANGELOG.md or use changesets: npx changeset init";
|
|
724
|
-
if (rule === "env-not-ignored")
|
|
725
|
-
return "Add .env to .gitignore";
|
|
726
|
-
if (rule === "secret-detected")
|
|
727
|
-
return "Move to environment variable, rotate the exposed secret";
|
|
728
|
-
if (rule === "no-ci")
|
|
729
|
-
return "Run: npx @vibecodeqa/cli init";
|
|
730
|
-
if (rule === "missing-lockfile")
|
|
731
|
-
return "Run: pnpm install (or npm install) to generate lockfile";
|
|
732
|
-
if (rule === "missing-file" && message.includes("LICENSE"))
|
|
733
|
-
return "Add LICENSE file: https://choosealicense.com/";
|
|
734
|
-
if (rule === "long-function")
|
|
735
|
-
return "Extract logic into smaller helper functions";
|
|
736
|
-
if (rule === "high-complexity")
|
|
737
|
-
return "Reduce nesting: use early returns, extract conditions";
|
|
738
|
-
if (rule === "duplicate-code")
|
|
739
|
-
return "Extract shared logic into a helper function";
|
|
740
|
-
if (rule === "circular-dep")
|
|
741
|
-
return "Extract shared types to a separate file both modules import";
|
|
742
|
-
if (rule === "god-module")
|
|
743
|
-
return "Split into focused interfaces — one responsibility per module";
|
|
744
|
-
if (rule === "process-exit")
|
|
745
|
-
return "Replace process.exit() with throw new Error()";
|
|
746
|
-
if (check === "security" && message.includes("innerHTML"))
|
|
747
|
-
return "Use textContent or DOM APIs instead";
|
|
748
|
-
if (check === "security" && message.includes("ev" + "al"))
|
|
749
|
-
return `Remove ${"ev" + "al"}() — use a safer alternative`;
|
|
750
|
-
if (check === "security" && message.includes("v-html"))
|
|
751
|
-
return 'Sanitize with DOMPurify: v-html="DOMPurify.sanitize(input)"';
|
|
752
|
-
return null;
|
|
404
|
+
catch { /* best-effort */ }
|
|
405
|
+
});
|
|
753
406
|
}
|
|
754
|
-
function
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
process.exit(1);
|
|
758
|
-
}
|
|
407
|
+
async function promptNextAction(cwd, outputDir) {
|
|
408
|
+
process.stdout.write(" \x1b[1m[m]\x1b[0m\x1b[2m monitor\x1b[0m \x1b[1m[o]\x1b[0m\x1b[2m open report\x1b[0m \x1b[1m[f]\x1b[0m\x1b[2m fix --ai\x1b[0m \x1b[1m[enter]\x1b[0m\x1b[2m quit\x1b[0m ");
|
|
409
|
+
let key;
|
|
759
410
|
try {
|
|
760
|
-
|
|
761
|
-
console.error(` \x1b[31mError: not a directory: ${cwd}\x1b[0m`);
|
|
762
|
-
process.exit(1);
|
|
763
|
-
}
|
|
411
|
+
key = await readKey();
|
|
764
412
|
}
|
|
765
413
|
catch {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
function generateMarkdown(report, trend) {
|
|
771
|
-
const { score, grade, checks } = report;
|
|
772
|
-
const gradeEmoji = grade === "A" ? "🟢" : grade === "B" ? "🟡" : grade === "C" ? "🟠" : "🔴";
|
|
773
|
-
let md = `# ${gradeEmoji} VibeCode QA: ${grade} ${score}/100\n\n`;
|
|
774
|
-
if (trend) {
|
|
775
|
-
const arrow = trend.scoreDelta > 0 ? "📈" : trend.scoreDelta < 0 ? "📉" : "➡️";
|
|
776
|
-
md += `${arrow} **${trend.scoreDelta > 0 ? "+" : ""}${trend.scoreDelta}** vs previous`;
|
|
777
|
-
if (trend.fixedIssues > 0)
|
|
778
|
-
md += ` · ${trend.fixedIssues} fixed`;
|
|
779
|
-
if (trend.newIssues > 0)
|
|
780
|
-
md += ` · ${trend.newIssues} new`;
|
|
781
|
-
md += "\n\n";
|
|
414
|
+
process.stdout.write("\n");
|
|
415
|
+
return;
|
|
782
416
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
const emoji = c.score >= 90 ? "🟢" : c.score >= 75 ? "🟡" : c.score >= 60 ? "🟠" : "🔴";
|
|
789
|
-
md += `| ${emoji} ${c.name} | ${c.score}/100 | ${c.grade} |\n`;
|
|
417
|
+
process.stdout.write("\n");
|
|
418
|
+
const k = key.toLowerCase();
|
|
419
|
+
if (k === "m") {
|
|
420
|
+
const { startMonitor } = await import("./monitor.js");
|
|
421
|
+
await startMonitor(cwd);
|
|
790
422
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
for (const i of [...errors, ...warnings].slice(0, 15)) {
|
|
796
|
-
const loc = i.file ? ` \`${i.file}${i.line ? `:${i.line}` : ""}\`` : "";
|
|
797
|
-
md += `- ${i.severity === "error" ? "❌" : "⚠️"} ${i.message}${loc}\n`;
|
|
798
|
-
}
|
|
799
|
-
const remaining = errors.length + warnings.length - 15;
|
|
800
|
-
if (remaining > 0)
|
|
801
|
-
md += `\n*...and ${remaining} more*\n`;
|
|
423
|
+
else if (k === "o") {
|
|
424
|
+
const reportPath = join(outputDir, "report/index.html");
|
|
425
|
+
openPath(reportPath);
|
|
426
|
+
console.log(` \x1b[2mOpening ${reportPath}\x1b[0m`);
|
|
802
427
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
}
|
|
806
|
-
function emitAnnotations(report) {
|
|
807
|
-
for (const c of report.checks) {
|
|
808
|
-
for (const i of c.issues) {
|
|
809
|
-
if (i.severity === "info")
|
|
810
|
-
continue;
|
|
811
|
-
const level = i.severity === "error" ? "error" : "warning";
|
|
812
|
-
const file = i.file && typeof i.file === "string" ? i.file : "";
|
|
813
|
-
const line = i.line || "";
|
|
814
|
-
const loc = file ? ` file=${file}${line ? `,line=${line}` : ""}` : "";
|
|
815
|
-
// GitHub Actions annotation format
|
|
816
|
-
console.log(`::${level}${loc ? loc : ""}::${c.name}: ${i.message}`);
|
|
817
|
-
}
|
|
428
|
+
else if (k === "f") {
|
|
429
|
+
console.log("");
|
|
430
|
+
await runFix(cwd, { ai: true });
|
|
818
431
|
}
|
|
819
432
|
}
|
|
820
|
-
|
|
821
|
-
function
|
|
433
|
+
// ── Update check ──
|
|
434
|
+
async function checkForUpdate(currentVersion) {
|
|
822
435
|
try {
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
console.log("");
|
|
836
|
-
console.log(` \x1b[1m\x1b[38;5;141mvcqa\x1b[0m v${VERSION}`);
|
|
837
|
-
console.log(` \x1b[2m${cwd}\x1b[0m`);
|
|
838
|
-
console.log("");
|
|
839
|
-
const parts = [stack.language, stack.framework, stack.bundler, stack.testRunner, stack.linter, stack.packageManager].filter((v) => v !== "none" && v !== "unknown");
|
|
840
|
-
console.log(` stack: ${parts.join(" + ")}`);
|
|
841
|
-
if (workspace.isMonorepo) {
|
|
842
|
-
console.log(` workspace: ${workspace.tool} monorepo — ${workspace.packages.length} packages`);
|
|
843
|
-
for (const pkg of workspace.packages.slice(0, 8)) {
|
|
844
|
-
const f = [pkg.hasSrc && "src", pkg.hasTests && "tests", pkg.hasLinter && "linter"].filter(Boolean).join(", ");
|
|
845
|
-
console.log(` \x1b[2m${pkg.path}\x1b[0m (${f || "empty"})`);
|
|
436
|
+
const res = await fetch("https://registry.npmjs.org/@vibecodeqa/cli/latest", { signal: AbortSignal.timeout(3000) });
|
|
437
|
+
if (!res.ok)
|
|
438
|
+
return;
|
|
439
|
+
const data = (await res.json());
|
|
440
|
+
const latest = data.version;
|
|
441
|
+
if (!latest || latest === currentVersion)
|
|
442
|
+
return;
|
|
443
|
+
const cur = currentVersion.split(".").map(Number);
|
|
444
|
+
const lat = latest.split(".").map(Number);
|
|
445
|
+
const isNewer = lat[0] > cur[0] || (lat[0] === cur[0] && lat[1] > cur[1]) || (lat[0] === cur[0] && lat[1] === cur[1] && lat[2] > cur[2]);
|
|
446
|
+
if (isNewer) {
|
|
447
|
+
console.log(` \x1b[33mUpdate available: ${currentVersion} → ${latest}\x1b[0m Run \x1b[1mnpx @vibecodeqa/cli@latest\x1b[0m\n`);
|
|
846
448
|
}
|
|
847
|
-
if (workspace.packages.length > 8)
|
|
848
|
-
console.log(` \x1b[2m...and ${workspace.packages.length - 8} more\x1b[0m`);
|
|
849
449
|
}
|
|
850
|
-
|
|
450
|
+
catch { /* network error — silently ignore */ }
|
|
851
451
|
}
|
|
452
|
+
// ── Main ──
|
|
852
453
|
async function main() {
|
|
853
454
|
const args = process.argv.slice(2);
|
|
854
455
|
if (args.includes("--version") || args.includes("-v")) {
|
|
@@ -860,8 +461,7 @@ async function main() {
|
|
|
860
461
|
return;
|
|
861
462
|
}
|
|
862
463
|
if (args[0] === "init") {
|
|
863
|
-
|
|
864
|
-
await runInit(resolve(path));
|
|
464
|
+
await runInit(resolve(args.slice(1).find((a) => !a.startsWith("-")) || "."));
|
|
865
465
|
return;
|
|
866
466
|
}
|
|
867
467
|
if (args[0] === "fix") {
|
|
@@ -879,43 +479,35 @@ async function main() {
|
|
|
879
479
|
return;
|
|
880
480
|
}
|
|
881
481
|
if (args[0] === "monitor") {
|
|
882
|
-
const path = args.slice(1).find((a) => !a.startsWith("-")) || ".";
|
|
883
482
|
const { startMonitor } = await import("./monitor.js");
|
|
884
|
-
await startMonitor(resolve(
|
|
483
|
+
await startMonitor(resolve(args.slice(1).find((a) => !a.startsWith("-")) || "."));
|
|
885
484
|
return;
|
|
886
485
|
}
|
|
887
486
|
const flags = parseFlags();
|
|
888
487
|
const { cwd, outputDir, jsonOnly, ciMode, skipTests, watchMode, diffBase } = flags;
|
|
889
|
-
const start = Date.now();
|
|
890
488
|
validateCwd(cwd);
|
|
891
489
|
const config = loadConfig(cwd);
|
|
892
490
|
const workspace = detectWorkspace(cwd);
|
|
893
491
|
const stack = detectStack(cwd, workspace);
|
|
894
|
-
setGlobalSrcRoots(workspace.isMonorepo ? workspace.srcRoots : undefined);
|
|
895
|
-
setGlobalIgnore(config.ignore);
|
|
896
492
|
const quietMode = jsonOnly || flags.markdownMode;
|
|
897
493
|
if (!quietMode)
|
|
898
494
|
printHeader(cwd, stack, workspace);
|
|
899
|
-
|
|
900
|
-
const
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
return f.startsWith(p);
|
|
916
|
-
});
|
|
917
|
-
});
|
|
918
|
-
}
|
|
495
|
+
// Run scan using core API with progress output
|
|
496
|
+
const report = await scan(cwd, {
|
|
497
|
+
skipTests,
|
|
498
|
+
config,
|
|
499
|
+
onProgress: quietMode ? undefined : (check, result) => {
|
|
500
|
+
const det = result.details;
|
|
501
|
+
const premium = det.comingSoon;
|
|
502
|
+
const skipped = det.skipped;
|
|
503
|
+
const c = premium ? "\x1b[2m" : skipped ? "\x1b[2m" : color(result.grade);
|
|
504
|
+
const label = premium ? "soon" : skipped ? "skip" : result.grade;
|
|
505
|
+
const scoreStr = premium ? "PRO" : skipped ? "—" : `${result.score}/100`;
|
|
506
|
+
const issueStr = result.issues.length > 0 ? ` \x1b[2m${result.issues.length} issues\x1b[0m` : "";
|
|
507
|
+
console.log(` ${check.padEnd(14)}${c}${label.padEnd(5)}${scoreStr}\x1b[0m \x1b[2m${result.duration}ms\x1b[0m${issueStr}`);
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
const { score, checks } = report;
|
|
919
511
|
// --diff: filter issues to only changed files
|
|
920
512
|
if (diffBase) {
|
|
921
513
|
const changedFiles = getChangedFiles(cwd, diffBase);
|
|
@@ -925,21 +517,8 @@ async function main() {
|
|
|
925
517
|
}
|
|
926
518
|
}
|
|
927
519
|
}
|
|
928
|
-
const score = computeScore(checks);
|
|
929
|
-
const grade = gradeFromScore(score);
|
|
930
|
-
const duration = Date.now() - start;
|
|
931
|
-
const report = {
|
|
932
|
-
version: VERSION,
|
|
933
|
-
timestamp: new Date().toISOString(),
|
|
934
|
-
score,
|
|
935
|
-
grade,
|
|
936
|
-
checks,
|
|
937
|
-
meta: { cwd, node: process.version, duration, stack, workspace, ...detectRepoUrl(cwd) },
|
|
938
|
-
};
|
|
939
520
|
const trend = computeTrend(report, outputDir);
|
|
940
521
|
await writeOutputs(report, outputDir, flags);
|
|
941
|
-
// Interactive = a real terminal session (not piped, CI, JSON, or watch). Gates the
|
|
942
|
-
// post-scan prompt and the default top-issues view.
|
|
943
522
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !quietMode && !ciMode && !watchMode;
|
|
944
523
|
if (flags.markdownMode) {
|
|
945
524
|
console.log(generateMarkdown(report, trend));
|
|
@@ -947,12 +526,10 @@ async function main() {
|
|
|
947
526
|
else {
|
|
948
527
|
await printResults(report, trend, flags, outputDir, interactive);
|
|
949
528
|
}
|
|
950
|
-
if (flags.annotations)
|
|
529
|
+
if (flags.annotations)
|
|
951
530
|
emitAnnotations(report);
|
|
952
|
-
|
|
953
|
-
if (flags.uploadMode) {
|
|
531
|
+
if (flags.uploadMode)
|
|
954
532
|
await handleUpload(report, cwd, quietMode);
|
|
955
|
-
}
|
|
956
533
|
if (flags.prComment) {
|
|
957
534
|
const posted = await postPRComment(report, trend, cwd);
|
|
958
535
|
if (!quietMode) {
|
|
@@ -962,97 +539,27 @@ async function main() {
|
|
|
962
539
|
console.log(" \x1b[2mNo PR detected or no GITHUB_TOKEN — skipping PR comment\x1b[0m");
|
|
963
540
|
}
|
|
964
541
|
}
|
|
965
|
-
// CI exit code: fail if score below threshold (skip in watch mode)
|
|
966
542
|
const failUnder = flags.failUnder ?? (ciMode ? 60 : (config.failUnder ?? 0));
|
|
967
543
|
if (failUnder > 0 && score < failUnder && !watchMode) {
|
|
968
544
|
if (!quietMode)
|
|
969
545
|
console.log(` \x1b[31mFailing: score ${score} < ${failUnder}\x1b[0m\n`);
|
|
970
546
|
process.exit(1);
|
|
971
547
|
}
|
|
972
|
-
// Non-blocking update check (don't slow down the scan)
|
|
973
548
|
if (!quietMode && !ciMode && !watchMode && !process.env.VCQA_NO_UPDATE_CHECK) {
|
|
974
|
-
checkForUpdate(VERSION).catch(() => { });
|
|
549
|
+
checkForUpdate(VERSION).catch(() => { }); // ok — non-blocking, best-effort
|
|
975
550
|
}
|
|
976
551
|
if (watchMode) {
|
|
977
552
|
await startWatch(cwd);
|
|
978
553
|
return;
|
|
979
554
|
}
|
|
980
|
-
|
|
555
|
+
if (interactive && existsSync(join(cwd, ".git")) && !existsSync(join(cwd, ".github", "workflows", "vibecodeqa.yml"))) {
|
|
556
|
+
console.log(` \x1b[2mTip: Add CI scanning with one line:\x1b[0m \x1b[1m- uses: vibecodeqa/action@v1\x1b[0m`);
|
|
557
|
+
console.log("");
|
|
558
|
+
}
|
|
981
559
|
if (interactive && !flags.uploadMode && !flags.prComment) {
|
|
982
560
|
await promptNextAction(cwd, outputDir);
|
|
983
561
|
}
|
|
984
562
|
}
|
|
985
|
-
/** Read a single keypress from a TTY, restoring stdin state afterward. */
|
|
986
|
-
function readKey() {
|
|
987
|
-
return new Promise((resolve) => {
|
|
988
|
-
const stdin = process.stdin;
|
|
989
|
-
const wasRaw = stdin.isRaw;
|
|
990
|
-
stdin.setRawMode?.(true);
|
|
991
|
-
stdin.resume();
|
|
992
|
-
stdin.once("data", (buf) => {
|
|
993
|
-
stdin.setRawMode?.(wasRaw ?? false);
|
|
994
|
-
stdin.pause();
|
|
995
|
-
resolve(buf.toString("utf-8"));
|
|
996
|
-
});
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
/** Open a file/URL with the OS default handler (detached). */
|
|
1000
|
-
function openPath(target) {
|
|
1001
|
-
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
1002
|
-
import("node:child_process").then(({ spawn }) => {
|
|
1003
|
-
try {
|
|
1004
|
-
spawn(cmd, [target], { detached: true, stdio: "ignore", shell: process.platform === "win32" }).unref();
|
|
1005
|
-
}
|
|
1006
|
-
catch {
|
|
1007
|
-
/* opening is best-effort */
|
|
1008
|
-
}
|
|
1009
|
-
});
|
|
1010
|
-
}
|
|
1011
|
-
/** Post-scan prompt: [m] monitor · [o] open report · anything else quits. */
|
|
1012
|
-
async function promptNextAction(cwd, outputDir) {
|
|
1013
|
-
process.stdout.write(" \x1b[1m[m]\x1b[0m\x1b[2m monitor\x1b[0m \x1b[1m[o]\x1b[0m\x1b[2m open report\x1b[0m \x1b[1m[enter]\x1b[0m\x1b[2m quit\x1b[0m ");
|
|
1014
|
-
let key;
|
|
1015
|
-
try {
|
|
1016
|
-
key = await readKey();
|
|
1017
|
-
}
|
|
1018
|
-
catch {
|
|
1019
|
-
process.stdout.write("\n");
|
|
1020
|
-
return;
|
|
1021
|
-
}
|
|
1022
|
-
process.stdout.write("\n");
|
|
1023
|
-
const k = key.toLowerCase();
|
|
1024
|
-
if (k === "m") {
|
|
1025
|
-
const { startMonitor } = await import("./monitor.js");
|
|
1026
|
-
await startMonitor(cwd);
|
|
1027
|
-
}
|
|
1028
|
-
else if (k === "o") {
|
|
1029
|
-
const reportPath = join(outputDir, "report/index.html");
|
|
1030
|
-
openPath(reportPath);
|
|
1031
|
-
console.log(` \x1b[2mOpening ${reportPath}\x1b[0m`);
|
|
1032
|
-
}
|
|
1033
|
-
// any other key (enter, q, ctrl-c, …) → quit
|
|
1034
|
-
}
|
|
1035
|
-
async function checkForUpdate(currentVersion) {
|
|
1036
|
-
try {
|
|
1037
|
-
const res = await fetch("https://registry.npmjs.org/@vibecodeqa/cli/latest", { signal: AbortSignal.timeout(3000) });
|
|
1038
|
-
if (!res.ok)
|
|
1039
|
-
return;
|
|
1040
|
-
const data = (await res.json());
|
|
1041
|
-
const latest = data.version;
|
|
1042
|
-
if (!latest || latest === currentVersion)
|
|
1043
|
-
return;
|
|
1044
|
-
// Only show if npm version is actually newer (semver compare)
|
|
1045
|
-
const cur = currentVersion.split(".").map(Number);
|
|
1046
|
-
const lat = latest.split(".").map(Number);
|
|
1047
|
-
const isNewer = lat[0] > cur[0] || (lat[0] === cur[0] && lat[1] > cur[1]) || (lat[0] === cur[0] && lat[1] === cur[1] && lat[2] > cur[2]);
|
|
1048
|
-
if (isNewer) {
|
|
1049
|
-
console.log(` \x1b[33mUpdate available: ${currentVersion} → ${latest}\x1b[0m Run \x1b[1mnpx @vibecodeqa/cli@latest\x1b[0m\n`);
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
catch {
|
|
1053
|
-
/* network error — silently ignore */
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
563
|
main().catch((err) => {
|
|
1057
564
|
console.error("vibe-check error:", err);
|
|
1058
565
|
process.exit(1);
|