@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,280 @@
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 { existsSync, readFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { getProductionFiles, readDeps } from "../fs-utils.js";
17
+ import { gradeFromScore } from "../types.js";
18
+ import { runJSON } from "./exec.js";
19
+ function tryStylelint(cwd) {
20
+ const deps = readDeps(cwd);
21
+ if (!deps.stylelint)
22
+ return null;
23
+ // Check for config
24
+ const hasConfig = existsSync(join(cwd, ".stylelintrc.json")) ||
25
+ existsSync(join(cwd, ".stylelintrc.js")) ||
26
+ existsSync(join(cwd, ".stylelintrc.yml")) ||
27
+ existsSync(join(cwd, "stylelint.config.js")) ||
28
+ existsSync(join(cwd, "stylelint.config.mjs")) ||
29
+ existsSync(join(cwd, "stylelint.config.cjs"));
30
+ if (!hasConfig) {
31
+ // No config — try with standard config if available
32
+ const hasStandard = deps["stylelint-config-standard"] || deps["stylelint-config-recommended"];
33
+ if (!hasStandard)
34
+ return null;
35
+ }
36
+ const results = runJSON("npx stylelint --formatter json \"**/*.{css,scss}\" --ignore-pattern node_modules 2>/dev/null || true", cwd, 30_000);
37
+ if (!results || !Array.isArray(results))
38
+ return null;
39
+ const issues = [];
40
+ for (const file of results) {
41
+ const relPath = file.source.replace(cwd + "/", "").replace(cwd + "\\", "");
42
+ for (const w of file.warnings) {
43
+ issues.push({
44
+ severity: w.severity === "error" ? "error" : "warning",
45
+ message: `${w.text} (${w.rule})`,
46
+ file: relPath,
47
+ line: w.line,
48
+ rule: w.rule,
49
+ });
50
+ }
51
+ }
52
+ return issues;
53
+ }
54
+ // ── Cross-file analysis (our unique value — no linter covers this) ──
55
+ const TAILWIND_CLASS = /className\s*=\s*["'`][^"'`]*(?:flex|grid|p-|m-|text-|bg-|rounded|border|shadow|w-|h-)/;
56
+ const CSS_MODULE = /styles\.\w+|\.module\.css|\.module\.scss/;
57
+ const STYLED_COMPONENT = /styled\.\w+|styled\(|css`/;
58
+ const INLINE_STYLE = /style\s*=\s*\{\s*\{|style\s*=\s*\{[^}]/;
59
+ const EMOTION_CSS = /@emotion|css\s*\(/;
60
+ const HARDCODED_COLOR_JSX = /(?:color|backgroundColor|borderColor|fill|stroke)\s*:\s*['"]#[0-9a-fA-F]{3,8}/;
61
+ const SPACING_PROP = /(?:margin|padding|gap|top|bottom|left|right|width|height|inset)\s*:\s*/;
62
+ const MAGIC_PX = /(\d+)px/g;
63
+ export function runStyling(cwd) {
64
+ const start = Date.now();
65
+ const issues = [];
66
+ const files = getProductionFiles(cwd);
67
+ const deps = readDeps(cwd);
68
+ const componentFiles = files.filter((f) => !f.isTest && /\.(tsx|jsx|vue|svelte)$/.test(f.path));
69
+ if (componentFiles.length === 0) {
70
+ return {
71
+ name: "styling",
72
+ score: 0,
73
+ grade: "F",
74
+ details: { skipped: true, reason: "no component files found" },
75
+ issues: [],
76
+ duration: Date.now() - start,
77
+ };
78
+ }
79
+ // ── Phase 1: Delegate to Stylelint ──
80
+ let tool = "built-in";
81
+ const stylelintIssues = tryStylelint(cwd);
82
+ if (stylelintIssues) {
83
+ tool = "stylelint";
84
+ issues.push(...stylelintIssues);
85
+ }
86
+ // ── Phase 2: Cross-file analysis (always runs — Stylelint can't do this) ──
87
+ const approaches = new Map();
88
+ const hasTailwind = existsSync(join(cwd, "tailwind.config.js")) ||
89
+ existsSync(join(cwd, "tailwind.config.ts")) ||
90
+ existsSync(join(cwd, "tailwind.config.mjs")) ||
91
+ !!deps.tailwindcss;
92
+ let inlineStyleCount = 0;
93
+ let classNameCount = 0;
94
+ let hardcodedColorCount = 0;
95
+ let importantCount = 0;
96
+ const spacingValues = new Map();
97
+ const tailwindStrings = new Map();
98
+ // Scan CSS files for !important (only if Stylelint didn't already flag them)
99
+ if (!stylelintIssues) {
100
+ scanCssFiles(cwd, "src", (content) => {
101
+ importantCount += (content.match(/!important/g) || []).length;
102
+ });
103
+ }
104
+ for (const f of componentFiles) {
105
+ const lines = f.content.split("\n");
106
+ for (let i = 0; i < lines.length; i++) {
107
+ const line = lines[i];
108
+ const trimmed = line.trim();
109
+ if (trimmed.startsWith("//") || trimmed.startsWith("*"))
110
+ continue;
111
+ // Detect styling approaches
112
+ if (TAILWIND_CLASS.test(line))
113
+ approaches.set("tailwind", (approaches.get("tailwind") || 0) + 1);
114
+ if (CSS_MODULE.test(line))
115
+ approaches.set("css-modules", (approaches.get("css-modules") || 0) + 1);
116
+ if (STYLED_COMPONENT.test(line))
117
+ approaches.set("styled-components", (approaches.get("styled-components") || 0) + 1);
118
+ if (EMOTION_CSS.test(line))
119
+ approaches.set("emotion", (approaches.get("emotion") || 0) + 1);
120
+ if (INLINE_STYLE.test(line)) {
121
+ approaches.set("inline", (approaches.get("inline") || 0) + 1);
122
+ inlineStyleCount++;
123
+ }
124
+ if (/className/.test(line))
125
+ classNameCount++;
126
+ // Hardcoded colors in JSX (Stylelint only catches CSS files)
127
+ if (HARDCODED_COLOR_JSX.test(line)) {
128
+ if (f.path.includes("tailwind.config") || f.path.includes("theme") || f.path.includes("tokens"))
129
+ continue;
130
+ hardcodedColorCount++;
131
+ if (hardcodedColorCount <= 5) {
132
+ const match = line.match(/#[0-9a-fA-F]{3,8}/);
133
+ issues.push({
134
+ severity: "warning",
135
+ message: `Hardcoded color ${match?.[0] || ""} in JSX — use a CSS variable or design token`,
136
+ file: f.path,
137
+ line: i + 1,
138
+ rule: "hardcoded-color",
139
+ });
140
+ }
141
+ }
142
+ // Spacing values (cross-file consistency)
143
+ if (SPACING_PROP.test(line)) {
144
+ let match;
145
+ MAGIC_PX.lastIndex = 0;
146
+ while ((match = MAGIC_PX.exec(line)) !== null) {
147
+ const px = parseInt(match[1], 10);
148
+ if (px > 2)
149
+ spacingValues.set(px, (spacingValues.get(px) || 0) + 1);
150
+ }
151
+ }
152
+ // Duplicate Tailwind class strings
153
+ if (hasTailwind) {
154
+ const classMatch = line.match(/className\s*=\s*["'`]([^"'`]{20,})["'`]/);
155
+ if (classMatch) {
156
+ const classes = classMatch[1].trim();
157
+ const existing = tailwindStrings.get(classes) || [];
158
+ existing.push(f.path);
159
+ tailwindStrings.set(classes, existing);
160
+ }
161
+ }
162
+ }
163
+ }
164
+ // ── Aggregate findings ──
165
+ // Mixed approaches
166
+ const activeApproaches = [...approaches.entries()].filter(([, count]) => count >= 3);
167
+ if (activeApproaches.length > 1) {
168
+ issues.push({
169
+ severity: "warning",
170
+ message: `Mixed styling approaches: ${activeApproaches.map(([n, c]) => `${n} (${c})`).join(", ")} — pick one`,
171
+ rule: "mixed-styling",
172
+ });
173
+ }
174
+ // Inline style ratio
175
+ if (componentFiles.length > 3 && inlineStyleCount > 0) {
176
+ const ratio = inlineStyleCount / (inlineStyleCount + classNameCount);
177
+ if (ratio > 0.3) {
178
+ issues.push({
179
+ severity: "warning",
180
+ message: `${Math.round(ratio * 100)}% inline styles — extract to CSS classes or Tailwind`,
181
+ rule: "inline-style-ratio",
182
+ });
183
+ }
184
+ }
185
+ // Hardcoded colors summary
186
+ if (hardcodedColorCount > 5) {
187
+ issues.push({ severity: "warning", message: `${hardcodedColorCount} hardcoded colors in JSX — define a color palette`, rule: "hardcoded-color" });
188
+ }
189
+ // !important (only if we counted, not Stylelint)
190
+ if (!stylelintIssues && importantCount > 3) {
191
+ issues.push({ severity: "warning", message: `${importantCount} uses of !important — indicates specificity wars`, rule: "important-abuse" });
192
+ }
193
+ // Inconsistent spacing
194
+ const values = [...spacingValues.keys()].sort((a, b) => a - b);
195
+ const notOnScale = values.filter((v) => v % 4 !== 0 && v !== 1 && v !== 2);
196
+ if (notOnScale.length > 3) {
197
+ issues.push({ severity: "warning", message: `Inconsistent spacing: ${notOnScale.slice(0, 6).join(", ")}px — use a 4px/8px scale`, rule: "inconsistent-spacing" });
198
+ }
199
+ // Duplicate Tailwind strings
200
+ if (hasTailwind) {
201
+ let dupeCount = 0;
202
+ for (const [classes, usedIn] of tailwindStrings) {
203
+ if (usedIn.length >= 3) {
204
+ dupeCount++;
205
+ if (dupeCount <= 3) {
206
+ issues.push({
207
+ severity: "info",
208
+ message: `Tailwind classes duplicated in ${usedIn.length} files — extract component: "${classes.slice(0, 60)}${classes.length > 60 ? "..." : ""}"`,
209
+ rule: "duplicate-tailwind",
210
+ });
211
+ }
212
+ }
213
+ }
214
+ if (dupeCount > 3) {
215
+ issues.push({ severity: "warning", message: `${dupeCount} duplicated Tailwind class strings — extract shared components`, rule: "duplicate-tailwind" });
216
+ }
217
+ }
218
+ // Tailwind theme check
219
+ if (hasTailwind && componentFiles.length > 5) {
220
+ let hasThemeExtend = false;
221
+ for (const cfg of ["tailwind.config.js", "tailwind.config.ts", "tailwind.config.mjs"]) {
222
+ if (existsSync(join(cwd, cfg))) {
223
+ try {
224
+ if (readFileSync(join(cwd, cfg), "utf-8").includes("extend"))
225
+ hasThemeExtend = true;
226
+ }
227
+ catch { /* ignore */ }
228
+ break;
229
+ }
230
+ }
231
+ if (!hasThemeExtend) {
232
+ issues.push({ severity: "info", message: "No theme extension in tailwind.config — consider defining custom colors/spacing", rule: "tailwind-no-theme" });
233
+ }
234
+ }
235
+ const errorCount = issues.filter((i) => i.severity === "error").length;
236
+ const warnCount = issues.filter((i) => i.severity === "warning").length;
237
+ const score = Math.max(0, 100 - errorCount * 20 - warnCount * 12);
238
+ return {
239
+ name: "styling",
240
+ score,
241
+ grade: gradeFromScore(score),
242
+ details: {
243
+ tool,
244
+ totalComponentFiles: componentFiles.length,
245
+ approaches: Object.fromEntries(approaches),
246
+ hasTailwind,
247
+ inlineStyleCount,
248
+ hardcodedColorCount,
249
+ importantCount,
250
+ spacingValues: spacingValues.size,
251
+ stylelintIssues: stylelintIssues?.length ?? 0,
252
+ suggestion: !stylelintIssues ? "Install Stylelint for deeper CSS analysis (170+ rules): pnpm add -D stylelint stylelint-config-standard" : undefined,
253
+ },
254
+ issues,
255
+ duration: Date.now() - start,
256
+ };
257
+ }
258
+ /** Recursively scan for CSS/SCSS files. */
259
+ function scanCssFiles(cwd, subdir, fn) {
260
+ const { readdirSync, statSync } = require("node:fs");
261
+ const dir = join(cwd, subdir);
262
+ if (!existsSync(dir))
263
+ return;
264
+ try {
265
+ for (const entry of readdirSync(dir)) {
266
+ if (entry === "node_modules" || entry === ".git" || entry === ".vibe-check")
267
+ continue;
268
+ const full = join(dir, entry);
269
+ try {
270
+ const stat = statSync(full);
271
+ if (stat.isDirectory())
272
+ scanCssFiles(cwd, join(subdir, entry), fn);
273
+ else if (/\.(css|scss)$/.test(entry))
274
+ fn(readFileSync(full, "utf-8"));
275
+ }
276
+ catch { /* skip */ }
277
+ }
278
+ }
279
+ catch { /* skip */ }
280
+ }
@@ -47,7 +47,9 @@ function countPatterns(content) {
47
47
  // ── File discovery ──
48
48
  function findTestFiles(cwd, srcRoots) {
49
49
  const files = [];
50
- const dirs = srcRoots ? [...srcRoots, "e2e", "playwright"] : ["src", "web/src", "lib", "test", "tests", "__tests__", "e2e", "playwright"];
50
+ const dirs = srcRoots ? [...srcRoots, "e2e", "playwright"]
51
+ : existsSync(join(cwd, "src")) ? ["src", "web/src", "lib", "test", "tests", "__tests__", "e2e", "playwright"]
52
+ : [".", "test", "tests", "__tests__", "e2e", "playwright"];
51
53
  const seen = new Set();
52
54
  for (const dir of dirs) {
53
55
  const full = join(cwd, dir);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vibecodeqa/cli",
3
- "version": "0.42.0",
4
- "description": "Code health scanner for the AI coding era. 25 checks, zero config, full report.",
3
+ "version": "0.44.0",
4
+ "description": "Code health scanner for the AI coding era. 34 checks, zero config, full report.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "vcqa": "./dist/cli.js",