@vibecodeqa/cli 0.41.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.
Files changed (37) hide show
  1. package/README.md +130 -165
  2. package/dist/check-meta.js +99 -6
  3. package/dist/cli.js +268 -761
  4. package/dist/commands/explain.d.ts +2 -0
  5. package/dist/commands/explain.js +33 -0
  6. package/dist/commands/fix.d.ts +6 -0
  7. package/dist/commands/fix.js +131 -0
  8. package/dist/commands/init.d.ts +2 -0
  9. package/dist/commands/init.js +96 -0
  10. package/dist/commands/shared.d.ts +4 -0
  11. package/dist/commands/shared.js +80 -0
  12. package/dist/core.js +18 -0
  13. package/dist/runners/accessibility.js +4 -1
  14. package/dist/runners/container-health.d.ts +3 -0
  15. package/dist/runners/container-health.js +141 -0
  16. package/dist/runners/design-consistency.d.ts +12 -0
  17. package/dist/runners/design-consistency.js +125 -0
  18. package/dist/runners/env-validation.d.ts +3 -0
  19. package/dist/runners/env-validation.js +122 -0
  20. package/dist/runners/error-handling.js +18 -2
  21. package/dist/runners/file-cohesion.d.ts +17 -0
  22. package/dist/runners/file-cohesion.js +177 -0
  23. package/dist/runners/frontend-health.d.ts +14 -0
  24. package/dist/runners/frontend-health.js +206 -0
  25. package/dist/runners/git-hygiene.d.ts +3 -0
  26. package/dist/runners/git-hygiene.js +125 -0
  27. package/dist/runners/html-quality.d.ts +8 -0
  28. package/dist/runners/html-quality.js +203 -0
  29. package/dist/runners/memory-safety.d.ts +3 -0
  30. package/dist/runners/memory-safety.js +114 -0
  31. package/dist/runners/react.js +1 -0
  32. package/dist/runners/secrets.js +7 -2
  33. package/dist/runners/security.js +7 -1
  34. package/dist/runners/standards.js +29 -9
  35. package/dist/runners/styling.d.ts +15 -0
  36. package/dist/runners/styling.js +280 -0
  37. package/package.json +1 -1
@@ -0,0 +1,2 @@
1
+ /** vcqa explain — deep-dive explanation of a check. */
2
+ export declare function runExplain(checkName?: string): Promise<void>;
@@ -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,6 @@
1
+ /** vcqa fix — auto-fix + AI-powered fixing. */
2
+ export declare function runFix(cwd: string, opts?: {
3
+ ai?: boolean;
4
+ dryRun?: boolean;
5
+ checkFilter?: string;
6
+ }): Promise<void>;
@@ -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,2 @@
1
+ /** vcqa init — set up CI workflow + recommended configs. */
2
+ export declare function runInit(cwd: string): Promise<void>;
@@ -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,4 @@
1
+ /** Shared utilities for CLI commands. */
2
+ export declare function validateCwd(cwd: string): void;
3
+ /** Map common issue rules to actionable fix suggestions. */
4
+ export declare function suggestFix(check: string, rule: string, message: string): string | null;
@@ -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
@@ -19,15 +19,24 @@ import { runCommentStaleness } from "./runners/comment-staleness.js";
19
19
  import { runComplexity } from "./runners/complexity.js";
20
20
  import { runDeadPatterns } from "./runners/dead-patterns.js";
21
21
  import { runTestAudit } from "./runners/test-audit.js";
22
+ import { runContainerHealth } from "./runners/container-health.js";
22
23
  import { runConfusion } from "./runners/confusion.js";
23
24
  import { runContext } from "./runners/context.js";
24
25
  import { runDependencies } from "./runners/dependencies.js";
26
+ import { runDesignConsistency } from "./runners/design-consistency.js";
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";
31
+ import { runGitHygiene } from "./runners/git-hygiene.js";
25
32
  import { runDocCoherence } from "./runners/doc-coherence.js";
26
33
  import { runDocs } from "./runners/docs.js";
27
34
  import { runDuplication } from "./runners/duplication.js";
28
35
  import { runErrorHandling } from "./runners/error-handling.js";
29
36
  import { runLint } from "./runners/lint.js";
37
+ import { runMemorySafety } from "./runners/memory-safety.js";
30
38
  import { runPerformance } from "./runners/performance.js";
