@vibecodeqa/cli 0.39.1 → 0.41.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.
@@ -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
- // Collect actionable issues with fix suggestions
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 path = args.slice(1).find((a) => !a.startsWith("-")) || ".";
830
- await runFix(resolve(path));
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, jsonOnly);
954
+ await handleUpload(report, cwd, quietMode);
911
955
  }
912
956
  if (flags.prComment) {
913
957
  const posted = await postPRComment(report, trend, cwd);
914
- if (!jsonOnly) {
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 (!jsonOnly)
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 (!jsonOnly && !ciMode && !watchMode && !process.env.VCQA_NO_UPDATE_CHECK) {
973
+ if (!quietMode && !ciMode && !watchMode && !process.env.VCQA_NO_UPDATE_CHECK) {
930
974
  checkForUpdate(VERSION).catch(() => { });
931
975
  }
932
976
  if (watchMode) {
package/dist/core.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ /** @vibecodeqa/cli/core — Programmatic scan API.
2
+ *
3
+ * Usage:
4
+ * import { scan } from "@vibecodeqa/cli/core";
5
+ * const report = await scan("./src");
6
+ * console.log(report.score, report.grade);
7
+ */
8
+ import { CHECK_META, getCheckMeta, type CheckMeta } from "./check-meta.js";
9
+ import { type VcqaConfig } from "./config.js";
10
+ import type { CheckResult, Issue, StackInfo, VibeReport, WorkspaceInfo, WorkspacePackage } from "./types.js";
11
+ export interface ScanOptions {
12
+ /** Skip test execution (faster scan). Default: false */
13
+ skipTests?: boolean;
14
+ /** Only run these checks (by name). Default: all checks */
15
+ checks?: string[];
16
+ /** Override config (instead of loading from .vcqa.json). */
17
+ config?: VcqaConfig;
18
+ /** Progress callback — called for each completed check. */
19
+ onProgress?: (check: string, result: CheckResult, index: number, total: number) => void;
20
+ }
21
+ /** Run a full code health scan. Returns a VibeReport with score, grade, and all check results. */
22
+ export declare function scan(cwd: string, options?: ScanOptions): Promise<VibeReport>;
23
+ export { CHECK_META, getCheckMeta, type CheckMeta };
24
+ export { computeScore } from "./score.js";
25
+ export { loadConfig, type VcqaConfig } from "./config.js";
26
+ export { detectStack, detectWorkspace } from "./detect.js";
27
+ export { gradeFromScore } from "./types.js";
28
+ export type { CheckResult, Issue, StackInfo, VibeReport, WorkspaceInfo, WorkspacePackage };
package/dist/core.js ADDED
@@ -0,0 +1,163 @@
1
+ /** @vibecodeqa/cli/core — Programmatic scan API.
2
+ *
3
+ * Usage:
4
+ * import { scan } from "@vibecodeqa/cli/core";
5
+ * const report = await scan("./src");
6
+ * console.log(report.score, report.grade);
7
+ */
8
+ import { resolve } from "node:path";
9
+ import { readFileSync } from "node:fs";
10
+ import { CHECK_META, getCheckMeta } from "./check-meta.js";
11
+ import { getCheckIgnore, isCheckEnabled, loadConfig } from "./config.js";
12
+ import { detectRepoUrl, detectStack, detectWorkspace } from "./detect.js";
13
+ import { setGlobalIgnore, setGlobalSrcRoots } from "./fs-utils.js";
14
+ import { runAccessibility } from "./runners/accessibility.js";
15
+ import { runArchitecture } from "./runners/architecture.js";
16
+ import { runBestPractices } from "./runners/best-practices.js";
17
+ import { runCodeCoherence } from "./runners/code-coherence.js";
18
+ import { runCommentStaleness } from "./runners/comment-staleness.js";
19
+ import { runComplexity } from "./runners/complexity.js";
20
+ import { runDeadPatterns } from "./runners/dead-patterns.js";
21
+ import { runTestAudit } from "./runners/test-audit.js";
22
+ import { runConfusion } from "./runners/confusion.js";
23
+ import { runContext } from "./runners/context.js";
24
+ import { runDependencies } from "./runners/dependencies.js";
25
+ import { runDocCoherence } from "./runners/doc-coherence.js";
26
+ import { runDocs } from "./runners/docs.js";
27
+ import { runDuplication } from "./runners/duplication.js";
28
+ import { runErrorHandling } from "./runners/error-handling.js";
29
+ import { runLint } from "./runners/lint.js";
30
+ import { runPerformance } from "./runners/performance.js";
31
+ import { runReact } from "./runners/react.js";
32
+ import { runSecrets } from "./runners/secrets.js";
33
+ import { runSecurity } from "./runners/security.js";
34
+ import { runStandards } from "./runners/standards.js";
35
+ import { runStructure } from "./runners/structure.js";
36
+ import { runTesting } from "./runners/testing.js";
37
+ import { runTypeSafety } from "./runners/type-safety.js";
38
+ import { runTypeCheck } from "./runners/types-check.js";
39
+ import { computeScore } from "./score.js";
40
+ import { gradeFromScore } from "./types.js";
41
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
42
+ const VERSION = pkg.version;
43
+ /** Run a full code health scan. Returns a VibeReport with score, grade, and all check results. */
44
+ export async function scan(cwd, options = {}) {
45
+ const start = Date.now();
46
+ const resolvedCwd = resolve(cwd);
47
+ const config = options.config ?? loadConfig(resolvedCwd);
48
+ const workspace = detectWorkspace(resolvedCwd);
49
+ const stack = detectStack(resolvedCwd, workspace);
50
+ const isDart = stack.language === "dart";
51
+ setGlobalSrcRoots(workspace.isMonorepo ? workspace.srcRoots : undefined);
52
+ setGlobalIgnore(config.ignore);
53
+ const srcRoots = workspace.isMonorepo ? workspace.srcRoots : undefined;
54
+ const skipTests = options.skipTests ?? false;
55
+ const allRunners = [
56
+ { name: "structure", fn: () => runStructure(resolvedCwd, stack, workspace) },
57
+ { name: "lint", fn: () => runLint(resolvedCwd, stack, workspace) },
58
+ { name: "types", fn: () => runTypeCheck(resolvedCwd, isDart, workspace) },
59
+ { name: "type-safety", fn: () => runTypeSafety(resolvedCwd, isDart) },
60
+ { name: "standards", fn: () => runStandards(resolvedCwd, stack) },
61
+ { name: "complexity", fn: () => runComplexity(resolvedCwd) },
62
+ { name: "duplication", fn: () => runDuplication(resolvedCwd) },
63
+ { name: "error-handling", fn: () => runErrorHandling(resolvedCwd, stack) },
64
+ { name: "react", fn: () => runReact(resolvedCwd, stack) },
65
+ { name: "accessibility", fn: () => runAccessibility(resolvedCwd) },
66
+ { name: "docs", fn: () => runDocs(resolvedCwd) },
67
+ { name: "best-practices", fn: () => runBestPractices(resolvedCwd, workspace) },
68
+ { name: "testing", fn: () => runTesting(resolvedCwd, stack, skipTests, srcRoots) },
69
+ { name: "secrets", fn: () => runSecrets(resolvedCwd) },
70
+ { name: "security", fn: () => runSecurity(resolvedCwd) },
71
+ { name: "dependencies", fn: () => runDependencies(resolvedCwd, stack) },
72
+ { name: "architecture", fn: () => runArchitecture(resolvedCwd, workspace) },
73
+ { name: "performance", fn: () => runPerformance(resolvedCwd) },
74
+ { name: "confusion", fn: () => runConfusion(resolvedCwd) },
75
+ { name: "context", fn: () => runContext(resolvedCwd) },
76
+ { name: "doc-coherence", fn: () => runDocCoherence(resolvedCwd) },
77
+ { name: "code-coherence", fn: () => runCodeCoherence(resolvedCwd) },
78
+ { name: "comment-staleness", fn: () => runCommentStaleness(resolvedCwd) },
79
+ { name: "dead-patterns", fn: () => runDeadPatterns(resolvedCwd) },
80
+ { name: "test-audit", fn: () => runTestAudit(resolvedCwd) },
81
+ ];
82
+ // Filter checks if specified
83
+ const checkFilter = options.checks ? new Set(options.checks) : null;
84
+ const runners = checkFilter
85
+ ? allRunners.filter((r) => checkFilter.has(r.name))
86
+ : allRunners;
87
+ const checks = [];
88
+ const total = runners.length;
89
+ for (let i = 0; i < total; i++) {
90
+ const runner = runners[i];
91
+ if (!isCheckEnabled(config, runner.name)) {
92
+ const skipped = {
93
+ name: runner.name,
94
+ score: 0,
95
+ grade: "F",
96
+ details: { skipped: true, reason: "disabled in config" },
97
+ issues: [],
98
+ duration: 0,
99
+ };
100
+ checks.push(skipped);
101
+ options.onProgress?.(runner.name, skipped, i, total);
102
+ continue;
103
+ }
104
+ let result;
105
+ try {
106
+ const maybeResult = runner.fn();
107
+ result = maybeResult instanceof Promise ? await maybeResult : maybeResult;
108
+ }
109
+ catch (err) {
110
+ result = {
111
+ name: runner.name,
112
+ score: 0,
113
+ grade: "F",
114
+ details: { skipped: true, reason: `runner error: ${err instanceof Error ? err.message : "unknown"}` },
115
+ issues: [],
116
+ duration: 0,
117
+ };
118
+ }
119
+ // Apply per-check ignore patterns
120
+ const patterns = getCheckIgnore(config, result.name);
121
+ if (patterns?.length) {
122
+ result.issues = result.issues.filter((issue) => {
123
+ if (!issue.file || typeof issue.file !== "string")
124
+ return true;
125
+ const f = issue.file;
126
+ return !patterns.some((p) => {
127
+ if (p.endsWith("/**"))
128
+ return f.startsWith(p.slice(0, -3) + "/");
129
+ if (p.startsWith("*"))
130
+ return f.endsWith(p.slice(1));
131
+ return f.startsWith(p);
132
+ });
133
+ });
134
+ }
135
+ checks.push(result);
136
+ options.onProgress?.(runner.name, result, i, total);
137
+ }
138
+ const score = computeScore(checks);
139
+ const grade = gradeFromScore(score);
140
+ const { repoUrl, branch } = detectRepoUrl(resolvedCwd);
141
+ return {
142
+ version: VERSION,
143
+ timestamp: new Date().toISOString(),
144
+ score,
145
+ grade,
146
+ checks,
147
+ meta: {
148
+ cwd: resolvedCwd,
149
+ node: process.version,
150
+ duration: Date.now() - start,
151
+ stack,
152
+ workspace,
153
+ repoUrl,
154
+ branch,
155
+ },
156
+ };
157
+ }
158
+ // ── Re-exports ──
159
+ export { CHECK_META, getCheckMeta };
160
+ export { computeScore } from "./score.js";
161
+ export { loadConfig } from "./config.js";
162
+ export { detectStack, detectWorkspace } from "./detect.js";
163
+ export { gradeFromScore } from "./types.js";
package/dist/fs-utils.js CHANGED
@@ -168,7 +168,7 @@ function walk(dir, cwd, out, exts) {
168
168
  });
169
169
  }
170
170
  catch {
171
- continue; // broken symlink, deleted file, or permission denied
171
+ /* broken symlink, deleted file, or permission denied */
172
172
  }
173
173
  }
174
174
  }
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@vibecodeqa/cli",
3
- "version": "0.39.1",
3
+ "version": "0.41.0",
4
4
  "description": "Code health scanner for the AI coding era. 25 checks, zero config, full report.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "vcqa": "./dist/cli.js",
8
8
  "vibe-check": "./dist/cli.js"
9
9
  },
10
+ "exports": {
11
+ ".": "./dist/cli.js",
12
+ "./core": "./dist/core.js"
13
+ },
10
14
  "files": [
11
15
  "dist",
12
16
  "LICENSE",