@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.
- package/README.md +130 -165
- package/dist/check-meta.js +59 -6
- package/dist/cli.js +299 -762
- package/dist/commands/explain.d.ts +2 -0
- package/dist/commands/explain.js +33 -0
- package/dist/commands/fix.d.ts +6 -0
- package/dist/commands/fix.js +157 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +96 -0
- package/dist/commands/shared.d.ts +4 -0
- package/dist/commands/shared.js +80 -0
- package/dist/core.d.ts +1 -0
- package/dist/core.js +12 -1
- package/dist/delta.d.ts +45 -0
- package/dist/delta.js +158 -0
- package/dist/detect.js +2 -2
- package/dist/pr-comment.d.ts +1 -1
- package/dist/pr-comment.js +23 -4
- package/dist/report/html.d.ts +1 -1
- package/dist/report/html.js +7 -2
- package/dist/report/pages.d.ts +2 -0
- package/dist/report/pages.js +167 -0
- package/dist/report/styles.d.ts +1 -1
- package/dist/report/styles.js +37 -0
- package/dist/runners/accessibility.js +4 -1
- package/dist/runners/best-practices.js +1 -1
- package/dist/runners/confusion.js +28 -17
- package/dist/runners/design-consistency.d.ts +12 -0
- package/dist/runners/design-consistency.js +125 -0
- package/dist/runners/error-handling.js +18 -2
- package/dist/runners/file-cohesion.d.ts +17 -0
- package/dist/runners/file-cohesion.js +177 -0
- package/dist/runners/frontend-health.d.ts +14 -0
- package/dist/runners/frontend-health.js +206 -0
- package/dist/runners/html-quality.d.ts +8 -0
- package/dist/runners/html-quality.js +203 -0
- package/dist/runners/lint.js +6 -1
- package/dist/runners/react.js +1 -0
- package/dist/runners/secrets.js +7 -2
- package/dist/runners/security.js +7 -1
- package/dist/runners/standards.d.ts +2 -2
- package/dist/runners/standards.js +45 -12
- package/dist/runners/structure.js +1 -1
- package/dist/runners/styling.d.ts +15 -0
- package/dist/runners/styling.js +280 -0
- package/dist/runners/testing.js +3 -1
- 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
|
+
}
|
package/dist/runners/testing.js
CHANGED
|
@@ -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"]
|
|
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.
|
|
4
|
-
"description": "Code health scanner for the AI coding era.
|
|
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",
|