39
+ import { runStyling } from "./runners/styling.js";
31
40
  import { runReact } from "./runners/react.js";
32
41
  import { runSecrets } from "./runners/secrets.js";
33
42
  import { runSecurity } from "./runners/security.js";
@@ -65,19 +74,28 @@ export async function scan(cwd, options = {}) {
65
74
  { name: "accessibility", fn: () => runAccessibility(resolvedCwd) },
66
75
  { name: "docs", fn: () => runDocs(resolvedCwd) },
67
76
  { name: "best-practices", fn: () => runBestPractices(resolvedCwd, workspace) },
77
+ { name: "env-validation", fn: () => runEnvValidation(resolvedCwd) },
78
+ { name: "git-hygiene", fn: () => runGitHygiene(resolvedCwd) },
79
+ { name: "memory-safety", fn: () => runMemorySafety(resolvedCwd) },
68
80
  { name: "testing", fn: () => runTesting(resolvedCwd, stack, skipTests, srcRoots) },
69
81
  { name: "secrets", fn: () => runSecrets(resolvedCwd) },
70
82
  { name: "security", fn: () => runSecurity(resolvedCwd) },
71
83
  { name: "dependencies", fn: () => runDependencies(resolvedCwd, stack) },
72
84
  { name: "architecture", fn: () => runArchitecture(resolvedCwd, workspace) },
73
85
  { name: "performance", fn: () => runPerformance(resolvedCwd) },
86
+ { name: "container-health", fn: () => runContainerHealth(resolvedCwd) },
74
87
  { name: "confusion", fn: () => runConfusion(resolvedCwd) },
75
88
  { name: "context", fn: () => runContext(resolvedCwd) },
76
89
  { name: "doc-coherence", fn: () => runDocCoherence(resolvedCwd) },
77
90
  { name: "code-coherence", fn: () => runCodeCoherence(resolvedCwd) },
78
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) },
79
95
  { name: "dead-patterns", fn: () => runDeadPatterns(resolvedCwd) },
80
96
  { name: "test-audit", fn: () => runTestAudit(resolvedCwd) },
97
+ { name: "file-cohesion", fn: () => runFileCohesion(resolvedCwd) },
98
+ { name: "design-consistency", fn: () => runDesignConsistency(resolvedCwd) },
81
99
  ];
82
100
  // Filter checks if specified
