@vibecodeqa/cli 0.42.0 → 0.44.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 (47) hide show
  1. package/README.md +130 -165
  2. package/dist/check-meta.js +59 -6
  3. package/dist/cli.js +299 -762
  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 +157 -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.d.ts +1 -0
  13. package/dist/core.js +12 -1
  14. package/dist/delta.d.ts +45 -0
  15. package/dist/delta.js +158 -0
  16. package/dist/detect.js +2 -2
  17. package/dist/pr-comment.d.ts +1 -1
  18. package/dist/pr-comment.js +23 -4
  19. package/dist/report/html.d.ts +1 -1
  20. package/dist/report/html.js +7 -2
  21. package/dist/report/pages.d.ts +2 -0
  22. package/dist/report/pages.js +167 -0
  23. package/dist/report/styles.d.ts +1 -1
  24. package/dist/report/styles.js +37 -0
  25. package/dist/runners/accessibility.js +4 -1
  26. package/dist/runners/best-practices.js +1 -1
  27. package/dist/runners/confusion.js +28 -17
  28. package/dist/runners/design-consistency.d.ts +12 -0
  29. package/dist/runners/design-consistency.js +125 -0
  30. package/dist/runners/error-handling.js +18 -2
  31. package/dist/runners/file-cohesion.d.ts +17 -0
  32. package/dist/runners/file-cohesion.js +177 -0
  33. package/dist/runners/frontend-health.d.ts +14 -0
  34. package/dist/runners/frontend-health.js +206 -0
  35. package/dist/runners/html-quality.d.ts +8 -0
  36. package/dist/runners/html-quality.js +203 -0
  37. package/dist/runners/lint.js +6 -1
  38. package/dist/runners/react.js +1 -0
  39. package/dist/runners/secrets.js +7 -2
  40. package/dist/runners/security.js +7 -1
  41. package/dist/runners/standards.d.ts +2 -2
  42. package/dist/runners/standards.js +45 -12
  43. package/dist/runners/structure.js +1 -1
  44. package/dist/runners/styling.d.ts +15 -0
  45. package/dist/runners/styling.js +280 -0
  46. package/dist/runners/testing.js +3 -1
  47. package/package.json +2 -2
