@vibecodeqa/cli 0.22.0 → 0.23.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/dist/cli.js +122 -102
- package/dist/runners/best-practices.js +93 -21
- package/dist/runners/standards.js +4 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -31,17 +31,22 @@ import { computeTrend, formatTrend } from "./trend.js";
|
|
|
31
31
|
import { gradeFromScore } from "./types.js";
|
|
32
32
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
33
33
|
const VERSION = pkg.version;
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
34
|
+
function parseFlags() {
|
|
35
|
+
const args = process.argv.slice(2);
|
|
36
|
+
const flags = new Set(args.filter((a) => a.startsWith("--")));
|
|
37
|
+
const cwd = resolve(args.find((a) => !a.startsWith("--")) || ".");
|
|
38
|
+
return {
|
|
39
|
+
cwd,
|
|
40
|
+
outputDir: join(cwd, ".vibe-check"),
|
|
41
|
+
jsonOnly: flags.has("--json"),
|
|
42
|
+
ciMode: flags.has("--ci"),
|
|
43
|
+
skipTests: flags.has("--skip-tests"),
|
|
44
|
+
watchMode: flags.has("--watch"),
|
|
45
|
+
badgeMode: flags.has("--badge"),
|
|
46
|
+
sarifMode: flags.has("--sarif"),
|
|
47
|
+
uploadMode: flags.has("--upload"),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
45
50
|
function color(grade) {
|
|
46
51
|
if (grade === "A")
|
|
47
52
|
return "\x1b[32m";
|
|
@@ -49,23 +54,7 @@ function color(grade) {
|
|
|
49
54
|
return "\x1b[33m";
|
|
50
55
|
return "\x1b[31m";
|
|
51
56
|
}
|
|
52
|
-
|
|
53
|
-
const start = Date.now();
|
|
54
|
-
if (!jsonOnly) {
|
|
55
|
-
console.log("");
|
|
56
|
-
console.log(` \x1b[1m\x1b[38;5;141mvcqa\x1b[0m v${VERSION}`);
|
|
57
|
-
console.log(` \x1b[2m${cwd}\x1b[0m`);
|
|
58
|
-
console.log("");
|
|
59
|
-
}
|
|
60
|
-
const stack = detectStack(cwd);
|
|
61
|
-
if (!jsonOnly) {
|
|
62
|
-
const parts = [stack.language, stack.framework, stack.bundler, stack.testRunner, stack.linter, stack.packageManager].filter((v) => v !== "none" && v !== "unknown");
|
|
63
|
-
console.log(` stack: ${parts.join(" + ")}`);
|
|
64
|
-
console.log("");
|
|
65
|
-
}
|
|
66
|
-
const checks = [];
|
|
67
|
-
const isDart = stack.language === "dart";
|
|
68
|
-
// All runners grouped by category
|
|
57
|
+
function runChecks(cwd, stack, skipTests, isDart, jsonOnly) {
|
|
69
58
|
const runners = [
|
|
70
59
|
// Foundations
|
|
71
60
|
{ name: "structure", fn: () => runStructure(cwd, stack) },
|
|
@@ -97,6 +86,7 @@ async function main() {
|
|
|
97
86
|
{ name: "doc-coherence", fn: () => runDocCoherence(cwd) },
|
|
98
87
|
{ name: "code-coherence", fn: () => runCodeCoherence(cwd) },
|
|
99
88
|
];
|
|
89
|
+
const checks = [];
|
|
100
90
|
for (const runner of runners) {
|
|
101
91
|
if (!jsonOnly)
|
|
102
92
|
process.stdout.write(` ${runner.name.padEnd(14)}`);
|
|
@@ -113,20 +103,9 @@ async function main() {
|
|
|
113
103
|
console.log(`${c}${label.padEnd(5)}${scoreStr}\x1b[0m \x1b[2m${result.duration}ms\x1b[0m${issueStr}`);
|
|
114
104
|
}
|
|
115
105
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const totalIssues = checks.reduce((s, c) => s + c.issues.length, 0);
|
|
120
|
-
const report = {
|
|
121
|
-
version: VERSION,
|
|
122
|
-
timestamp: new Date().toISOString(),
|
|
123
|
-
score,
|
|
124
|
-
grade,
|
|
125
|
-
checks,
|
|
126
|
-
meta: { cwd, node: process.version, duration, stack, ...detectRepoUrl(cwd) },
|
|
127
|
-
};
|
|
128
|
-
// Trend comparison (read previous report before overwriting)
|
|
129
|
-
const trend = computeTrend(report, outputDir);
|
|
106
|
+
return checks;
|
|
107
|
+
}
|
|
108
|
+
async function writeOutputs(report, outputDir, flags) {
|
|
130
109
|
if (!existsSync(outputDir))
|
|
131
110
|
mkdirSync(outputDir, { recursive: true });
|
|
132
111
|
// Save to history before overwriting current report
|
|
@@ -159,58 +138,127 @@ async function main() {
|
|
|
159
138
|
writeFileSync(join(reportDir, filename), html);
|
|
160
139
|
}
|
|
161
140
|
// Badge SVG
|
|
162
|
-
if (badgeMode) {
|
|
141
|
+
if (flags.badgeMode) {
|
|
163
142
|
const { buildBadge } = await import("./report/svg.js");
|
|
164
|
-
const badgeSvg = buildBadge(score, grade);
|
|
143
|
+
const badgeSvg = buildBadge(report.score, report.grade);
|
|
165
144
|
writeFileSync(join(outputDir, "badge.svg"), badgeSvg);
|
|
166
145
|
}
|
|
167
146
|
// SARIF output for GitHub Code Scanning
|
|
168
|
-
if (sarifMode) {
|
|
147
|
+
if (flags.sarifMode) {
|
|
169
148
|
const { generateSARIF } = await import("./report/sarif.js");
|
|
170
149
|
writeFileSync(join(outputDir, "report.sarif"), generateSARIF(report));
|
|
171
150
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const res = await fetch("https://api.vibecodeqa.online/api/reports", {
|
|
178
|
-
method: "POST",
|
|
179
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
180
|
-
body: JSON.stringify({ repo, report }),
|
|
181
|
-
});
|
|
182
|
-
if (res.ok) {
|
|
183
|
-
const data = (await res.json());
|
|
184
|
-
if (!jsonOnly)
|
|
185
|
-
console.log(` \x1b[32m\u2713 Uploaded to dashboard\x1b[0m \x1b[2m(${data.totalReports || 1} reports)\x1b[0m`);
|
|
186
|
-
}
|
|
187
|
-
else if (!jsonOnly) {
|
|
188
|
-
console.log(` \x1b[33m\u26a0 Upload failed: ${res.status}\x1b[0m \x1b[2m(set VCQA_TOKEN env var)\x1b[0m`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
catch {
|
|
192
|
-
if (!jsonOnly)
|
|
193
|
-
console.log(` \x1b[33m\u26a0 Upload failed (network error)\x1b[0m`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
if (jsonOnly) {
|
|
151
|
+
}
|
|
152
|
+
function printResults(report, trend, flags, outputDir) {
|
|
153
|
+
const { score, grade, checks } = report;
|
|
154
|
+
const totalIssues = checks.reduce((s, c) => s + c.issues.length, 0);
|
|
155
|
+
if (flags.jsonOnly) {
|
|
197
156
|
console.log(JSON.stringify(report));
|
|
198
157
|
}
|
|
199
158
|
else {
|
|
200
159
|
const gc = color(grade);
|
|
201
160
|
console.log("");
|
|
202
|
-
console.log(` ${gc}\x1b[1m${grade}\x1b[0m ${gc}${score}/100\x1b[0m \x1b[2m${checks.length} checks · ${totalIssues} issues · ${duration}ms\x1b[0m`);
|
|
161
|
+
console.log(` ${gc}\x1b[1m${grade}\x1b[0m ${gc}${score}/100\x1b[0m \x1b[2m${checks.length} checks · ${totalIssues} issues · ${report.meta.duration}ms\x1b[0m`);
|
|
203
162
|
if (trend)
|
|
204
163
|
console.log(formatTrend(trend));
|
|
205
164
|
console.log("");
|
|
206
165
|
console.log(` \x1b[2mReport: ${join(outputDir, "report/index.html")}\x1b[0m`);
|
|
207
166
|
console.log(` \x1b[2mJSON: ${join(outputDir, "report.json")}\x1b[0m`);
|
|
208
|
-
if (badgeMode)
|
|
167
|
+
if (flags.badgeMode)
|
|
209
168
|
console.log(` \x1b[2mBadge: ${join(outputDir, "badge.svg")}\x1b[0m`);
|
|
210
|
-
if (sarifMode)
|
|
169
|
+
if (flags.sarifMode)
|
|
211
170
|
console.log(` \x1b[2mSARIF: ${join(outputDir, "report.sarif")}\x1b[0m`);
|
|
212
171
|
console.log("");
|
|
213
172
|
}
|
|
173
|
+
}
|
|
174
|
+
async function handleUpload(report, cwd, jsonOnly) {
|
|
175
|
+
const repo = report.meta.repoUrl?.replace(/^https:\/\/github\.com\//, "") || cwd.split("/").pop() || "project";
|
|
176
|
+
const token = process.env.VCQA_TOKEN || "";
|
|
177
|
+
try {
|
|
178
|
+
const res = await fetch("https://api.vibecodeqa.online/api/reports", {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
181
|
+
body: JSON.stringify({ repo, report }),
|
|
182
|
+
});
|
|
183
|
+
if (res.ok) {
|
|
184
|
+
const data = (await res.json());
|
|
185
|
+
if (!jsonOnly)
|
|
186
|
+
console.log(` \x1b[32m\u2713 Uploaded to dashboard\x1b[0m \x1b[2m(${data.totalReports || 1} reports)\x1b[0m`);
|
|
187
|
+
}
|
|
188
|
+
else if (!jsonOnly) {
|
|
189
|
+
console.log(` \x1b[33m\u26a0 Upload failed: ${res.status}\x1b[0m \x1b[2m(set VCQA_TOKEN env var)\x1b[0m`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
if (!jsonOnly)
|
|
194
|
+
console.log(` \x1b[33m\u26a0 Upload failed (network error)\x1b[0m`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function startWatch(cwd) {
|
|
198
|
+
const { watch } = await import("node:fs");
|
|
199
|
+
const srcDirs = ["src", "web/src"].map((d) => join(cwd, d)).filter((d) => existsSync(d));
|
|
200
|
+
if (srcDirs.length === 0) {
|
|
201
|
+
console.log(" \x1b[31mNo src/ directory to watch\x1b[0m");
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
console.log(" \x1b[2mWatching for changes... (Ctrl+C to stop)\x1b[0m");
|
|
205
|
+
console.log("");
|
|
206
|
+
let debounce = null;
|
|
207
|
+
let running = false;
|
|
208
|
+
for (const dir of srcDirs) {
|
|
209
|
+
watch(dir, { recursive: true }, (_event, filename) => {
|
|
210
|
+
if (!filename || filename.includes("node_modules") || filename.includes(".vibe-check"))
|
|
211
|
+
return;
|
|
212
|
+
if (running)
|
|
213
|
+
return;
|
|
214
|
+
if (debounce)
|
|
215
|
+
clearTimeout(debounce);
|
|
216
|
+
debounce = setTimeout(async () => {
|
|
217
|
+
running = true;
|
|
218
|
+
console.log(` \x1b[2mChanged: ${filename} — re-scanning...\x1b[0m`);
|
|
219
|
+
await main().catch(() => { });
|
|
220
|
+
running = false;
|
|
221
|
+
}, 500);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
// Keep process alive
|
|
225
|
+
await new Promise(() => { });
|
|
226
|
+
}
|
|
227
|
+
async function main() {
|
|
228
|
+
const flags = parseFlags();
|
|
229
|
+
const { cwd, outputDir, jsonOnly, ciMode, skipTests, watchMode } = flags;
|
|
230
|
+
const start = Date.now();
|
|
231
|
+
if (!jsonOnly) {
|
|
232
|
+
console.log("");
|
|
233
|
+
console.log(` \x1b[1m\x1b[38;5;141mvcqa\x1b[0m v${VERSION}`);
|
|
234
|
+
console.log(` \x1b[2m${cwd}\x1b[0m`);
|
|
235
|
+
console.log("");
|
|
236
|
+
}
|
|
237
|
+
const stack = detectStack(cwd);
|
|
238
|
+
if (!jsonOnly) {
|
|
239
|
+
const parts = [stack.language, stack.framework, stack.bundler, stack.testRunner, stack.linter, stack.packageManager].filter((v) => v !== "none" && v !== "unknown");
|
|
240
|
+
console.log(` stack: ${parts.join(" + ")}`);
|
|
241
|
+
console.log("");
|
|
242
|
+
}
|
|
243
|
+
const isDart = stack.language === "dart";
|
|
244
|
+
const checks = runChecks(cwd, stack, skipTests, isDart, jsonOnly);
|
|
245
|
+
const score = computeScore(checks);
|
|
246
|
+
const grade = gradeFromScore(score);
|
|
247
|
+
const duration = Date.now() - start;
|
|
248
|
+
const report = {
|
|
249
|
+
version: VERSION,
|
|
250
|
+
timestamp: new Date().toISOString(),
|
|
251
|
+
score,
|
|
252
|
+
grade,
|
|
253
|
+
checks,
|
|
254
|
+
meta: { cwd, node: process.version, duration, stack, ...detectRepoUrl(cwd) },
|
|
255
|
+
};
|
|
256
|
+
const trend = computeTrend(report, outputDir);
|
|
257
|
+
await writeOutputs(report, outputDir, flags);
|
|
258
|
+
printResults(report, trend, flags, outputDir);
|
|
259
|
+
if (flags.uploadMode) {
|
|
260
|
+
await handleUpload(report, cwd, jsonOnly);
|
|
261
|
+
}
|
|
214
262
|
if (ciMode && score < 60) {
|
|
215
263
|
process.exit(1);
|
|
216
264
|
}
|
|
@@ -224,36 +272,8 @@ async function main() {
|
|
|
224
272
|
/* failed to open browser */
|
|
225
273
|
}
|
|
226
274
|
}
|
|
227
|
-
// Watch mode — re-run on file changes
|
|
228
275
|
if (watchMode) {
|
|
229
|
-
|
|
230
|
-
const srcDirs = ["src", "web/src"].map((d) => join(cwd, d)).filter((d) => existsSync(d));
|
|
231
|
-
if (srcDirs.length === 0) {
|
|
232
|
-
console.log(" \x1b[31mNo src/ directory to watch\x1b[0m");
|
|
233
|
-
process.exit(1);
|
|
234
|
-
}
|
|
235
|
-
console.log(" \x1b[2mWatching for changes... (Ctrl+C to stop)\x1b[0m");
|
|
236
|
-
console.log("");
|
|
237
|
-
let debounce = null;
|
|
238
|
-
let running = false;
|
|
239
|
-
for (const dir of srcDirs) {
|
|
240
|
-
watch(dir, { recursive: true }, (_event, filename) => {
|
|
241
|
-
if (!filename || filename.includes("node_modules") || filename.includes(".vibe-check"))
|
|
242
|
-
return;
|
|
243
|
-
if (running)
|
|
244
|
-
return; // prevent concurrent re-runs (M5)
|
|
245
|
-
if (debounce)
|
|
246
|
-
clearTimeout(debounce);
|
|
247
|
-
debounce = setTimeout(async () => {
|
|
248
|
-
running = true;
|
|
249
|
-
console.log(` \x1b[2mChanged: ${filename} — re-scanning...\x1b[0m`);
|
|
250
|
-
await main().catch(() => { });
|
|
251
|
-
running = false;
|
|
252
|
-
}, 500);
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
// Keep process alive
|
|
256
|
-
await new Promise(() => { });
|
|
276
|
+
await startWatch(cwd);
|
|
257
277
|
}
|
|
258
278
|
}
|
|
259
279
|
main().catch((err) => {
|
|
@@ -13,21 +13,10 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { readDeps } from "../fs-utils.js";
|
|
15
15
|
import { gradeFromScore } from "../types.js";
|
|
16
|
-
|
|
17
|
-
const start = Date.now();
|
|
16
|
+
function checkCICD(cwd, has, read) {
|
|
18
17
|
const issues = [];
|
|
19
18
|
let practices = 0;
|
|
20
19
|
let followed = 0;
|
|
21
|
-
const has = (f) => existsSync(join(cwd, f));
|
|
22
|
-
const read = (f) => {
|
|
23
|
-
try {
|
|
24
|
-
return readFileSync(join(cwd, f), "utf-8");
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return "";
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
// ── 1. CI/CD Best Practices ──
|
|
31
20
|
// Check for GitHub Actions workflows
|
|
32
21
|
const hasWorkflows = has(".github/workflows");
|
|
33
22
|
practices++;
|
|
@@ -99,7 +88,12 @@ export function runBestPractices(cwd) {
|
|
|
99
88
|
rule: "no-ci",
|
|
100
89
|
});
|
|
101
90
|
}
|
|
102
|
-
|
|
91
|
+
return { practices, followed, issues };
|
|
92
|
+
}
|
|
93
|
+
function checkSupplyChain(has, read) {
|
|
94
|
+
const issues = [];
|
|
95
|
+
let practices = 0;
|
|
96
|
+
let followed = 0;
|
|
103
97
|
// Lockfile committed
|
|
104
98
|
practices++;
|
|
105
99
|
const hasLockfile = has("pnpm-lock.yaml") || has("package-lock.json") || has("yarn.lock") || has("bun.lockb") || has("pubspec.lock");
|
|
@@ -136,7 +130,12 @@ export function runBestPractices(cwd) {
|
|
|
136
130
|
});
|
|
137
131
|
}
|
|
138
132
|
}
|
|
139
|
-
|
|
133
|
+
return { practices, followed, issues };
|
|
134
|
+
}
|
|
135
|
+
function checkRepoHygiene(has) {
|
|
136
|
+
const issues = [];
|
|
137
|
+
let practices = 0;
|
|
138
|
+
let followed = 0;
|
|
140
139
|
// SECURITY.md or security policy
|
|
141
140
|
practices++;
|
|
142
141
|
if (has("SECURITY.md") || has(".github/SECURITY.md")) {
|
|
@@ -165,7 +164,12 @@ export function runBestPractices(cwd) {
|
|
|
165
164
|
rule: "contributing-guide",
|
|
166
165
|
});
|
|
167
166
|
}
|
|
168
|
-
|
|
167
|
+
return { practices, followed, issues };
|
|
168
|
+
}
|
|
169
|
+
function checkDevExperience(cwd, has) {
|
|
170
|
+
const issues = [];
|
|
171
|
+
let practices = 0;
|
|
172
|
+
let followed = 0;
|
|
169
173
|
// .env.example
|
|
170
174
|
practices++;
|
|
171
175
|
const hasEnvFiles = has(".env") || has(".env.local") || has(".env.development");
|
|
@@ -204,7 +208,12 @@ export function runBestPractices(cwd) {
|
|
|
204
208
|
rule: "automated-deps",
|
|
205
209
|
});
|
|
206
210
|
}
|
|
207
|
-
|
|
211
|
+
return { practices, followed, issues };
|
|
212
|
+
}
|
|
213
|
+
function checkCodeQualityTooling(has, read) {
|
|
214
|
+
const issues = [];
|
|
215
|
+
let practices = 0;
|
|
216
|
+
let followed = 0;
|
|
208
217
|
// Linter configured
|
|
209
218
|
practices++;
|
|
210
219
|
if (has("biome.json") ||
|
|
@@ -247,7 +256,13 @@ export function runBestPractices(cwd) {
|
|
|
247
256
|
rule: "ts-strict-mode",
|
|
248
257
|
});
|
|
249
258
|
}
|
|
250
|
-
|
|
259
|
+
return { practices, followed, issues };
|
|
260
|
+
}
|
|
261
|
+
function checkTesting(has, read) {
|
|
262
|
+
const issues = [];
|
|
263
|
+
let practices = 0;
|
|
264
|
+
let followed = 0;
|
|
265
|
+
const pkg = read("package.json");
|
|
251
266
|
// Test script exists
|
|
252
267
|
practices++;
|
|
253
268
|
if (pkg.includes('"test"') || has("pubspec.yaml")) {
|
|
@@ -268,7 +283,12 @@ export function runBestPractices(cwd) {
|
|
|
268
283
|
rule: "coverage-config",
|
|
269
284
|
});
|
|
270
285
|
}
|
|
271
|
-
|
|
286
|
+
return { practices, followed, issues };
|
|
287
|
+
}
|
|
288
|
+
function checkDocker(has, read) {
|
|
289
|
+
const issues = [];
|
|
290
|
+
let practices = 0;
|
|
291
|
+
let followed = 0;
|
|
272
292
|
// Dockerfile best practices (if Docker is used)
|
|
273
293
|
if (has("Dockerfile") || has("docker-compose.yml") || has("docker-compose.yaml")) {
|
|
274
294
|
practices++;
|
|
@@ -315,7 +335,13 @@ export function runBestPractices(cwd) {
|
|
|
315
335
|
});
|
|
316
336
|
}
|
|
317
337
|
}
|
|
318
|
-
|
|
338
|
+
return { practices, followed, issues };
|
|
339
|
+
}
|
|
340
|
+
function checkGitPractices(cwd, has, read) {
|
|
341
|
+
const issues = [];
|
|
342
|
+
let practices = 0;
|
|
343
|
+
let followed = 0;
|
|
344
|
+
const deps = readDeps(cwd);
|
|
319
345
|
// .gitignore is comprehensive
|
|
320
346
|
practices++;
|
|
321
347
|
const gitignore = read(".gitignore");
|
|
@@ -344,7 +370,13 @@ export function runBestPractices(cwd) {
|
|
|
344
370
|
rule: "conventional-commits",
|
|
345
371
|
});
|
|
346
372
|
}
|
|
347
|
-
|
|
373
|
+
return { practices, followed, issues };
|
|
374
|
+
}
|
|
375
|
+
function checkMonitoring(cwd) {
|
|
376
|
+
const issues = [];
|
|
377
|
+
let practices = 0;
|
|
378
|
+
let followed = 0;
|
|
379
|
+
const deps = readDeps(cwd);
|
|
348
380
|
// Error tracking (Sentry, Bugsnag, etc.) — only for apps/servers, not CLI tools
|
|
349
381
|
const isApp = deps.react || deps.vue || deps.svelte || deps.express || deps.fastify || deps.hono || deps.next || deps.nuxt;
|
|
350
382
|
if (isApp) {
|
|
@@ -360,7 +392,14 @@ export function runBestPractices(cwd) {
|
|
|
360
392
|
});
|
|
361
393
|
}
|
|
362
394
|
}
|
|
363
|
-
|
|
395
|
+
return { practices, followed, issues };
|
|
396
|
+
}
|
|
397
|
+
function checkAPIConfig(cwd, read) {
|
|
398
|
+
const issues = [];
|
|
399
|
+
let practices = 0;
|
|
400
|
+
let followed = 0;
|
|
401
|
+
const deps = readDeps(cwd);
|
|
402
|
+
const pkg = read("package.json");
|
|
364
403
|
// Environment validation (zod, joi, envalid)
|
|
365
404
|
practices++;
|
|
366
405
|
if (deps.zod || deps.joi || deps.envalid || deps["@t3-oss/env-core"] || deps["@t3-oss/env-nextjs"]) {
|
|
@@ -379,6 +418,39 @@ export function runBestPractices(cwd) {
|
|
|
379
418
|
followed++;
|
|
380
419
|
}
|
|
381
420
|
}
|
|
421
|
+
return { practices, followed, issues };
|
|
422
|
+
}
|
|
423
|
+
export function runBestPractices(cwd) {
|
|
424
|
+
const start = Date.now();
|
|
425
|
+
const has = (f) => existsSync(join(cwd, f));
|
|
426
|
+
const read = (f) => {
|
|
427
|
+
try {
|
|
428
|
+
return readFileSync(join(cwd, f), "utf-8");
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return "";
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
const categories = [
|
|
435
|
+
checkCICD(cwd, has, read),
|
|
436
|
+
checkSupplyChain(has, read),
|
|
437
|
+
checkRepoHygiene(has),
|
|
438
|
+
checkDevExperience(cwd, has),
|
|
439
|
+
checkCodeQualityTooling(has, read),
|
|
440
|
+
checkTesting(has, read),
|
|
441
|
+
checkDocker(has, read),
|
|
442
|
+
checkGitPractices(cwd, has, read),
|
|
443
|
+
checkMonitoring(cwd),
|
|
444
|
+
checkAPIConfig(cwd, read),
|
|
445
|
+
];
|
|
446
|
+
let practices = 0;
|
|
447
|
+
let followed = 0;
|
|
448
|
+
const issues = [];
|
|
449
|
+
for (const cat of categories) {
|
|
450
|
+
practices += cat.practices;
|
|
451
|
+
followed += cat.followed;
|
|
452
|
+
issues.push(...cat.issues);
|
|
453
|
+
}
|
|
382
454
|
// ── Score ──
|
|
383
455
|
const pct = practices > 0 ? Math.round((followed / practices) * 100) : 100;
|
|
384
456
|
const score = pct;
|
|
@@ -28,7 +28,7 @@ const CODE_SMELLS = [
|
|
|
28
28
|
message: "dangerouslySetInnerHTML bypasses React's XSS protection",
|
|
29
29
|
},
|
|
30
30
|
{ name: "document.write", pattern: /document\.write\s*\(/, severity: "error", message: "document.write blocks rendering" },
|
|
31
|
-
{ name: "http:// URL", pattern: /['"]http:\/\/(?!localhost|127\.0\.0\.1)/, severity: "warning", message: "Non-HTTPS URL — use https://" },
|
|
31
|
+
{ name: "http:// URL", pattern: /['"]http:\/\/(?!localhost|127\.0\.0\.1|www\.w3\.org|schemas?\.)/, severity: "warning", message: "Non-HTTPS URL — use https://" },
|
|
32
32
|
{ name: "TODO/FIXME", pattern: /\b(TODO|FIXME|HACK|XXX)\b/, severity: "warning", message: "Unresolved TODO/FIXME comment" },
|
|
33
33
|
{
|
|
34
34
|
name: "magic number",
|
|
@@ -96,6 +96,9 @@ export function runStandards(cwd, stack) {
|
|
|
96
96
|
continue;
|
|
97
97
|
if (/\bpattern\s*:|name:\s*["']|message:\s*["']|description:\s*["']|risk:\s*["']|recommendation:\s*["']/.test(trimmed))
|
|
98
98
|
continue;
|
|
99
|
+
// Skip string-only lines (check-meta descriptions, inline scripts)
|
|
100
|
+
if (/^\s*["'`].*["'`][,;]?\s*$/.test(line))
|
|
101
|
+
continue;
|
|
99
102
|
for (const check of CODE_SMELLS) {
|
|
100
103
|
// Skip console.log in CLI entry points (intentional output)
|
|
101
104
|
if (check.name === "console.log" && (f.path.includes("cli.") || f.path.includes("bin/")))
|