@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,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
+ }
@@ -0,0 +1,3 @@
1
+ /** Environment validation — checks .env hygiene, .env.example drift, and unsafe patterns. */
2
+ import type { CheckResult } from "../types.js";
3
+ export declare function runEnvValidation(cwd: string): CheckResult;
@@ -0,0 +1,122 @@
1
+ /** Environment validation — checks .env hygiene, .env.example drift, and unsafe patterns. */
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { gradeFromScore } from "../types.js";
5
+ export function runEnvValidation(cwd) {
6
+ const start = Date.now();
7
+ const issues = [];
8
+ const envFiles = readdirSync(cwd).filter((f) => f.startsWith(".env"));
9
+ const hasEnv = envFiles.some((f) => f === ".env" || f === ".env.local");
10
+ const hasExample = envFiles.some((f) => f === ".env.example" || f === ".env.template");
11
+ // Check .gitignore includes .env
12
+ if (hasEnv) {
13
+ const gitignore = existsSync(join(cwd, ".gitignore")) ? readFileSync(join(cwd, ".gitignore"), "utf-8") : "";
14
+ if (!gitignore.includes(".env")) {
15
+ issues.push({ severity: "error", message: ".env not in .gitignore — secrets may be committed", file: ".gitignore", rule: "env-not-ignored" });
16
+ }
17
+ }
18
+ // Check .env.example exists when .env does
19
+ if (hasEnv && !hasExample) {
20
+ issues.push({ severity: "warning", message: "No .env.example — other developers won't know which vars are needed", rule: "no-env-example" });
21
+ }
22
+ // Check .env.example drift — vars in .env.example should match .env
23
+ if (hasEnv && hasExample) {
24
+ const exampleFile = envFiles.find((f) => f === ".env.example" || f === ".env.template");
25
+ const envVars = parseEnvKeys(readFileSync(join(cwd, ".env"), "utf-8"));
26
+ const exampleVars = parseEnvKeys(readFileSync(join(cwd, exampleFile), "utf-8"));
27
+ for (const key of exampleVars) {
28
+ if (!envVars.has(key)) {
29
+ issues.push({ severity: "info", message: `${exampleFile} has ${key} but .env doesn't — may be missing`, file: exampleFile, rule: "env-example-drift" });
30
+ }
31
+ }
32
+ for (const key of envVars) {
33
+ if (!exampleVars.has(key)) {
34
+ issues.push({ severity: "warning", message: `${key} in .env but not in ${exampleFile} — won't be documented for other developers`, file: exampleFile, rule: "env-example-drift" });
35
+ }
36
+ }
37
+ }
38
+ // Scan .env files for unsafe patterns
39
+ for (const f of envFiles) {
40
+ if (f === ".env.example" || f === ".env.template")
41
+ continue;
42
+ const content = readFileSync(join(cwd, f), "utf-8");
43
+ const lines = content.split("\n");
44
+ for (let i = 0; i < lines.length; i++) {
45
+ const line = lines[i].trim();
46
+ if (!line || line.startsWith("#"))
47
+ continue;
48
+ // Check for values that look like they should be secret but have defaults
49
+ if (/^(DATABASE_URL|DB_PASSWORD|SECRET_KEY|JWT_SECRET|API_KEY|PRIVATE_KEY)=/i.test(line)) {
50
+ const value = line.split("=").slice(1).join("=").trim().replace(/^["']|["']$/g, "");
51
+ if (value && !value.startsWith("$") && !value.includes("${") && value.length < 20 && !/^(changeme|replace|todo|xxx|your[-_])/i.test(value)) {
52
+ issues.push({
53
+ severity: "warning",
54
+ message: `${line.split("=")[0]} appears to have a hardcoded value — use a placeholder in committed files`,
55
+ file: f,
56
+ line: i + 1,
57
+ rule: "env-hardcoded-secret",
58
+ });
59
+ }
60
+ }
61
+ // Check for empty required-looking vars
62
+ if (/^[A-Z_]+=\s*$/.test(line)) {
63
+ const key = line.split("=")[0];
64
+ if (/KEY|SECRET|TOKEN|PASSWORD|URL/i.test(key)) {
65
+ issues.push({
66
+ severity: "info",
67
+ message: `${key} is empty — may cause runtime errors`,
68
+ file: f,
69
+ line: i + 1,
70
+ rule: "env-empty-var",
71
+ });
72
+ }
73
+ }
74
+ }
75
+ }
76
+ // Check for env vars used in code but not in .env.example
77
+ if (hasExample) {
78
+ const exampleFile = envFiles.find((f) => f === ".env.example" || f === ".env.template");
79
+ const exampleVars = parseEnvKeys(readFileSync(join(cwd, exampleFile), "utf-8"));
80
+ // Quick scan of package.json for referenced env vars
81
+ if (existsSync(join(cwd, "package.json"))) {
82
+ try {
83
+ const pkg = readFileSync(join(cwd, "package.json"), "utf-8");
84
+ const envRefs = pkg.match(/process\.env\.([A-Z_]+)/g) || [];
85
+ for (const ref of new Set(envRefs)) {
86
+ const varName = ref.replace("process.env.", "");
87
+ if (!exampleVars.has(varName) && !["NODE_ENV", "CI", "HOME", "PATH", "PWD"].includes(varName)) {
88
+ issues.push({
89
+ severity: "info",
90
+ message: `${varName} used in code but not in ${exampleFile}`,
91
+ rule: "env-undocumented",
92
+ });
93
+ }
94
+ }
95
+ }
96
+ catch { /* ignore */ }
97
+ }
98
+ }
99
+ const errorCount = issues.filter((i) => i.severity === "error").length;
100
+ const warnCount = issues.filter((i) => i.severity === "warning").length;
101
+ const score = Math.max(0, 100 - errorCount * 25 - warnCount * 10);
102
+ return {
103
+ name: "env-validation",
104
+ score,
105
+ grade: gradeFromScore(score),
106
+ details: { envFiles, hasExample },
107
+ issues,
108
+ duration: Date.now() - start,
109
+ };
110
+ }
111
+ function parseEnvKeys(content) {
112
+ const keys = new Set();
113
+ for (const line of content.split("\n")) {
114
+ const trimmed = line.trim();
115
+ if (!trimmed || trimmed.startsWith("#"))
116
+ continue;
117
+ const eq = trimmed.indexOf("=");
118
+ if (eq > 0)
119
+ keys.add(trimmed.slice(0, eq).trim());
120
+ }
121
+ return keys;
122
+ }
@@ -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 string-only lines and metadata definitions
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>;
@@ -0,0 +1,177 @@
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 { createHash } from "node:crypto";
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { getProductionFiles } from "../fs-utils.js";
20
+ import { gradeFromScore } from "../types.js";
21
+ const CONCERN_PATTERNS = [
22
+ { name: "HTTP/routing", patterns: [/\b(app|router|server)\.(get|post|put|delete|patch|use)\b/, /\bRequest\b.*\bResponse\b/, /\breq\s*,\s*res\b/, /\bfastify\b|\bhono\b|\bexpress\b/] },
23
+ { name: "database", patterns: [/\b(prisma|sequelize|typeorm|knex|drizzle)\b/, /\bSELECT\b.*\bFROM\b/i, /\.query\s*\(/, /\bcreateClient\b.*\bsupabase\b/i] },
24
+ { name: "auth", patterns: [/\b(jwt|token|session|cookie|passport|oauth|login|signup|signIn|signUp)\b/i, /\bverify(Token|Session|Auth)\b/, /\bbcrypt|argon2|scrypt\b/] },
25
+ { name: "email", patterns: [/\bsendMail|sendEmail|transporter|nodemailer|resend|postmark\b/i, /\bsmtp\b/i] },
26
+ { name: "file I/O", patterns: [/\breadFileSync|writeFileSync|createReadStream|createWriteStream\b/, /\bfs\.(read|write|mkdir|unlink|stat)\b/] },
27
+ { name: "validation", patterns: [/\bz\.(string|number|object|array)\b/, /\bJoi\.(string|number|object)\b/, /\byup\.(string|number|object)\b/, /\bvalidate\w*Schema\b/] },
28
+ { name: "UI rendering", patterns: [/\bJSX\b|\breturn\s*\(?\s*</, /\buseState|useEffect|useRef|useMemo\b/, /\brender\(\)/, /\bcomponent\b/i] },
29
+ { name: "state management", patterns: [/\bcreateStore|useStore|createSlice|createReducer\b/, /\bdispatch\(|getState\(\)/, /\buseSelector|useDispatch\b/] },
30
+ { name: "testing", patterns: [/\bdescribe\s*\(|it\s*\(|test\s*\(|expect\s*\(/, /\bbeforeEach|afterEach|beforeAll\b/, /\bjest\.|vitest\./] },
31
+ { name: "CLI", patterns: [/\bprocess\.argv\b/, /\bcommander|yargs|meow|cac\b/, /\bparse(Args|Options)\b/] },
32
+ ];
33
+ export async function runFileCohesion(cwd) {
34
+ const start = Date.now();
35
+ const proKey = process.env.VCQA_PRO_KEY || "";
36
+ if (!proKey) {
37
+ return {
38
+ name: "file-cohesion",
39
+ score: 0,
40
+ grade: "F",
41
+ details: {
42
+ premium: true,
43
+ comingSoon: true,
44
+ reason: "Set VCQA_PRO_KEY to enable file cohesion analysis",
45
+ description: "Detects files with multiple responsibilities — the #1 code smell in AI-generated code. Labels each file's concern clusters and suggests concrete split points.",
46
+ },
47
+ issues: [],
48
+ duration: Date.now() - start,
49
+ };
50
+ }
51
+ const files = getProductionFiles(cwd);
52
+ const issues = [];
53
+ const cache = loadCache(cwd);
54
+ let cacheHits = 0;
55
+ // Phase 1: local heuristics — detect multi-concern files
56
+ const candidates = [];
57
+ for (const f of files) {
58
+ if (f.isTest)
59
+ continue;
60
+ const lines = f.content.split("\n").length;
61
+ if (lines < 100)
62
+ continue; // small files rarely have cohesion problems
63
+ const detectedConcerns = [];
64
+ for (const concern of CONCERN_PATTERNS) {
65
+ if (concern.patterns.some((p) => p.test(f.content))) {
66
+ detectedConcerns.push(concern.name);
67
+ }
68
+ }
69
+ if (detectedConcerns.length >= 2) {
70
+ candidates.push({ path: f.path, content: f.content, concerns: detectedConcerns, lines });
71
+ // Local issue for 2+ concerns
72
+ issues.push({
73
+ severity: detectedConcerns.length >= 3 ? "error" : "warning",
74
+ message: `${detectedConcerns.length} concerns detected: ${detectedConcerns.join(", ")} — consider splitting`,
75
+ file: f.path,
76
+ rule: "multi-concern",
77
+ });
78
+ }
79
+ }
80
+ // Phase 2: LLM analysis for top candidates (by line count, most likely to benefit from splitting)
81
+ const topCandidates = candidates
82
+ .sort((a, b) => b.lines - a.lines)
83
+ .slice(0, 5);
84
+ for (const candidate of topCandidates) {
85
+ const hash = createHash("sha256").update(candidate.content).digest("hex").slice(0, 16);
86
+ // Check cache
87
+ const cached = cache.files[candidate.path];
88
+ if (cached && cached.hash === hash) {
89
+ cacheHits++;
90
+ if (cached.suggestion) {
91
+ issues.push({
92
+ severity: "warning",
93
+ message: cached.suggestion,
94
+ file: candidate.path,
95
+ rule: "split-suggestion",
96
+ });
97
+ }
98
+ continue;
99
+ }
100
+ // Call LLM for concrete split suggestions
101
+ const analysis = await analyzeCohesion(candidate.path, candidate.content, proKey);
102
+ if (analysis) {
103
+ cache.files[candidate.path] = { hash, concerns: analysis.concerns, suggestion: analysis.suggestion };
104
+ if (analysis.suggestion) {
105
+ issues.push({
106
+ severity: "warning",
107
+ message: analysis.suggestion,
108
+ file: candidate.path,
109
+ rule: "split-suggestion",
110
+ });
111
+ }
112
+ }
113
+ }
114
+ saveCache(cwd, cache);
115
+ const totalFiles = files.filter((f) => !f.isTest).length;
116
+ const multiConcernFiles = candidates.length;
117
+ const ratio = totalFiles > 0 ? multiConcernFiles / totalFiles : 0;
118
+ const score = Math.round(Math.max(0, 100 - ratio * 300));
119
+ return {
120
+ name: "file-cohesion",
121
+ score,
122
+ grade: gradeFromScore(score),
123
+ details: {
124
+ premium: true,
125
+ totalFiles,
126
+ multiConcernFiles,
127
+ cacheHits,
128
+ candidates: candidates.map((c) => ({ path: c.path, concerns: c.concerns, lines: c.lines })),
129
+ },
130
+ issues,
131
+ duration: Date.now() - start,
132
+ };
133
+ }
134
+ async function analyzeCohesion(filePath, content, proKey) {
135
+ const truncated = content.slice(0, 4000);
136
+ try {
137
+ const res = await fetch("https://api.vibecodeqa.online/api/pro/file-cohesion", {
138
+ method: "POST",
139
+ headers: {
140
+ "Content-Type": "application/json",
141
+ Authorization: `Bearer ${proKey}`,
142
+ },
143
+ body: JSON.stringify({ file: filePath, content: truncated }),
144
+ });
145
+ if (!res.ok)
146
+ return null;
147
+ const data = (await res.json());
148
+ return {
149
+ concerns: data.concerns || [],
150
+ suggestion: data.suggestion || "",
151
+ };
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ function loadCache(cwd) {
158
+ try {
159
+ const cachePath = join(cwd, ".vibe-check", "file-cohesion-cache.json");
160
+ if (existsSync(cachePath)) {
161
+ const data = JSON.parse(readFileSync(cachePath, "utf-8"));
162
+ if (data.version === 1)
163
+ return data;
164
+ }
165
+ }
166
+ catch { /* corrupt cache */ }
167
+ return { version: 1, files: {} };
168
+ }
169
+ function saveCache(cwd, cache) {
170
+ try {
171
+ const dir = join(cwd, ".vibe-check");
172
+ if (!existsSync(dir))
173
+ mkdirSync(dir, { recursive: true });
174
+ writeFileSync(join(dir, "file-cohesion-cache.json"), JSON.stringify(cache));
175
+ }
176
+ catch { /* write failed */ }
177
+ }
@@ -0,0 +1,14 @@
1
+ /** Frontend health — detects HTML/framework antipatterns in component code.
2
+ *
3
+ * Checks:
4
+ * 1. Conflicting UI frameworks (MUI + Tailwind, Chakra + Tailwind, etc.)
5
+ * 2. Large/unoptimized images (no width/height, no next/image, large src)
6
+ * 3. Inconsistent icon systems (mixing lucide + heroicons + fontawesome)
7
+ * 4. Bundle-heavy imports (entire libraries instead of specific components)
8
+ * 5. Missing loading states (async data without suspense/skeleton)
9
+ * 6. Hardcoded strings (potential i18n issues)
10
+ * 7. Missing meta tags / head management
11
+ * 8. DOM nesting violations (div in p, button in a)
12
+ */
13
+ import type { CheckResult } from "../types.js";
14
+ export declare function runFrontendHealth(cwd: string): CheckResult;