@@ -0,0 +1,203 @@
1
+ /** HTML quality — checks static HTML sites for meta tags, images, links, a11y, performance, SEO, security.
2
+ *
3
+ * Activates when the project has .html files. Works alongside framework checks —
4
+ * catches issues in static sites, landing pages, and docs that framework-specific
5
+ * checks miss entirely.
6
+ */
7
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
8
+ import { join, relative } from "node:path";
9
+ import { gradeFromScore } from "../types.js";
10
+ const SKIP_DIRS = new Set(["node_modules", ".git", ".vibe-check", "dist", "build", "coverage", ".next", ".nuxt"]);
11
+ export function runHtmlQuality(cwd) {
12
+ const start = Date.now();
13
+ const issues = [];
14
+ const htmlFiles = collectHtmlFiles(cwd);
15
+ if (htmlFiles.length === 0) {
16
+ return {
17
+ name: "html-quality",
18
+ score: 0,
19
+ grade: "F",
20
+ details: { skipped: true, reason: "no HTML files found" },
21
+ issues: [],
22
+ duration: Date.now() - start,
23
+ };
24
+ }
25
+ const allLinks = new Set();
26
+ const titles = new Map();
27
+ for (const file of htmlFiles) {
28
+ const relPath = relative(cwd, file);
29
+ let content;
30
+ try {
31
+ content = readFileSync(file, "utf-8");
32
+ }
33
+ catch {
34
+ continue;
35
+ }
36
+ // ── Meta tags ──
37
+ if (content.includes("<head")) {
38
+ if (!/<title[^>]*>/.test(content)) {
39
+ issues.push({ severity: "error", message: "Missing <title> tag", file: relPath, rule: "missing-title" });
40
+ }
41
+ else {
42
+ const titleMatch = content.match(/<title[^>]*>([^<]*)<\/title>/i);
43
+ if (titleMatch) {
44
+ const title = titleMatch[1].trim();
45
+ if (title.length < 10) {
46
+ issues.push({ severity: "warning", message: `Title too short: "${title}" — aim for 30-60 characters`, file: relPath, rule: "short-title" });
47
+ }
48
+ const existing = titles.get(title) || [];
49
+ existing.push(relPath);
50
+ titles.set(title, existing);
51
+ }
52
+ }
53
+ if (!/<meta\s[^>]*name=["']description["']/i.test(content)) {
54
+ issues.push({ severity: "warning", message: "Missing meta description", file: relPath, rule: "missing-description" });
55
+ }
56
+ if (!/<meta\s[^>]*name=["']viewport["']/i.test(content)) {
57
+ issues.push({ severity: "error", message: "Missing viewport meta — page won't be mobile-responsive", file: relPath, rule: "missing-viewport" });
58
+ }
59
+ if (!/<meta\s[^>]*charset/i.test(content)) {
60
+ issues.push({ severity: "warning", message: "Missing charset declaration", file: relPath, rule: "missing-charset" });
61
+ }
62
+ if (!/<meta\s[^>]*property=["']og:title["']/i.test(content)) {
63
+ issues.push({ severity: "info", message: "Missing Open Graph tags (og:title, og:description) — social sharing will look plain", file: relPath, rule: "missing-og" });
64
+ }
65
+ if (!/<link\s[^>]*rel=["']canonical["']/i.test(content)) {
66
+ issues.push({ severity: "info", message: "Missing canonical link — may cause duplicate content issues", file: relPath, rule: "missing-canonical" });
67
+ }
68
+ if (!/<link\s[^>]*rel=["']icon["']/i.test(content) && !/<link\s[^>]*rel=["']shortcut icon["']/i.test(content)) {
69
+ issues.push({ severity: "info", message: "Missing favicon", file: relPath, rule: "missing-favicon" });
70
+ }
71
+ }
72
+ // ── HTML lang ──
73
+ if (/<html[\s>]/.test(content) && !/<html\s[^>]*lang=/i.test(content)) {
74
+ issues.push({ severity: "warning", message: "Missing lang attribute on <html> — screen readers need this", file: relPath, rule: "missing-lang" });
75
+ }
76
+ // ── Images ──
77
+ const imgRegex = /<img\s[^>]*>/gi;
78
+ let imgMatch;
79
+ while ((imgMatch = imgRegex.exec(content)) !== null) {
80
+ const tag = imgMatch[0];
81
+ const line = content.slice(0, imgMatch.index).split("\n").length;
82
+ if (!/alt\s*=/i.test(tag)) {
83
+ issues.push({ severity: "error", message: "Image missing alt attribute", file: relPath, line, rule: "img-no-alt" });
84
+ }
85
+ if (!/(?:width|height)\s*=/i.test(tag)) {
86
+ issues.push({ severity: "warning", message: "Image missing width/height — causes layout shift", file: relPath, line, rule: "img-no-dimensions" });
87
+ }
88
+ if (!/loading\s*=/i.test(tag)) {
89
+ issues.push({ severity: "info", message: "Image missing loading=\"lazy\" — add for below-fold images", file: relPath, line, rule: "img-no-lazy" });
90
+ }
91
+ }
92
+ // ── Links ──
93
+ const linkRegex = /<a\s[^>]*href=["']([^"']+)["'][^>]*>/gi;
94
+ let linkMatch;
95
+ while ((linkMatch = linkRegex.exec(content)) !== null) {
96
+ const href = linkMatch[1];
97
+ const tag = linkMatch[0];
98
+ const line = content.slice(0, linkMatch.index).split("\n").length;
99
+ // HTTP links on what should be HTTPS
100
+ if (href.startsWith("http://") && !href.includes("localhost")) {
101
+ issues.push({ severity: "warning", message: `HTTP link: ${href} — use HTTPS`, file: relPath, line, rule: "http-link" });
102
+ }
103
+ // External links without rel=noopener
104
+ if (href.startsWith("http") && /target\s*=\s*["']_blank["']/i.test(tag) && !/rel\s*=\s*["'][^"']*noopener/i.test(tag)) {
105
+ issues.push({ severity: "warning", message: "External link with target=\"_blank\" missing rel=\"noopener\"", file: relPath, line, rule: "missing-noopener" });
106
+ }
107
+ // Collect internal links for broken link check
108
+ if (!href.startsWith("http") && !href.startsWith("mailto:") && !href.startsWith("#") && !href.startsWith("javascript:")) {
109
+ allLinks.add(join(cwd, href.split("#")[0].split("?")[0]));
110
+ }
111
+ }
112
+ // ── Heading hierarchy ──
113
+ const headings = [];
114
+ const headingRegex = /<h(\d)/gi;
115
+ let hMatch;
116
+ while ((hMatch = headingRegex.exec(content)) !== null) {
117
+ headings.push(parseInt(hMatch[1], 10));
118
+ }
119
+ for (let i = 1; i < headings.length; i++) {
120
+ if (headings[i] > headings[i - 1] + 1) {
121
+ issues.push({
122
+ severity: "warning",
123
+ message: `Heading hierarchy skip: h${headings[i - 1]} → h${headings[i]} (should be h${headings[i - 1] + 1})`,
124
+ file: relPath,
125
+ rule: "heading-skip",
126
+ });
127
+ break;
128
+ }
129
+ }
130
+ // ── Performance ──
131
+ // Render-blocking scripts (script in head without async/defer)
132
+ const headContent = content.match(/<head[^>]*>([\s\S]*?)<\/head>/i)?.[1] || "";
133
+ const scriptInHead = /<script\s[^>]*src=["'][^"']+["'][^>]*>/gi;
134
+ let scriptMatch;
135
+ while ((scriptMatch = scriptInHead.exec(headContent)) !== null) {
136
+ const tag = scriptMatch[0];
137
+ if (!/\b(?:async|defer|type=["']module["'])\b/i.test(tag)) {
138
+ issues.push({ severity: "warning", message: "Render-blocking script in <head> — add async or defer", file: relPath, rule: "render-blocking" });
139
+ }
140
+ }
141
+ // ── Security ──
142
+ // Mixed content (http:// resources on a page)
143
+ if (/<(?:img|script|link|iframe)\s[^>]*(?:src|href)=["']http:\/\/(?!localhost)/i.test(content)) {
144
+ issues.push({ severity: "warning", message: "Mixed content: HTTP resource on page — use HTTPS", file: relPath, rule: "mixed-content" });
145
+ }
146
+ }
147
+ // ── Cross-file checks ──
148
+ // Broken internal links
149
+ for (const link of allLinks) {
150
+ if (!existsSync(link)) {
151
+ const relLink = relative(cwd, link);
152
+ issues.push({ severity: "warning", message: `Broken internal link: ${relLink}`, rule: "broken-link" });
153
+ }
154
+ }
155
+ // Duplicate titles
156
+ for (const [title, files] of titles) {
157
+ if (files.length > 1) {
158
+ issues.push({ severity: "warning", message: `Duplicate title "${title}" in ${files.length} files — each page should have a unique title`, rule: "duplicate-title" });
159
+ }
160
+ }
161
+ // SEO files
162
+ if (!existsSync(join(cwd, "robots.txt"))) {
163
+ issues.push({ severity: "info", message: "Missing robots.txt", rule: "missing-robots" });
164
+ }
165
+ if (!existsSync(join(cwd, "sitemap.xml"))) {
166
+ issues.push({ severity: "info", message: "Missing sitemap.xml", rule: "missing-sitemap" });
167
+ }
168
+ // Score
169
+ const errorCount = issues.filter((i) => i.severity === "error").length;
170
+ const warnCount = issues.filter((i) => i.severity === "warning").length;
171
+ const score = Math.max(0, 100 - errorCount * 15 - warnCount * 5);
172
+ return {
173
+ name: "html-quality",
174
+ score,
175
+ grade: gradeFromScore(score),
176
+ details: { htmlFiles: htmlFiles.length },
177
+ issues,
178
+ duration: Date.now() - start,
179
+ };
180
+ }
181
+ function collectHtmlFiles(cwd, subdir = "") {
182
+ const files = [];
183
+ const dir = subdir ? join(cwd, subdir) : cwd;
184
+ try {
185
+ for (const entry of readdirSync(dir)) {
186
+ if (SKIP_DIRS.has(entry))
187
+ continue;
188
+ const full = join(dir, entry);
189
+ try {
190
+ const stat = statSync(full);
191
+ if (stat.isDirectory()) {
192
+ files.push(...collectHtmlFiles(cwd, subdir ? join(subdir, entry) : entry));
193
+ }
194
+ else if (entry.endsWith(".html") || entry.endsWith(".htm")) {
195
+ files.push(full);
196
+ }
197
+ }
198
+ catch { /* skip */ }
199
+ }
200
+ }
201
+ catch { /* skip */ }
202
+ return files;
203
+ }
@@ -13,7 +13,9 @@ export function runLint(cwd, stack, workspace) {
13
13
  // Determine the target path for linting
14
14
  // Monorepos with root config: lint "." (biome/eslint will find all files)
15
15
  // Single-package: lint "src/"
16
- const lintTarget = workspace?.isMonorepo ? "." : "src/";
16
+ const lintTarget = workspace?.isMonorepo ? "."
17
+ : existsSync(join(cwd, "src")) ? "src/"
18
+ : ".";
17
19
  if (stack.linter === "biome") {
18
20
  const { stdout } = run(`npx biome check ${lintTarget} --reporter=json 2>/dev/null || true`, cwd);
19
21
  try {
@@ -23,6 +25,9 @@ export function runLint(cwd, stack, workspace) {
23
25
  // biome path can be string or {file: "..."} depending on version
24
26
  const rawPath = d.location?.path;
25
27
  const file = typeof rawPath === "string" ? rawPath : rawPath?.file || undefined;
28
+ // Skip generated output files
29
+ if (file && (file.includes(".vibe-check/") || file.includes("node_modules/")))
30
+ continue;
26
31
  issues.push({
27
32
  severity: d.severity === "error" ? "error" : d.severity === "warning" ? "warning" : "info",
28
33
  message: d.description || d.message || "lint issue",
@@ -201,6 +201,7 @@ export function runReact(cwd, stack) {
201
201
  inlineHandlers,
202
202
  effectNoDeps,
203
203
  domManipulation,
204
+ suggestion: !hasHooksPlugin ? "Install eslint-plugin-react-hooks for deeper React analysis: pnpm add -D eslint-plugin-react-hooks" : undefined,
204
205
  },
205
206
  issues,
206
207
  duration: Date.now() - start,
@@ -122,8 +122,9 @@ export async function runSecrets(cwd) {
122
122
  // Try gitleaks first (industry standard, 800+ patterns)
123
123
  const gitleaksResult = tryGitleaks(cwd, issues);
124
124
  const tool = gitleaksResult ? "gitleaks" : "secretlint";
125
- if (!gitleaksResult)
125
+ if (!gitleaksResult) {
126
126
  issues.push(...(await scanFallback(cwd)));
127
+ }
127
128
  // ── .env file audit ──
128
129
  const envFiles = [".env", ".env.local", ".env.production", ".env.development"];
129
130
  const gitignore = existsSync(join(cwd, ".gitignore")) ? readFileSync(join(cwd, ".gitignore"), "utf-8") : "";
@@ -185,7 +186,11 @@ export async function runSecrets(cwd) {
185
186
  name: "secrets",
186
187
  score,
187
188
  grade: gradeFromScore(score),
188
- details: { secretsFound: issues.length, tool },
189
+ details: {
190
+ secretsFound: issues.length,
191
+ tool,
192
+ suggestion: !gitleaksResult ? "Install gitleaks for deeper secret detection (800+ patterns): brew install gitleaks" : undefined,
193
+ },
189
194
  issues,
190
195
  duration: Date.now() - start,
191
196
  };
@@ -301,11 +301,17 @@ export function runSecurity(cwd) {
301
301
  if (trimmed.startsWith("//") || trimmed.startsWith("*"))
302
302
  continue;
303
303
  // Skip pattern/config definition lines and string-heavy metadata (prevents false positives on own code)
304
- if (/\bpattern\s*:|name:\s*["']|message:\s*["']|description:\s*["']|risk:\s*["']|recommendation:\s*["']/.test(trimmed))
304
+ if (/\bpattern\s*:|name:\s*["']|message:\s*["'`]|description:\s*["']|risk:\s*["']|recommendation:\s*["']|rule:\s*["']|severity:\s*["']/.test(trimmed))
305
305
  continue;
306
306
  // Skip lines that are primarily string content (check-meta descriptions, etc.)
307
307
  if (/^\s*["'`].*["'`][,;]?\s*$/.test(line))
308
308
  continue;
309
+ // Skip return statements returning string literals (e.g., fix suggestions mentioning patterns)
310
+ if (/\breturn\s+["'`]/.test(trimmed))
311
+ continue;
312
+ // Skip rule-matching conditionals (e.g., if (rule === "secret-detected") ...)
313
+ if (/\brule\s*===?\s*["']/.test(trimmed) || /\bcheck\s*===?\s*["']/.test(trimmed))
314
+ continue;
309
315
  for (const p of PATTERNS) {
310
316
  if (p.pattern.test(line)) {
311
317
  issues.push({
@@ -1,3 +1,3 @@
1
1
  /** Code standards check — naming conventions, anti-patterns, config hygiene. */
2
- import type { CheckResult, StackInfo } from "../types.js";
3
- export declare function runStandards(cwd: string, stack: StackInfo): CheckResult;
2
+ import type { CheckResult, StackInfo, WorkspaceInfo } from "../types.js";
3
+ export declare function runStandards(cwd: string, stack: StackInfo, workspace?: WorkspaceInfo): CheckResult;
@@ -1,5 +1,5 @@
1
1
  /** Code standards check — naming conventions, anti-patterns, config hygiene. */
2
- import { readFileSync } from "node:fs";
2
+ import { existsSync, readFileSync } from "node:fs";
3
3
  import { basename, extname, join } from "node:path";
4
4
  import { getProductionFiles, readDeps } from "../fs-utils.js";
5
5
  import { gradeFromScore } from "../types.js";
@@ -42,22 +42,39 @@ const CODE_SMELLS = [
42
42
  message: "Large magic number — consider a named constant",
43
43
  },
44
44
  ];
45
- export function runStandards(cwd, stack) {
45
+ export function runStandards(cwd, stack, workspace) {
46
46
  const start = Date.now();
47
47
  const issues = [];
48
+ // Detect CLI projects — console.log is intentional in CLI tools
49
+ let isCLI = false;
50
+ try {
51
+ const pkgPath = join(cwd, "package.json");
52
+ if (existsSync(pkgPath)) {
53
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
54
+ isCLI = !!pkg.bin;
55
+ }
56
+ }
57
+ catch { /* ignore */ }
48
58
  // Collect source files
49
59
  const files = getProductionFiles(cwd);
50
60
  // ── File naming conventions ──
51
61
  let namingViolations = 0;
62
+ // Detect dominant convention for tsx/jsx files before flagging
63
+ const tsxFiles = files.filter((f) => {
64
+ const ext = extname(basename(f.path));
65
+ return ext === ".tsx" || ext === ".jsx";
66
+ });
67
+ const pascalCount = tsxFiles.filter((f) => /^[A-Z]/.test(basename(f.path).replace(extname(basename(f.path)), ""))).length;
68
+ const usesKebabConvention = tsxFiles.length >= 3 && pascalCount < tsxFiles.length / 2;
52
69
  for (const f of files) {
53
70
  const name = basename(f.path);
54
71
  const ext = extname(name);
55
72
  const base = name.replace(ext, "");
56
- // React components should be PascalCase
73
+ // React components should be PascalCase — but only flag if PascalCase is the project convention
57
74
  if ((ext === ".tsx" || ext === ".jsx") && /^[A-Z]/.test(base)) {
58
75
  // PascalCase component file — correct
59
76
  }
60
- else if ((ext === ".tsx" || ext === ".jsx") && /^[a-z]/.test(base) && base !== "main" && base !== "index") {
77
+ else if (!usesKebabConvention && (ext === ".tsx" || ext === ".jsx") && /^[a-z]/.test(base) && base !== "main" && base !== "index") {
61
78
  // lowercase tsx file that's not main/index — check if it exports a component
62
79
  if (/export (default )?(function|const) [A-Z]/.test(f.content)) {
63
80
  namingViolations++;
@@ -78,16 +95,25 @@ export function runStandards(cwd, stack) {
78
95
  }
79
96
  }
80
97
  }
81
- // ── Large files ──
98
+ // ── Large files (exponential penalty) ──
99
+ // Penalty grows with the square of excess lines. A 300-line file is a nudge.
100
+ // A 600-line file is a wall. A 1000-line file tanks the check.
82
101
  let largeFiles = 0;
102
+ let fileSizePenalty = 0;
103
+ const SOFT_LIMIT = 300;
83
104
  for (const f of files) {
84
105
  const lines = f.content.split("\n").length;
85
- if (lines > 300) {
106
+ if (lines > SOFT_LIMIT * 2) {
86
107
  largeFiles++;
87
- issues.push({ severity: "warning", message: `${lines} lines — consider splitting (max 300)`, file: f.path, rule: "large-file" });
108
+ const excess = lines - SOFT_LIMIT;
109
+ fileSizePenalty += (excess / 100) ** 2;
110
+ issues.push({ severity: "error", message: `${lines} lines — split this file (exponential penalty above ${SOFT_LIMIT})`, file: f.path, rule: "large-file" });
88
111
  }
89
- else if (lines > 200) {
90
- issues.push({ severity: "warning", message: `${lines} lines — getting large`, file: f.path, rule: "large-file" });
112
+ else if (lines > SOFT_LIMIT) {
113
+ largeFiles++;
114
+ const excess = lines - SOFT_LIMIT;
115
+ fileSizePenalty += (excess / 100) ** 2;
116
+ issues.push({ severity: "warning", message: `${lines} lines — consider splitting (penalty grows exponentially above ${SOFT_LIMIT})`, file: f.path, rule: "large-file" });
91
117
  }
92
118
  }
93
119
  // ── Code smell patterns ──
@@ -105,8 +131,8 @@ export function runStandards(cwd, stack) {
105
131
  if (/^\s*["'`].*["'`][,;]?\s*$/.test(line))
106
132
  continue;
107
133
  for (const check of CODE_SMELLS) {
108
- // Skip console.log in CLI entry points (intentional output)
109
- if (check.name === "console.log" && (f.path.includes("cli.") || f.path.includes("bin/")))
134
+ // Skip console.log in CLI projects (intentional terminal output)
135
+ if (check.name === "console.log" && isCLI)
110
136
  continue;
111
137
  if (check.pattern.test(line)) {
112
138
  if (check.exclude?.test(line))
@@ -121,6 +147,12 @@ export function runStandards(cwd, stack) {
121
147
  // tsconfig maturity
122
148
  if (stack.language === "typescript") {
123
149
  const tsconfigPaths = ["tsconfig.json", "tsconfig.app.json", "tsconfig.base.json"];
150
+ // In monorepos, also check each workspace package's tsconfig
151
+ if (workspace?.isMonorepo) {
152
+ for (const pkg of workspace.packages) {
153
+ tsconfigPaths.push(join(pkg.path, "tsconfig.json"));
154
+ }
155
+ }
124
156
  let strictFound = false;
125
157
  let compilerOpts = {};
126
158
  for (const p of tsconfigPaths) {
@@ -192,7 +224,8 @@ export function runStandards(cwd, stack) {
192
224
  const totalFiles = files.length || 1;
193
225
  const errorPenalty = Math.min(40, (errors / totalFiles) * 150);
194
226
  const warningPenalty = Math.min(30, (warnings / totalFiles) * 80);
195
- const largePenalty = Math.min(20, (largeFiles / totalFiles) * 100);
227
+ // fileSizePenalty is already exponential normalize by file count, cap at 80
228
+ const largePenalty = Math.min(80, (fileSizePenalty / totalFiles) * 3);
196
229
  const score = Math.max(0, Math.min(100, Math.round(100 - errorPenalty - warningPenalty - largePenalty)));
197
230
  return {
198
231
  name: "standards",
@@ -61,7 +61,7 @@ export function runStructure(cwd, stack, workspace) {
61
61
  }
62
62
  }
63
63
  // Check for lockfile — in monorepos, lockfiles may be in packages
64
- const lockfiles = isDart ? ["pubspec.lock"] : ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"];
64
+ const lockfiles = isDart ? ["pubspec.lock"] : ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb", "bun.lock"];
65
65
  let hasLock = lockfiles.some((f) => existsSync(join(cwd, f)));
66
66
  if (!hasLock && workspace?.isMonorepo) {
67
67
  hasLock = workspace.packages.some((p) => lockfiles.some((f) => existsSync(join(cwd, p.path, f))));
@@ -0,0 +1,15 @@
1
+ /** Styling consistency — delegates to Stylelint when available, adds cross-file analysis.
2
+ *
3
+ * Tool delegation (same pattern as lint → biome/eslint, secrets → gitleaks):
4
+ * - Stylelint installed → run it for CSS/SCSS linting (170+ rules)
5
+ * - Always: cross-file analysis that no CSS linter covers:
6
+ * 1. Mixed styling approaches (Tailwind + CSS modules + styled-components + inline)
7
+ * 2. Hardcoded colors in JSX (not CSS — Stylelint handles CSS)
8
+ * 3. Magic numbers in spacing (cross-file consistency)
9
+ * 4. Inline style ratio
10
+ * 5. !important abuse
11
+ * 6. Duplicate Tailwind class strings across components
12
+ * 7. Inconsistent spacing values across components
13
+ */
14
+ import type { CheckResult } from "../types.js";
15
+ export declare function runStyling(cwd: string): CheckResult;