@vibecodeqa/cli 0.39.1 → 0.40.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/ai-fix.d.ts +27 -0
- package/dist/ai-fix.js +216 -0
- package/dist/cli.js +53 -9
- package/dist/fs-utils.js +1 -1
- package/package.json +1 -1
package/dist/ai-fix.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** AI-powered code fixing — uses Claude to fix issues found by vcqa scan.
|
|
2
|
+
*
|
|
3
|
+
* Auth: ANTHROPIC_API_KEY (direct) or VCQA_PRO_KEY (via api.vibecodeqa.online).
|
|
4
|
+
*/
|
|
5
|
+
import type { CheckResult } from "./types.js";
|
|
6
|
+
export interface FixableIssue {
|
|
7
|
+
check: string;
|
|
8
|
+
file: string;
|
|
9
|
+
line: number;
|
|
10
|
+
rule: string;
|
|
11
|
+
message: string;
|
|
12
|
+
suggestion: string | null;
|
|
13
|
+
}
|
|
14
|
+
export interface AiFixResult {
|
|
15
|
+
file: string;
|
|
16
|
+
line: number;
|
|
17
|
+
check: string;
|
|
18
|
+
message: string;
|
|
19
|
+
explanation: string;
|
|
20
|
+
applied: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Collect fixable issues from scan results, optionally filtered by check name. */
|
|
23
|
+
export declare function collectFixableIssues(checks: CheckResult[], suggestFix: (check: string, rule: string, message: string) => string | null, checkFilter?: string): FixableIssue[];
|
|
24
|
+
/** Fix issues using AI. Returns results for each attempted fix. */
|
|
25
|
+
export declare function aiFixIssues(cwd: string, issues: FixableIssue[], opts: {
|
|
26
|
+
dryRun: boolean;
|
|
27
|
+
}): Promise<AiFixResult[]>;
|
package/dist/ai-fix.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/** AI-powered code fixing — uses Claude to fix issues found by vcqa scan.
|
|
2
|
+
*
|
|
3
|
+
* Auth: ANTHROPIC_API_KEY (direct) or VCQA_PRO_KEY (via api.vibecodeqa.online).
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { getCheckMeta } from "./check-meta.js";
|
|
8
|
+
/** Collect fixable issues from scan results, optionally filtered by check name. */
|
|
9
|
+
export function collectFixableIssues(checks, suggestFix, checkFilter) {
|
|
10
|
+
const issues = [];
|
|
11
|
+
for (const c of checks) {
|
|
12
|
+
if (checkFilter && c.name !== checkFilter)
|
|
13
|
+
continue;
|
|
14
|
+
for (const iss of c.issues) {
|
|
15
|
+
if (!iss.file || typeof iss.file !== "string" || !iss.line)
|
|
16
|
+
continue;
|
|
17
|
+
if (iss.severity === "info")
|
|
18
|
+
continue;
|
|
19
|
+
issues.push({
|
|
20
|
+
check: c.name,
|
|
21
|
+
file: iss.file,
|
|
22
|
+
line: iss.line,
|
|
23
|
+
rule: iss.rule || "",
|
|
24
|
+
message: iss.message,
|
|
25
|
+
suggestion: suggestFix(c.name, iss.rule || "", iss.message),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return issues;
|
|
30
|
+
}
|
|
31
|
+
/** Fix issues using AI. Returns results for each attempted fix. */
|
|
32
|
+
export async function aiFixIssues(cwd, issues, opts) {
|
|
33
|
+
const apiKey = process.env.ANTHROPIC_API_KEY || "";
|
|
34
|
+
const proKey = process.env.VCQA_PRO_KEY || "";
|
|
35
|
+
if (!apiKey && !proKey) {
|
|
36
|
+
console.log(" \x1b[31mNo API key found.\x1b[0m Set ANTHROPIC_API_KEY or VCQA_PRO_KEY.");
|
|
37
|
+
console.log(" \x1b[2mANTHROPIC_API_KEY — direct Claude API (recommended)\x1b[0m");
|
|
38
|
+
console.log(" \x1b[2mVCQA_PRO_KEY — via api.vibecodeqa.online\x1b[0m");
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const results = [];
|
|
42
|
+
// Process files in order, one issue at a time to avoid drift
|
|
43
|
+
const sorted = [...issues].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
44
|
+
// Limit to 10 fixes per run to avoid burning tokens
|
|
45
|
+
const batch = sorted.slice(0, 10);
|
|
46
|
+
if (sorted.length > 10) {
|
|
47
|
+
console.log(` \x1b[2mFixing 10 of ${sorted.length} issues (run again for more)\x1b[0m`);
|
|
48
|
+
}
|
|
49
|
+
for (const issue of batch) {
|
|
50
|
+
const absPath = join(cwd, issue.file);
|
|
51
|
+
let content;
|
|
52
|
+
try {
|
|
53
|
+
content = readFileSync(absPath, "utf-8");
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
results.push({ ...issue, explanation: "file not readable", applied: false });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const lines = content.split("\n");
|
|
60
|
+
const meta = getCheckMeta(issue.check);
|
|
61
|
+
// Extract context: ±20 lines around the issue
|
|
62
|
+
const start = Math.max(0, issue.line - 21);
|
|
63
|
+
const end = Math.min(lines.length, issue.line + 20);
|
|
64
|
+
const contextLines = lines.slice(start, end);
|
|
65
|
+
const numbered = contextLines.map((l, i) => `${start + i + 1}| ${l}`).join("\n");
|
|
66
|
+
const prompt = buildFixPrompt(issue, meta, numbered, issue.file);
|
|
67
|
+
process.stdout.write(` ${issue.file}:${issue.line} \x1b[2m${issue.message.slice(0, 50)}\x1b[0m `);
|
|
68
|
+
const fix = apiKey
|
|
69
|
+
? await callAnthropicDirect(prompt, apiKey)
|
|
70
|
+
: await callVcqaProxy(prompt, proKey);
|
|
71
|
+
if (!fix) {
|
|
72
|
+
console.log("\x1b[33mskip\x1b[0m");
|
|
73
|
+
results.push({ ...issue, explanation: "AI could not generate fix", applied: false });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (opts.dryRun) {
|
|
77
|
+
console.log("\x1b[36mdry-run\x1b[0m");
|
|
78
|
+
console.log(` \x1b[2m${fix.explanation}\x1b[0m`);
|
|
79
|
+
results.push({ ...issue, explanation: fix.explanation, applied: false });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
// Apply search/replace
|
|
83
|
+
const searchNormalized = fix.search.trim();
|
|
84
|
+
if (!content.includes(searchNormalized)) {
|
|
85
|
+
console.log("\x1b[33mskip (no match)\x1b[0m");
|
|
86
|
+
results.push({ ...issue, explanation: "search text not found in file", applied: false });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const idx = content.indexOf(searchNormalized);
|
|
90
|
+
const updated = content.slice(0, idx) + fix.replace.trim() + content.slice(idx + searchNormalized.length);
|
|
91
|
+
writeFileSync(absPath, updated);
|
|
92
|
+
console.log("\x1b[32mfixed\x1b[0m");
|
|
93
|
+
console.log(` \x1b[2m${fix.explanation}\x1b[0m`);
|
|
94
|
+
results.push({ ...issue, explanation: fix.explanation, applied: true });
|
|
95
|
+
}
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
98
|
+
function buildFixPrompt(issue, meta, codeContext, filePath) {
|
|
99
|
+
const ext = filePath.split(".").pop() || "ts";
|
|
100
|
+
const lang = ext === "vue" ? "vue" : ext === "svelte" ? "svelte" : ext === "dart" ? "dart" : "typescript";
|
|
101
|
+
let prompt = `Fix this code quality issue. Return a JSON object with search/replace strings.
|
|
102
|
+
|
|
103
|
+
## Issue
|
|
104
|
+
- Check: ${issue.check}
|
|
105
|
+
- Rule: ${issue.rule || "n/a"}
|
|
106
|
+
- Message: ${issue.message}
|
|
107
|
+
- File: ${filePath}:${issue.line}
|
|
108
|
+
|
|
109
|
+
## Why this matters
|
|
110
|
+
${meta.risk}
|
|
111
|
+
|
|
112
|
+
## Recommended approach
|
|
113
|
+
${meta.recommendation}`;
|
|
114
|
+
if (issue.suggestion) {
|
|
115
|
+
prompt += `\n\nSpecific suggestion: ${issue.suggestion}`;
|
|
116
|
+
}
|
|
117
|
+
prompt += `
|
|
118
|
+
|
|
119
|
+
## Code context (${filePath})
|
|
120
|
+
\`\`\`${lang}
|
|
121
|
+
${codeContext}
|
|
122
|
+
\`\`\`
|
|
123
|
+
|
|
124
|
+
## Instructions
|
|
125
|
+
Return ONLY a valid JSON object (no markdown, no explanation outside JSON):
|
|
126
|
+
{
|
|
127
|
+
"search": "exact lines from the code above that need to change (copy verbatim, preserve whitespace)",
|
|
128
|
+
"replace": "the fixed replacement code (same indentation)",
|
|
129
|
+
"explanation": "one sentence: what changed and why"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
Rules:
|
|
133
|
+
- The "search" string MUST appear verbatim in the file — copy it exactly
|
|
134
|
+
- Keep the fix minimal — change only what's needed to resolve the issue
|
|
135
|
+
- Preserve surrounding code, comments, and formatting
|
|
136
|
+
- Do not add imports unless absolutely required
|
|
137
|
+
- Match the existing code style`;
|
|
138
|
+
return prompt;
|
|
139
|
+
}
|
|
140
|
+
let _apiErrorShown = false;
|
|
141
|
+
async function callAnthropicDirect(prompt, apiKey) {
|
|
142
|
+
try {
|
|
143
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: {
|
|
146
|
+
"Content-Type": "application/json",
|
|
147
|
+
"x-api-key": apiKey,
|
|
148
|
+
"anthropic-version": "2023-06-01",
|
|
149
|
+
},
|
|
150
|
+
body: JSON.stringify({
|
|
151
|
+
model: "claude-haiku-4-5-20251001",
|
|
152
|
+
max_tokens: 1024,
|
|
153
|
+
messages: [{ role: "user", content: prompt }],
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
if (!res.ok) {
|
|
157
|
+
if (!_apiErrorShown) {
|
|
158
|
+
const err = (await res.json().catch(() => null));
|
|
159
|
+
console.log(`\n \x1b[31mAPI error:\x1b[0m ${err?.error?.message || `HTTP ${res.status}`}\n`);
|
|
160
|
+
_apiErrorShown = true;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const data = (await res.json());
|
|
165
|
+
const text = data.content?.find((c) => c.type === "text")?.text;
|
|
166
|
+
if (!text)
|
|
167
|
+
return null;
|
|
168
|
+
return parseFixResponse(text);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function callVcqaProxy(prompt, proKey) {
|
|
175
|
+
try {
|
|
176
|
+
const res = await fetch("https://api.vibecodeqa.online/api/pro/fix", {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: {
|
|
179
|
+
"Content-Type": "application/json",
|
|
180
|
+
Authorization: `Bearer ${proKey}`,
|
|
181
|
+
},
|
|
182
|
+
body: JSON.stringify({ prompt }),
|
|
183
|
+
});
|
|
184
|
+
if (!res.ok)
|
|
185
|
+
return null;
|
|
186
|
+
const data = (await res.json());
|
|
187
|
+
if (!data.search || !data.replace)
|
|
188
|
+
return null;
|
|
189
|
+
return { search: data.search, replace: data.replace, explanation: data.explanation || "" };
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function parseFixResponse(text) {
|
|
196
|
+
try {
|
|
197
|
+
// Try to extract JSON from the response (handle markdown fences)
|
|
198
|
+
let json = text.trim();
|
|
199
|
+
const fenceMatch = json.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
200
|
+
if (fenceMatch)
|
|
201
|
+
json = fenceMatch[1].trim();
|
|
202
|
+
const parsed = JSON.parse(json);
|
|
203
|
+
if (!parsed.search || typeof parsed.search !== "string")
|
|
204
|
+
return null;
|
|
205
|
+
if (!parsed.replace || typeof parsed.replace !== "string")
|
|
206
|
+
return null;
|
|
207
|
+
return {
|
|
208
|
+
search: parsed.search,
|
|
209
|
+
replace: parsed.replace,
|
|
210
|
+
explanation: parsed.explanation || "",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
/** vibe-check — code health scanner for the AI coding era. */
|
|
3
3
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
|
+
import { aiFixIssues, collectFixableIssues } from "./ai-fix.js";
|
|
5
6
|
import { getCheckMeta } from "./check-meta.js";
|
|
6
7
|
import { getCheckIgnore, isCheckEnabled, loadConfig } from "./config.js";
|
|
7
8
|
import { detectRepoUrl, detectStack, detectWorkspace } from "./detect.js";
|
|
@@ -387,6 +388,9 @@ function printHelp() {
|
|
|
387
388
|
\x1b[1mCommands:\x1b[0m
|
|
388
389
|
init [path] Set up CI workflow + recommended configs
|
|
389
390
|
fix [path] Auto-fix (.gitignore, strict mode, biome/eslint, suggestions)
|
|
391
|
+
--ai Use Claude to fix remaining issues (needs ANTHROPIC_API_KEY)
|
|
392
|
+
--check NAME Only fix issues from a specific check (e.g. --check security)
|
|
393
|
+
--dry-run Show what AI would fix without applying changes
|
|
390
394
|
explain [check] Deep-dive explanation of a check (what/risk/fix)
|
|
391
395
|
monitor [path] Live quality control panel — re-scans on file changes
|
|
392
396
|
|
|
@@ -411,6 +415,9 @@ function printHelp() {
|
|
|
411
415
|
npx @vibecodeqa/cli # scan current directory
|
|
412
416
|
npx @vibecodeqa/cli init # set up CI + configs
|
|
413
417
|
npx @vibecodeqa/cli fix # auto-fix what's fixable
|
|
418
|
+
npx @vibecodeqa/cli fix --ai # AI-powered fix (uses Claude)
|
|
419
|
+
npx @vibecodeqa/cli fix --ai --check security # fix only security issues
|
|
420
|
+
npx @vibecodeqa/cli fix --ai --dry-run # preview AI fixes without applying
|
|
414
421
|
npx @vibecodeqa/cli --skip-tests --top # fast scan with top issues
|
|
415
422
|
npx @vibecodeqa/cli --ci --fail-under 80 # CI with quality gate
|
|
416
423
|
`);
|
|
@@ -548,9 +555,9 @@ async function runExplain(checkName) {
|
|
|
548
555
|
console.log("");
|
|
549
556
|
}
|
|
550
557
|
// ── fix command ──
|
|
551
|
-
async function runFix(cwd) {
|
|
558
|
+
async function runFix(cwd, opts = {}) {
|
|
552
559
|
console.log("");
|
|
553
|
-
console.log(` \x1b[1m\x1b[38;5;141mvcqa fix\x1b[0m`);
|
|
560
|
+
console.log(` \x1b[1m\x1b[38;5;141mvcqa fix${opts.ai ? " --ai" : ""}${opts.dryRun ? " --dry-run" : ""}${opts.checkFilter ? ` --check ${opts.checkFilter}` : ""}\x1b[0m`);
|
|
554
561
|
console.log(` \x1b[2m${cwd}\x1b[0m`);
|
|
555
562
|
console.log("");
|
|
556
563
|
validateCwd(cwd);
|
|
@@ -621,7 +628,39 @@ async function runFix(cwd) {
|
|
|
621
628
|
const isDart = enrichedStack.language === "dart";
|
|
622
629
|
const checks = await runChecks(cwd, enrichedStack, workspace, true, isDart, true);
|
|
623
630
|
const score = computeScore(checks);
|
|
624
|
-
//
|
|
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)
|
|
625
664
|
const fixable = [];
|
|
626
665
|
for (const c of checks) {
|
|
627
666
|
for (const iss of c.issues) {
|
|
@@ -826,8 +865,13 @@ async function main() {
|
|
|
826
865
|
return;
|
|
827
866
|
}
|
|
828
867
|
if (args[0] === "fix") {
|
|
829
|
-
const
|
|
830
|
-
|
|
868
|
+
const fixArgs = args.slice(1);
|
|
869
|
+
const path = fixArgs.find((a) => !a.startsWith("-")) || ".";
|
|
870
|
+
const aiMode = fixArgs.includes("--ai");
|
|
871
|
+
const dryRun = fixArgs.includes("--dry-run");
|
|
872
|
+
const checkIdx = fixArgs.indexOf("--check");
|
|
873
|
+
const checkFilter = checkIdx !== -1 ? fixArgs[checkIdx + 1] : undefined;
|
|
874
|
+
await runFix(resolve(path), { ai: aiMode, dryRun, checkFilter });
|
|
831
875
|
return;
|
|
832
876
|
}
|
|
833
877
|
if (args[0] === "explain") {
|
|
@@ -907,11 +951,11 @@ async function main() {
|
|
|
907
951
|
emitAnnotations(report);
|
|
908
952
|
}
|
|
909
953
|
if (flags.uploadMode) {
|
|
910
|
-
await handleUpload(report, cwd,
|
|
954
|
+
await handleUpload(report, cwd, quietMode);
|
|
911
955
|
}
|
|
912
956
|
if (flags.prComment) {
|
|
913
957
|
const posted = await postPRComment(report, trend, cwd);
|
|
914
|
-
if (!
|
|
958
|
+
if (!quietMode) {
|
|
915
959
|
if (posted)
|
|
916
960
|
console.log(" \x1b[32m\u2713 PR comment posted\x1b[0m");
|
|
917
961
|
else
|
|
@@ -921,12 +965,12 @@ async function main() {
|
|
|
921
965
|
// CI exit code: fail if score below threshold (skip in watch mode)
|
|
922
966
|
const failUnder = flags.failUnder ?? (ciMode ? 60 : (config.failUnder ?? 0));
|
|
923
967
|
if (failUnder > 0 && score < failUnder && !watchMode) {
|
|
924
|
-
if (!
|
|
968
|
+
if (!quietMode)
|
|
925
969
|
console.log(` \x1b[31mFailing: score ${score} < ${failUnder}\x1b[0m\n`);
|
|
926
970
|
process.exit(1);
|
|
927
971
|
}
|
|
928
972
|
// Non-blocking update check (don't slow down the scan)
|
|
929
|
-
if (!
|
|
973
|
+
if (!quietMode && !ciMode && !watchMode && !process.env.VCQA_NO_UPDATE_CHECK) {
|
|
930
974
|
checkForUpdate(VERSION).catch(() => { });
|
|
931
975
|
}
|
|
932
976
|
if (watchMode) {
|
package/dist/fs-utils.js
CHANGED