83
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: { jsxFiles: files.length, missingAlt, clickDiv, missingLabel, missingLang, autofocus, positiveTabindex },
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,3 @@
1
+ /** Container health — Dockerfile best practices, .dockerignore, base image hygiene. */
2
+ import type { CheckResult } from "../types.js";
3
+ export declare function runContainerHealth(cwd: string): CheckResult;
@@ -0,0 +1,141 @@
1
+ /** Container health — Dockerfile best practices, .dockerignore, base image hygiene. */
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { gradeFromScore } from "../types.js";
5
+ export function runContainerHealth(cwd) {
6
+ const start = Date.now();
7
+ const issues = [];
8
+ // Find Dockerfiles
9
+ const dockerfiles = [];
10
+ try {
11
+ for (const f of readdirSync(cwd)) {
12
+ if (f === "Dockerfile" || f.startsWith("Dockerfile.") || f === "dockerfile") {
13
+ dockerfiles.push(f);
14
+ }
15
+ }
16
+ }
17
+ catch { /* not readable */ }
18
+ if (dockerfiles.length === 0) {
19
+ return {
20
+ name: "container-health",
21
+ score: 0,
22
+ grade: "F",
23
+ details: { skipped: true, reason: "no Dockerfile found" },
24
+ issues: [],
25
+ duration: Date.now() - start,
26
+ };
27
+ }
28
+ // Check .dockerignore exists
29
+ if (!existsSync(join(cwd, ".dockerignore"))) {
30
+ issues.push({
31
+ severity: "warning",
32
+ message: "No .dockerignore — node_modules, .git, and secrets may be included in the image",
33
+ rule: "no-dockerignore",
34
+ });
35
+ }
36
+ else {
37
+ const dockerignore = readFileSync(join(cwd, ".dockerignore"), "utf-8");
38
+ const missing = [];
39
+ if (!dockerignore.includes("node_modules"))
40
+ missing.push("node_modules");
41
+ if (!dockerignore.includes(".git"))
42
+ missing.push(".git");
43
+ if (!dockerignore.includes(".env"))
44
+ missing.push(".env");
45
+ if (missing.length > 0) {
46
+ issues.push({
47
+ severity: "warning",
48
+ message: `.dockerignore missing: ${missing.join(", ")}`,
49
+ file: ".dockerignore",
50
+ rule: "dockerignore-incomplete",
51
+ });
52
+ }
53
+ }
54
+ for (const df of dockerfiles) {
55
+ const content = readFileSync(join(cwd, df), "utf-8");
56
+ const lines = content.split("\n");
57
+ // Check for unpinned base images (FROM node, FROM ubuntu — no tag)
58
+ for (let i = 0; i < lines.length; i++) {
59
+ const line = lines[i].trim();
60
+ if (/^FROM\s+\S+$/i.test(line) && !line.includes(":") && !line.includes("@") && !line.toLowerCase().includes("scratch")) {
61
+ issues.push({
62
+ severity: "error",
63
+ message: `Unpinned base image: ${line} — use a specific tag (e.g., node:22-slim)`,
64
+ file: df,
65
+ line: i + 1,
66
+ rule: "unpinned-base",
67
+ });
68
+ }
69
+ // Check for :latest tag
70
+ if (/^FROM\s+\S+:latest/i.test(line)) {
71
+ issues.push({
72
+ severity: "warning",
73
+ message: `Using :latest tag: ${line} — pin to a specific version for reproducible builds`,
74
+ file: df,
75
+ line: i + 1,
76
+ rule: "latest-tag",
77
+ });
78
+ }
79
+ }
80
+ // Check for running as root (no USER instruction)
81
+ if (!content.match(/^USER\s+/m)) {
82
+ issues.push({
83
+ severity: "warning",
84
+ message: "No USER instruction — container runs as root by default",
85
+ file: df,
86
+ rule: "runs-as-root",
87
+ });
88
+ }
89
+ // Check for multi-stage build (good practice for smaller images)
90
+ const fromCount = (content.match(/^FROM\s+/gim) || []).length;
91
+ if (fromCount === 1 && existsSync(join(cwd, "package.json"))) {
92
+ issues.push({
93
+ severity: "info",
94
+ message: "Single-stage build — multi-stage builds produce smaller images",
95
+ file: df,
96
+ rule: "no-multi-stage",
97
+ });
98
+ }
99
+ // Check for COPY before npm install (cache busting)
100
+ const copyAllIdx = lines.findIndex((l) => /^COPY\s+\.\s+/i.test(l.trim()));
101
+ const npmInstallIdx = lines.findIndex((l) => /npm install|pnpm install|yarn install/i.test(l));
102
+ if (copyAllIdx !== -1 && npmInstallIdx !== -1 && copyAllIdx < npmInstallIdx) {
103
+ issues.push({
104
+ severity: "warning",
105
+ message: "COPY . before npm install — copy package.json first to leverage Docker cache",
106
+ file: df,
107
+ line: copyAllIdx + 1,
108
+ rule: "cache-bust",
109
+ });
110
+ }
111
+ // Check for apt-get without cleanup
112
+ if (content.includes("apt-get install") && !content.includes("apt-get clean") && !content.includes("rm -rf /var/lib/apt")) {
113
+ issues.push({
114
+ severity: "info",
115
+ message: "apt-get install without cleanup — add 'apt-get clean && rm -rf /var/lib/apt/lists/*'",
116
+ file: df,
117
+ rule: "apt-no-clean",
118
+ });
119
+ }
120
+ // Check for EXPOSE
121
+ if (!content.match(/^EXPOSE\s+/m) && (content.includes("node") || content.includes("npm start"))) {
122
+ issues.push({
123
+ severity: "info",
124
+ message: "No EXPOSE instruction — document which port the app listens on",
125
+ file: df,
126
+ rule: "no-expose",
127
+ });
128
+ }
129
+ }
130
+ const errorCount = issues.filter((i) => i.severity === "error").length;
131
+ const warnCount = issues.filter((i) => i.severity === "warning").length;
132
+ const score = Math.max(0, 100 - errorCount * 25 - warnCount * 10);
133
+ return {
134
+ name: "container-health",
135
+ score,
136
+ grade: gradeFromScore(score),
137
+ details: { dockerfiles, hasDockerignore: existsSync(join(cwd, ".dockerignore")) },
138
+ issues,
139
+ duration: Date.now() - start,
140
+ };
141
+ }
@@ -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>;