@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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** vcqa explain — deep-dive explanation of a check. */
|
|
2
|
+
import { getCheckMeta } from "../check-meta.js";
|
|
3
|
+
export async function runExplain(checkName) {
|
|
4
|
+
if (!checkName) {
|
|
5
|
+
console.log("\n \x1b[1mUsage:\x1b[0m vcqa explain <check>\n");
|
|
6
|
+
console.log(" Available checks:");
|
|
7
|
+
const { CHECK_META } = await import("../check-meta.js");
|
|
8
|
+
for (const [name, meta] of Object.entries(CHECK_META)) {
|
|
9
|
+
console.log(` \x1b[1m${name.padEnd(16)}\x1b[0m ${meta.label} (${meta.category}, ${meta.weight}%)`);
|
|
10
|
+
}
|
|
11
|
+
console.log("");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const meta = getCheckMeta(checkName);
|
|
15
|
+
if (!meta.description || meta.description.length < 20) {
|
|
16
|
+
console.log(`\n \x1b[31mUnknown check: ${checkName}\x1b[0m`);
|
|
17
|
+
console.log(" Run \x1b[1mvcqa explain\x1b[0m to see available checks.\n");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
console.log("");
|
|
21
|
+
console.log(` \x1b[1m\x1b[38;5;141m${meta.label}\x1b[0m \x1b[2m${meta.category} · ${meta.priority} priority · ${meta.weight}% weight\x1b[0m`);
|
|
22
|
+
console.log("");
|
|
23
|
+
console.log(` \x1b[1mWhat:\x1b[0m ${meta.description}`);
|
|
24
|
+
console.log("");
|
|
25
|
+
console.log(` \x1b[1mRisk:\x1b[0m ${meta.risk}`);
|
|
26
|
+
console.log("");
|
|
27
|
+
console.log(` \x1b[1mFix:\x1b[0m ${meta.recommendation}`);
|
|
28
|
+
if (meta.deeperTools?.length) {
|
|
29
|
+
console.log("");
|
|
30
|
+
console.log(` \x1b[1mGo deeper:\x1b[0m ${meta.deeperTools.join(", ")}`);
|
|
31
|
+
}
|
|
32
|
+
console.log("");
|
|
33
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/** vcqa fix — auto-fix + AI-powered fixing. */
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { aiFixIssues, collectFixableIssues } from "../ai-fix.js";
|
|
5
|
+
import { scan } from "../core.js";
|
|
6
|
+
import { detectStack } from "../detect.js";
|
|
7
|
+
import { gradeFromScore } from "../types.js";
|
|
8
|
+
import { suggestFix, validateCwd } from "./shared.js";
|
|
9
|
+
export async function runFix(cwd, opts = {}) {
|
|
10
|
+
console.log("");
|
|
11
|
+
console.log(` \x1b[1m\x1b[38;5;141mvcqa fix${opts.ai ? " --ai" : ""}${opts.dryRun ? " --dry-run" : ""}${opts.checkFilter ? ` --check ${opts.checkFilter}` : ""}\x1b[0m`);
|
|
12
|
+
console.log(` \x1b[2m${cwd}\x1b[0m`);
|
|
13
|
+
console.log("");
|
|
14
|
+
validateCwd(cwd);
|
|
15
|
+
const stack = detectStack(cwd);
|
|
16
|
+
let fixed = 0;
|
|
17
|
+
// 0. Auto-fix structure issues (missing files)
|
|
18
|
+
if (!existsSync(join(cwd, ".gitignore"))) {
|
|
19
|
+
writeFileSync(join(cwd, ".gitignore"), "node_modules\ndist\n.vibe-check\ncoverage\n.env\n.env.local\n");
|
|
20
|
+
console.log(" \x1b[32m\u2713 Created .gitignore\x1b[0m");
|
|
21
|
+
fixed++;
|
|
22
|
+
}
|
|
23
|
+
if (existsSync(join(cwd, ".gitignore"))) {
|
|
24
|
+
const gi = readFileSync(join(cwd, ".gitignore"), "utf-8");
|
|
25
|
+
if (!gi.includes(".vibe-check")) {
|
|
26
|
+
writeFileSync(join(cwd, ".gitignore"), gi.trimEnd() + "\n.vibe-check/\n");
|
|
27
|
+
console.log(" \x1b[32m\u2713 Added .vibe-check/ to .gitignore\x1b[0m");
|
|
28
|
+
fixed++;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const tsconfigPath = join(cwd, "tsconfig.json");
|
|
32
|
+
if (existsSync(tsconfigPath)) {
|
|
33
|
+
try {
|
|
34
|
+
const raw = readFileSync(tsconfigPath, "utf-8");
|
|
35
|
+
const tsconfig = JSON.parse(raw);
|
|
36
|
+
if (!tsconfig.compilerOptions?.strict) {
|
|
37
|
+
tsconfig.compilerOptions = { ...tsconfig.compilerOptions, strict: true };
|
|
38
|
+
writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
|
|
39
|
+
console.log(' \x1b[32m\u2713 Enabled "strict": true in tsconfig.json\x1b[0m');
|
|
40
|
+
fixed++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch { /* can't parse tsconfig */ }
|
|
44
|
+
}
|
|
45
|
+
// 1. Run linter auto-fix
|
|
46
|
+
if (stack.linter === "biome") {
|
|
47
|
+
console.log(" \x1b[1mFormatting with Biome...\x1b[0m");
|
|
48
|
+
const { execSync } = await import("node:child_process");
|
|
49
|
+
try {
|
|
50
|
+
execSync("npx biome check --write .", { cwd, stdio: "inherit", timeout: 30_000 });
|
|
51
|
+
fixed++;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
console.log(" \x1b[33mBiome had issues (some may be unfixable)\x1b[0m");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else if (stack.linter === "eslint") {
|
|
58
|
+
console.log(" \x1b[1mFixing with ESLint...\x1b[0m");
|
|
59
|
+
const { execSync } = await import("node:child_process");
|
|
60
|
+
try {
|
|
61
|
+
execSync("npx eslint --fix src/", { cwd, stdio: "inherit", timeout: 30_000 });
|
|
62
|
+
fixed++;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
console.log(" \x1b[33mESLint had issues (some may be unfixable)\x1b[0m");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// 2. Scan for remaining issues
|
|
69
|
+
console.log("");
|
|
70
|
+
console.log(" \x1b[1mScanning for remaining issues...\x1b[0m");
|
|
71
|
+
console.log("");
|
|
72
|
+
const report = await scan(cwd, { skipTests: true });
|
|
73
|
+
const { checks } = report;
|
|
74
|
+
const score = report.score;
|
|
75
|
+
// AI-powered fix mode
|
|
76
|
+
if (opts.ai) {
|
|
77
|
+
const aiIssues = collectFixableIssues(checks, suggestFix, opts.checkFilter);
|
|
78
|
+
if (aiIssues.length === 0) {
|
|
79
|
+
console.log(" \x1b[2mNo fixable issues found.\x1b[0m");
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log(` \x1b[1mAI fixing ${Math.min(aiIssues.length, 10)} issues${opts.dryRun ? " (dry run)" : ""}...\x1b[0m`);
|
|
83
|
+
console.log("");
|
|
84
|
+
const results = await aiFixIssues(cwd, aiIssues, { dryRun: opts.dryRun || false });
|
|
85
|
+
const applied = results.filter((r) => r.applied).length;
|
|
86
|
+
if (applied > 0) {
|
|
87
|
+
console.log("");
|
|
88
|
+
console.log(" \x1b[1mRe-scanning...\x1b[0m");
|
|
89
|
+
const reReport = await scan(cwd, { skipTests: true });
|
|
90
|
+
const delta = reReport.score - score;
|
|
91
|
+
console.log(` Score: \x1b[${reReport.score >= 75 ? "32" : reReport.score >= 60 ? "33" : "31"}m${reReport.grade} ${reReport.score}/100\x1b[0m${delta > 0 ? ` \x1b[32m(+${delta})\x1b[0m` : ""}`);
|
|
92
|
+
console.log(` \x1b[32m${applied} AI fix(es) applied.\x1b[0m Re-run \x1b[1mnpx @vibecodeqa/cli\x1b[0m for full report.`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const grade = gradeFromScore(score);
|
|
96
|
+
console.log(`\n Score: \x1b[${score >= 75 ? "32" : score >= 60 ? "33" : "31"}m${grade} ${score}/100\x1b[0m`);
|
|
97
|
+
if (opts.dryRun)
|
|
98
|
+
console.log(" \x1b[2mDry run — no files modified. Remove --dry-run to apply.\x1b[0m");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
console.log("");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Non-AI mode: show fix suggestions
|
|
105
|
+
const fixable = [];
|
|
106
|
+
for (const c of checks) {
|
|
107
|
+
for (const iss of c.issues) {
|
|
108
|
+
if (!iss.file || typeof iss.file !== "string" || !iss.line)
|
|
109
|
+
continue;
|
|
110
|
+
const fix = suggestFix(c.name, iss.rule || "", iss.message);
|
|
111
|
+
if (fix)
|
|
112
|
+
fixable.push({ check: c.name, file: iss.file, line: iss.line, message: iss.message, fix });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const top = fixable.slice(0, 10);
|
|
116
|
+
if (top.length > 0) {
|
|
117
|
+
console.log(` \x1b[1m${top.length} issues with fix suggestions:\x1b[0m`);
|
|
118
|
+
console.log("");
|
|
119
|
+
for (const f of top) {
|
|
120
|
+
console.log(` \x1b[2m${f.file}:${f.line}\x1b[0m`);
|
|
121
|
+
console.log(` ${f.message}`);
|
|
122
|
+
console.log(` \x1b[32mFix: ${f.fix}\x1b[0m`);
|
|
123
|
+
console.log("");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const grade = gradeFromScore(score);
|
|
127
|
+
console.log(` Score after fix: \x1b[${score >= 75 ? "32" : score >= 60 ? "33" : "31"}m${grade} ${score}/100\x1b[0m`);
|
|
128
|
+
if (fixed > 0)
|
|
129
|
+
console.log(` \x1b[32m${fixed} auto-fix(es) applied.\x1b[0m Re-run \x1b[1mnpx @vibecodeqa/cli\x1b[0m for full report.`);
|
|
130
|
+
console.log("");
|
|
131
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** vcqa init — set up CI workflow + recommended configs. */
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { detectStack } from "../detect.js";
|
|
5
|
+
import { validateCwd } from "./shared.js";
|
|
6
|
+
export async function runInit(cwd) {
|
|
7
|
+
console.log("");
|
|
8
|
+
console.log(` \x1b[1m\x1b[38;5;141mvcqa init\x1b[0m`);
|
|
9
|
+
console.log(` \x1b[2m${cwd}\x1b[0m`);
|
|
10
|
+
console.log("");
|
|
11
|
+
validateCwd(cwd);
|
|
12
|
+
const stack = detectStack(cwd);
|
|
13
|
+
let created = 0;
|
|
14
|
+
// 1. GitHub Actions workflow
|
|
15
|
+
const workflowDir = join(cwd, ".github", "workflows");
|
|
16
|
+
const workflowPath = join(workflowDir, "vibecodeqa.yml");
|
|
17
|
+
if (!existsSync(workflowPath)) {
|
|
18
|
+
try {
|
|
19
|
+
mkdirSync(workflowDir, { recursive: true });
|
|
20
|
+
writeFileSync(workflowPath, `name: VibeCode QA
|
|
21
|
+
on: [pull_request]
|
|
22
|
+
permissions: { contents: read }
|
|
23
|
+
jobs:
|
|
24
|
+
scan:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
- run: npx @vibecodeqa/cli --ci --fail-under 70 --sarif --badge
|
|
29
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
30
|
+
if: always()
|
|
31
|
+
with:
|
|
32
|
+
sarif_file: .vibe-check/report.sarif
|
|
33
|
+
`);
|
|
34
|
+
console.log(` \x1b[32m+\x1b[0m .github/workflows/vibecodeqa.yml`);
|
|
35
|
+
created++;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
console.log(` \x1b[31m!\x1b[0m .github/workflows/vibecodeqa.yml (write failed — check permissions)`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.log(` \x1b[2m=\x1b[0m .github/workflows/vibecodeqa.yml (exists)`);
|
|
43
|
+
}
|
|
44
|
+
// 2. Biome config (if biome is a dep but no config exists)
|
|
45
|
+
if ((stack.linter === "biome" || existsSync(join(cwd, "node_modules", "@biomejs", "biome"))) &&
|
|
46
|
+
!existsSync(join(cwd, "biome.json")) &&
|
|
47
|
+
!existsSync(join(cwd, "biome.jsonc"))) {
|
|
48
|
+
writeFileSync(join(cwd, "biome.json"), JSON.stringify({
|
|
49
|
+
$schema: "https://biomejs.dev/schemas/2.0.0/schema.json",
|
|
50
|
+
formatter: { indentStyle: "tab", lineWidth: 120 },
|
|
51
|
+
linter: { enabled: true, rules: { recommended: true } },
|
|
52
|
+
organizeImports: { enabled: true },
|
|
53
|
+
}, null, "\t") + "\n");
|
|
54
|
+
console.log(` \x1b[32m+\x1b[0m biome.json`);
|
|
55
|
+
created++;
|
|
56
|
+
}
|
|
57
|
+
// 3. Create .vcqa.json if not present
|
|
58
|
+
const vcqaConfigPath = join(cwd, ".vcqa.json");
|
|
59
|
+
if (!existsSync(vcqaConfigPath)) {
|
|
60
|
+
const { CHECK_META } = await import("../check-meta.js");
|
|
61
|
+
const checksConfig = {};
|
|
62
|
+
for (const name of Object.keys(CHECK_META)) {
|
|
63
|
+
checksConfig[name] = {};
|
|
64
|
+
}
|
|
65
|
+
const config = {
|
|
66
|
+
_comment: "vcqa config — docs: https://vibecodeqa.online/skills",
|
|
67
|
+
checks: checksConfig,
|
|
68
|
+
_checks_help: "Set { \"enabled\": false } to disable. Add \"ignore\": [\"generated/**\"] to skip files per-check.",
|
|
69
|
+
ignore: [],
|
|
70
|
+
_ignore_help: "Global file patterns to skip: [\"vendor/**\", \"*.generated.ts\", \"proto/**\"]",
|
|
71
|
+
failUnder: 60,
|
|
72
|
+
_failUnder_help: "Exit with code 1 if score below this. Overridden by --fail-under flag.",
|
|
73
|
+
};
|
|
74
|
+
writeFileSync(vcqaConfigPath, JSON.stringify(config, null, 2) + "\n");
|
|
75
|
+
console.log(` \x1b[32m+\x1b[0m .vcqa.json`);
|
|
76
|
+
created++;
|
|
77
|
+
}
|
|
78
|
+
// 4. Add .vibe-check to .gitignore
|
|
79
|
+
const gitignorePath = join(cwd, ".gitignore");
|
|
80
|
+
if (existsSync(gitignorePath)) {
|
|
81
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
82
|
+
if (!content.includes(".vibe-check")) {
|
|
83
|
+
writeFileSync(gitignorePath, content.trimEnd() + "\n.vibe-check/\n");
|
|
84
|
+
console.log(` \x1b[32m+\x1b[0m .gitignore (added .vibe-check/)`);
|
|
85
|
+
created++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
console.log("");
|
|
89
|
+
if (created > 0) {
|
|
90
|
+
console.log(` \x1b[32mCreated ${created} file(s).\x1b[0m Run \x1b[1mnpx @vibecodeqa/cli\x1b[0m to scan.`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
console.log(` \x1b[2mAlready set up. Run npx @vibecodeqa/cli to scan.\x1b[0m`);
|
|
94
|
+
}
|
|
95
|
+
console.log("");
|
|
96
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Shared utilities for CLI commands. */
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
export function validateCwd(cwd) {
|
|
4
|
+
if (!existsSync(cwd)) {
|
|
5
|
+
console.error(` \x1b[31mError: path does not exist: ${cwd}\x1b[0m`);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
if (!statSync(cwd).isDirectory()) {
|
|
10
|
+
console.error(` \x1b[31mError: not a directory: ${cwd}\x1b[0m`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
console.error(` \x1b[31mError: cannot access: ${cwd}\x1b[0m`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Map common issue rules to actionable fix suggestions. */
|
|
20
|
+
export function suggestFix(check, rule, message) {
|
|
21
|
+
if (rule === "empty-catch")
|
|
22
|
+
return "Add error logging: catch(e) { console.error(e); }";
|
|
23
|
+
if (rule === "throw-string")
|
|
24
|
+
return 'Replace throw "msg" with throw new Error("msg")';
|
|
25
|
+
if (rule === "swallowed-promise")
|
|
26
|
+
return "Add logging: .catch((e) => { console.error(e); })";
|
|
27
|
+
if (rule === "floating-promise")
|
|
28
|
+
return "Add await or .catch() to handle the promise";
|
|
29
|
+
if (rule === "unsafe-json-parse")
|
|
30
|
+
return "Wrap in try-catch: try { JSON.parse(x) } catch { /* handle */ }";
|
|
31
|
+
if (rule === "no-error-boundary")
|
|
32
|
+
return "Add <ErrorBoundary> wrapper in your React app root";
|
|
33
|
+
if (rule === "img-alt")
|
|
34
|
+
return 'Add alt attribute: <img alt="description" ...>';
|
|
35
|
+
if (rule === "click-events")
|
|
36
|
+
return 'Add role="button" and onKeyDown handler';
|
|
37
|
+
if (rule === "vue-v-for-key")
|
|
38
|
+
return 'Add :key="item.id" to the v-for element';
|
|
39
|
+
if (rule === "missing-key")
|
|
40
|
+
return "Add key={item.id} to the JSX element in .map()";
|
|
41
|
+
if (rule === "index-key")
|
|
42
|
+
return "Use a stable unique ID instead of array index for key";
|
|
43
|
+
if (rule === "conditional-hook")
|
|
44
|
+
return "Move the hook call before any conditional (if/switch)";
|
|
45
|
+
if (rule === "no-tests")
|
|
46
|
+
return "Create a test file: src/__tests__/example.test.ts";
|
|
47
|
+
if (rule === "no-readme")
|
|
48
|
+
return "Create README.md with: project description, install, usage";
|
|
49
|
+
if (rule === "no-changelog")
|
|
50
|
+
return "Create CHANGELOG.md or use changesets: npx changeset init";
|
|
51
|
+
if (rule === "env-not-ignored")
|
|
52
|
+
return "Add .env to .gitignore";
|
|
53
|
+
if (rule === "secret-detected")
|
|
54
|
+
return "Move to environment variable, rotate the exposed secret";
|
|
55
|
+
if (rule === "no-ci")
|
|
56
|
+
return "Run: npx @vibecodeqa/cli init";
|
|
57
|
+
if (rule === "missing-lockfile")
|
|
58
|
+
return "Run: pnpm install (or npm install) to generate lockfile";
|
|
59
|
+
if (rule === "missing-file" && message.includes("LICENSE"))
|
|
60
|
+
return "Add LICENSE file: https://choosealicense.com/";
|
|
61
|
+
if (rule === "long-function")
|
|
62
|
+
return "Extract logic into smaller helper functions";
|
|
63
|
+
if (rule === "high-complexity")
|
|
64
|
+
return "Reduce nesting: use early returns, extract conditions";
|
|
65
|
+
if (rule === "duplicate-code")
|
|
66
|
+
return "Extract shared logic into a helper function";
|
|
67
|
+
if (rule === "circular-dep")
|
|
68
|
+
return "Extract shared types to a separate file both modules import";
|
|
69
|
+
if (rule === "god-module")
|
|
70
|
+
return "Split into focused interfaces — one responsibility per module";
|
|
71
|
+
if (rule === "process-exit")
|
|
72
|
+
return "Replace process.exit() with throw new Error()";
|
|
73
|
+
if (check === "security" && message.includes("innerHTML"))
|
|
74
|
+
return "Use textContent or DOM APIs instead";
|
|
75
|
+
if (check === "security" && message.includes("ev" + "al"))
|
|
76
|
+
return `Remove ${"ev" + "al"}() — use a safer alternative`;
|
|
77
|
+
if (check === "security" && message.includes("v-html"))
|
|
78
|
+
return 'Sanitize with DOMPurify: v-html="DOMPurify.sanitize(input)"';
|
|
79
|
+
return null;
|
|
80
|
+
}
|
package/dist/core.js
CHANGED
|
@@ -23,7 +23,11 @@ import { runContainerHealth } from "./runners/container-health.js";
|
|
|
23
23
|
import { runConfusion } from "./runners/confusion.js";
|
|
24
24
|
import { runContext } from "./runners/context.js";
|
|
25
25
|
import { runDependencies } from "./runners/dependencies.js";
|
|
26
|
+
import { runDesignConsistency } from "./runners/design-consistency.js";
|
|
26
27
|
import { runEnvValidation } from "./runners/env-validation.js";
|
|
28
|
+
import { runFrontendHealth } from "./runners/frontend-health.js";
|
|
29
|
+
import { runHtmlQuality } from "./runners/html-quality.js";
|
|
30
|
+
import { runFileCohesion } from "./runners/file-cohesion.js";
|
|
27
31
|
import { runGitHygiene } from "./runners/git-hygiene.js";
|
|
28
32
|
import { runDocCoherence } from "./runners/doc-coherence.js";
|
|
29
33
|
import { runDocs } from "./runners/docs.js";
|
|
@@ -32,6 +36,7 @@ import { runErrorHandling } from "./runners/error-handling.js";
|
|
|
32
36
|
import { runLint } from "./runners/lint.js";
|
|
33
37
|
import { runMemorySafety } from "./runners/memory-safety.js";
|
|
34
38
|
import { runPerformance } from "./runners/performance.js";
|
|
39
|
+
import { runStyling } from "./runners/styling.js";
|
|
35
40
|
import { runReact } from "./runners/react.js";
|
|
36
41
|
import { runSecrets } from "./runners/secrets.js";
|
|
37
42
|
import { runSecurity } from "./runners/security.js";
|
|
@@ -84,8 +89,13 @@ export async function scan(cwd, options = {}) {
|
|
|
84
89
|
{ name: "doc-coherence", fn: () => runDocCoherence(resolvedCwd) },
|
|
85
90
|
{ name: "code-coherence", fn: () => runCodeCoherence(resolvedCwd) },
|
|
86
91
|
{ name: "comment-staleness", fn: () => runCommentStaleness(resolvedCwd) },
|
|
92
|
+
{ name: "html-quality", fn: () => runHtmlQuality(resolvedCwd) },
|
|
93
|
+
{ name: "frontend-health", fn: () => runFrontendHealth(resolvedCwd) },
|
|
94
|
+
{ name: "styling", fn: () => runStyling(resolvedCwd) },
|
|
87
95
|
{ name: "dead-patterns", fn: () => runDeadPatterns(resolvedCwd) },
|
|
88
96
|
{ name: "test-audit", fn: () => runTestAudit(resolvedCwd) },
|
|
97
|
+
{ name: "file-cohesion", fn: () => runFileCohesion(resolvedCwd) },
|
|
98
|
+
{ name: "design-consistency", fn: () => runDesignConsistency(resolvedCwd) },
|
|
89
99
|
];
|
|
90
100
|
// Filter checks if specified
|
|
91
101
|
const checkFilter = options.checks ? new Set(options.checks) : null;
|
|
@@ -183,7 +183,10 @@ export function runAccessibility(cwd) {
|
|
|
183
183
|
name: "accessibility",
|
|
184
184
|
score,
|
|
185
185
|
grade: gradeFromScore(score),
|
|
186
|
-
details: {
|
|
186
|
+
details: {
|
|
187
|
+
jsxFiles: files.length, missingAlt, clickDiv, missingLabel, missingLang, autofocus, positiveTabindex,
|
|
188
|
+
suggestion: !hasA11yPlugin ? "Install eslint-plugin-jsx-a11y for deeper accessibility analysis: pnpm add -D eslint-plugin-jsx-a11y" : undefined,
|
|
189
|
+
},
|
|
187
190
|
issues,
|
|
188
191
|
duration: Date.now() - start,
|
|
189
192
|
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Design Consistency — LLM-powered audit of visual consistency across components.
|
|
2
|
+
*
|
|
3
|
+
* Pro feature. Requires VCQA_PRO_KEY env var.
|
|
4
|
+
*
|
|
5
|
+
* Detects:
|
|
6
|
+
* - Components that define the same visual pattern differently (accidental design system)
|
|
7
|
+
* - Spacing/color/typography inconsistencies across files
|
|
8
|
+
* - Missing component extraction opportunities
|
|
9
|
+
* - Tailwind class patterns that should be @apply'd or componentized
|
|
10
|
+
*/
|
|
11
|
+
import type { CheckResult } from "../types.js";
|
|
12
|
+
export declare function runDesignConsistency(cwd: string): Promise<CheckResult>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/** Design Consistency — LLM-powered audit of visual consistency across components.
|
|
2
|
+
*
|
|
3
|
+
* Pro feature. Requires VCQA_PRO_KEY env var.
|
|
4
|
+
*
|
|
5
|
+
* Detects:
|
|
6
|
+
* - Components that define the same visual pattern differently (accidental design system)
|
|
7
|
+
* - Spacing/color/typography inconsistencies across files
|
|
8
|
+
* - Missing component extraction opportunities
|
|
9
|
+
* - Tailwind class patterns that should be @apply'd or componentized
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { getProductionFiles } from "../fs-utils.js";
|
|
15
|
+
import { gradeFromScore } from "../types.js";
|
|
16
|
+
export async function runDesignConsistency(cwd) {
|
|
17
|
+
const start = Date.now();
|
|
18
|
+
const proKey = process.env.VCQA_PRO_KEY || "";
|
|
19
|
+
if (!proKey) {
|
|
20
|
+
return {
|
|
21
|
+
name: "design-consistency",
|
|
22
|
+
score: 0,
|
|
23
|
+
grade: "F",
|
|
24
|
+
details: {
|
|
25
|
+
premium: true,
|
|
26
|
+
comingSoon: true,
|
|
27
|
+
reason: "Set VCQA_PRO_KEY to enable design consistency analysis",
|
|
28
|
+
description: "LLM-powered audit of visual consistency — finds components with duplicate styling, inconsistent spacing scales, and missing component extraction opportunities.",
|
|
29
|
+
},
|
|
30
|
+
issues: [],
|
|
31
|
+
duration: Date.now() - start,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const files = getProductionFiles(cwd);
|
|
35
|
+
const componentFiles = files.filter((f) => !f.isTest && /\.(tsx|jsx|vue|svelte)$/.test(f.path));
|
|
36
|
+
if (componentFiles.length < 2) {
|
|
37
|
+
return {
|
|
38
|
+
name: "design-consistency",
|
|
39
|
+
score: 100,
|
|
40
|
+
grade: "A",
|
|
41
|
+
details: { componentsAnalyzed: componentFiles.length },
|
|
42
|
+
issues: [],
|
|
43
|
+
duration: Date.now() - start,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Build hash of all component content for cache key
|
|
47
|
+
const h = createHash("sha256");
|
|
48
|
+
for (const f of componentFiles.sort((a, b) => a.path.localeCompare(b.path))) {
|
|
49
|
+
h.update(f.path);
|
|
50
|
+
h.update(f.content.slice(0, 2000));
|
|
51
|
+
}
|
|
52
|
+
const contentHash = h.digest("hex").slice(0, 16);
|
|
53
|
+
// Check cache
|
|
54
|
+
const cache = loadCache(cwd);
|
|
55
|
+
if (cache && cache.hash === contentHash) {
|
|
56
|
+
return {
|
|
57
|
+
name: "design-consistency",
|
|
58
|
+
score: cache.findings.length === 0 ? 100 : Math.max(20, 100 - cache.findings.length * 12),
|
|
59
|
+
grade: gradeFromScore(cache.findings.length === 0 ? 100 : Math.max(20, 100 - cache.findings.length * 12)),
|
|
60
|
+
details: { premium: true, componentsAnalyzed: componentFiles.length, cached: true },
|
|
61
|
+
issues: cache.findings,
|
|
62
|
+
duration: Date.now() - start,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Extract styling snippets from top components (by size, most likely to have styling)
|
|
66
|
+
const candidates = componentFiles
|
|
67
|
+
.sort((a, b) => b.content.length - a.content.length)
|
|
68
|
+
.slice(0, 10);
|
|
69
|
+
const snippets = candidates.map((f) => ({
|
|
70
|
+
path: f.path,
|
|
71
|
+
content: f.content.slice(0, 2000),
|
|
72
|
+
}));
|
|
73
|
+
const issues = await analyzeDesign(snippets, proKey);
|
|
74
|
+
// Cache results
|
|
75
|
+
saveCache(cwd, { version: 1, hash: contentHash, findings: issues });
|
|
76
|
+
const score = issues.length === 0 ? 100 : Math.max(20, 100 - issues.length * 12);
|
|
77
|
+
return {
|
|
78
|
+
name: "design-consistency",
|
|
79
|
+
score,
|
|
80
|
+
grade: gradeFromScore(score),
|
|
81
|
+
details: { premium: true, componentsAnalyzed: componentFiles.length },
|
|
82
|
+
issues,
|
|
83
|
+
duration: Date.now() - start,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function analyzeDesign(files, proKey) {
|
|
87
|
+
try {
|
|
88
|
+
const res = await fetch("https://api.vibecodeqa.online/api/pro/design-consistency", {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: {
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
Authorization: `Bearer ${proKey}`,
|
|
93
|
+
},
|
|
94
|
+
body: JSON.stringify({ files: files.map((f) => ({ path: f.path, content: f.content })) }),
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok)
|
|
97
|
+
return [];
|
|
98
|
+
const data = (await res.json());
|
|
99
|
+
return data.findings || [];
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function loadCache(cwd) {
|
|
106
|
+
try {
|
|
107
|
+
const cachePath = join(cwd, ".vibe-check", "design-consistency-cache.json");
|
|
108
|
+
if (existsSync(cachePath)) {
|
|
109
|
+
const data = JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
110
|
+
if (data.version === 1)
|
|
111
|
+
return data;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch { /* corrupt cache */ }
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
function saveCache(cwd, cache) {
|
|
118
|
+
try {
|
|
119
|
+
const dir = join(cwd, ".vibe-check");
|
|
120
|
+
if (!existsSync(dir))
|
|
121
|
+
mkdirSync(dir, { recursive: true });
|
|
122
|
+
writeFileSync(join(dir, "design-consistency-cache.json"), JSON.stringify(cache));
|
|
123
|
+
}
|
|
124
|
+
catch { /* write failed */ }
|
|
125
|
+
}
|
|
@@ -25,12 +25,22 @@ export function runErrorHandling(cwd, stack) {
|
|
|
25
25
|
const line = lines[i].trim();
|
|
26
26
|
if (line.startsWith("//") || line.startsWith("*"))
|
|
27
27
|
continue;
|
|
28
|
-
// Skip
|
|
28
|
+
// Skip suppressed lines (// ok, // intentional, eslint-disable, biome-ignore)
|
|
29
|
+
if (/\/\/\s*(?:ok|intentional|eslint-disable|biome-ignore)/.test(line))
|
|
30
|
+
continue;
|
|
31
|
+
// Skip string-only lines, metadata definitions, and return statements with string literals
|
|
29
32
|
if (/^\s*["'`].*["'`][,;]?\s*$/.test(lines[i]))
|
|
30
33
|
continue;
|
|
31
34
|
if (/\b(?:message|description|risk|recommendation|name)\s*:\s*["']/.test(line))
|
|
32
35
|
continue;
|
|
36
|
+
if (/\breturn\s+["'`]/.test(line))
|
|
37
|
+
continue;
|
|
38
|
+
if (/\brule\s*===?\s*["']/.test(line))
|
|
39
|
+
continue;
|
|
33
40
|
if (/catch\s*\([^)]*\)\s*\{\s*\}/.test(line) || /catch\s*\{\s*\}/.test(line)) {
|
|
41
|
+
// Skip intentional one-liner try-catch for optional browser APIs
|
|
42
|
+
if (/try\s*\{.*(?:localStorage|sessionStorage|document\.).*\}\s*catch/.test(line))
|
|
43
|
+
continue;
|
|
34
44
|
emptyCatch++;
|
|
35
45
|
issues.push({ severity: "error", message: "Empty catch block", file: f.path, line: i + 1, rule: "empty-catch" });
|
|
36
46
|
}
|
|
@@ -83,8 +93,14 @@ export function runErrorHandling(cwd, stack) {
|
|
|
83
93
|
const line = lines[i].trim();
|
|
84
94
|
if (line.startsWith("//") || line.startsWith("*"))
|
|
85
95
|
continue;
|
|
96
|
+
if (/\/\/\s*(?:ok|intentional|eslint-disable|biome-ignore)/.test(line))
|
|
97
|
+
continue;
|
|
98
|
+
if (/^\s*["'`].*["'`][,;]?\s*$/.test(lines[i]))
|
|
99
|
+
continue;
|
|
86
100
|
if (/\b(?:message|description|risk|recommendation|name)\s*:\s*["']/.test(line))
|
|
87
101
|
continue;
|
|
102
|
+
if (/\breturn\s+["'`]/.test(line))
|
|
103
|
+
continue;
|
|
88
104
|
// JSON.parse of external input without try-catch
|
|
89
105
|
if (/\bJSON\.parse\s*\(/.test(line)) {
|
|
90
106
|
// Only flag if parsing user/network input (not internal known-safe files)
|
|
@@ -141,7 +157,7 @@ export function runErrorHandling(cwd, stack) {
|
|
|
141
157
|
}
|
|
142
158
|
}
|
|
143
159
|
// process.exit() in non-CLI files (library code shouldn't exit)
|
|
144
|
-
if (/\bprocess\.exit\s*\(/.test(line) && !f.path.includes("cli") && !f.path.includes("bin/")) {
|
|
160
|
+
if (/\bprocess\.exit\s*\(/.test(line) && !f.path.includes("cli") && !f.path.includes("bin/") && !f.path.includes("commands/") && !f.path.includes("monitor")) {
|
|
145
161
|
processExit++;
|
|
146
162
|
issues.push({
|
|
147
163
|
severity: "warning",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** File Cohesion — detects files with multiple responsibilities.
|
|
2
|
+
*
|
|
3
|
+
* Pro feature. Requires VCQA_PRO_KEY env var.
|
|
4
|
+
*
|
|
5
|
+
* Two-phase analysis:
|
|
6
|
+
* 1. Local heuristics (always run with Pro key):
|
|
7
|
+
* - Files with exports spanning multiple domains (auth + email + db)
|
|
8
|
+
* - Mixed concern signals: HTTP handlers + business logic + data access
|
|
9
|
+
* - High export count relative to file purpose
|
|
10
|
+
*
|
|
11
|
+
* 2. LLM-powered analysis (via api.vibecodeqa.online):
|
|
12
|
+
* - Labels each file's responsibility clusters
|
|
13
|
+
* - Suggests concrete split points
|
|
14
|
+
* - "This file handles auth, session, AND email — split into 3 modules"
|
|
15
|
+
*/
|
|
16
|
+
import type { CheckResult } from "../types.js";
|
|
17
|
+
export declare function runFileCohesion(cwd: string): Promise<CheckResult>;
|