@vibecodeqa/cli 0.9.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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/dist/check-meta.d.ts +15 -0
  4. package/dist/check-meta.js +166 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.js +140 -0
  7. package/dist/detect.d.ts +8 -0
  8. package/dist/detect.js +67 -0
  9. package/dist/fs-utils.d.ts +23 -0
  10. package/dist/fs-utils.js +77 -0
  11. package/dist/report/html.d.ts +12 -0
  12. package/dist/report/html.js +400 -0
  13. package/dist/runners/architecture.d.ts +28 -0
  14. package/dist/runners/architecture.js +272 -0
  15. package/dist/runners/complexity.d.ts +3 -0
  16. package/dist/runners/complexity.js +152 -0
  17. package/dist/runners/confusion.d.ts +16 -0
  18. package/dist/runners/confusion.js +198 -0
  19. package/dist/runners/context.d.ts +15 -0
  20. package/dist/runners/context.js +200 -0
  21. package/dist/runners/coverage.d.ts +3 -0
  22. package/dist/runners/coverage.js +65 -0
  23. package/dist/runners/dependencies.d.ts +3 -0
  24. package/dist/runners/dependencies.js +106 -0
  25. package/dist/runners/docs.d.ts +3 -0
  26. package/dist/runners/docs.js +97 -0
  27. package/dist/runners/duplication.d.ts +3 -0
  28. package/dist/runners/duplication.js +100 -0
  29. package/dist/runners/exec.d.ts +6 -0
  30. package/dist/runners/exec.js +25 -0
  31. package/dist/runners/lint.d.ts +3 -0
  32. package/dist/runners/lint.js +78 -0
  33. package/dist/runners/secrets.d.ts +3 -0
  34. package/dist/runners/secrets.js +108 -0
  35. package/dist/runners/security.d.ts +3 -0
  36. package/dist/runners/security.js +121 -0
  37. package/dist/runners/standards.d.ts +3 -0
  38. package/dist/runners/standards.js +153 -0
  39. package/dist/runners/structure.d.ts +3 -0
  40. package/dist/runners/structure.js +110 -0
  41. package/dist/runners/testing.d.ts +12 -0
  42. package/dist/runners/testing.js +401 -0
  43. package/dist/runners/tests.d.ts +3 -0
  44. package/dist/runners/tests.js +54 -0
  45. package/dist/runners/type-safety.d.ts +3 -0
  46. package/dist/runners/type-safety.js +74 -0
  47. package/dist/runners/types-check.d.ts +3 -0
  48. package/dist/runners/types-check.js +44 -0
  49. package/dist/score.d.ts +6 -0
  50. package/dist/score.js +19 -0
  51. package/dist/trend.d.ts +19 -0
  52. package/dist/trend.js +63 -0
  53. package/dist/types.d.ts +40 -0
  54. package/dist/types.js +12 -0
  55. package/package.json +53 -0
@@ -0,0 +1,78 @@
1
+ /** Lint check — auto-detects biome or eslint. */
2
+ import { gradeFromScore } from "../types.js";
3
+ import { run } from "./exec.js";
4
+ export function runLint(cwd, stack) {
5
+ const start = Date.now();
6
+ const issues = [];
7
+ if (stack.linter === "biome") {
8
+ const { stdout } = run("npx biome check src/ --reporter=json 2>/dev/null || true", cwd);
9
+ try {
10
+ const data = JSON.parse(stdout);
11
+ const diagnostics = data.diagnostics || [];
12
+ for (const d of diagnostics) {
13
+ issues.push({
14
+ severity: d.severity === "error"
15
+ ? "error"
16
+ : d.severity === "warning"
17
+ ? "warning"
18
+ : "info",
19
+ message: d.description || d.message || "lint issue",
20
+ file: d.location?.path,
21
+ line: d.location?.span?.start?.line,
22
+ rule: d.category,
23
+ });
24
+ }
25
+ }
26
+ catch {
27
+ // biome may not output valid JSON on some errors — count from summary
28
+ const errors = stdout.match(/Found (\d+) error/)?.[1] || "0";
29
+ const warnings = stdout.match(/Found (\d+) warning/)?.[1] || "0";
30
+ for (let i = 0; i < parseInt(errors); i++)
31
+ issues.push({ severity: "error", message: "lint error" });
32
+ for (let i = 0; i < parseInt(warnings); i++)
33
+ issues.push({ severity: "warning", message: "lint warning" });
34
+ }
35
+ }
36
+ else if (stack.linter === "eslint") {
37
+ const { stdout } = run("npx eslint src/ --format json 2>/dev/null || true", cwd);
38
+ try {
39
+ const files = JSON.parse(stdout);
40
+ for (const file of files) {
41
+ for (const msg of file.messages || []) {
42
+ issues.push({
43
+ severity: msg.severity === 2 ? "error" : "warning",
44
+ message: msg.message,
45
+ file: file.filePath,
46
+ line: msg.line,
47
+ rule: msg.ruleId,
48
+ });
49
+ }
50
+ }
51
+ }
52
+ catch {
53
+ /* eslint output parse failed */
54
+ }
55
+ }
56
+ else {
57
+ return {
58
+ name: "lint",
59
+ score: 0,
60
+ grade: "F",
61
+ details: { skipped: true, reason: "no linter detected" },
62
+ issues: [],
63
+ duration: Date.now() - start,
64
+ };
65
+ }
66
+ const errors = issues.filter((i) => i.severity === "error").length;
67
+ const warnings = issues.filter((i) => i.severity === "warning").length;
68
+ // Score: start at 100, -10 per error, -2 per warning, floor at 0
69
+ const score = Math.max(0, Math.min(100, 100 - errors * 10 - warnings * 2));
70
+ return {
71
+ name: "lint",
72
+ score,
73
+ grade: gradeFromScore(score),
74
+ details: { errors, warnings, linter: stack.linter },
75
+ issues,
76
+ duration: Date.now() - start,
77
+ };
78
+ }
@@ -0,0 +1,3 @@
1
+ /** Secret detection — scans for hardcoded keys/tokens in source files. */
2
+ import type { CheckResult } from "../types.js";
3
+ export declare function runSecrets(cwd: string): CheckResult;
@@ -0,0 +1,108 @@
1
+ /** Secret detection — scans for hardcoded keys/tokens in source files. */
2
+ import { readdirSync, readFileSync, statSync } from "node:fs";
3
+ import { extname, join } from "node:path";
4
+ import { gradeFromScore } from "../types.js";
5
+ const SECRET_PATTERNS = [
6
+ { name: "AWS Access Key", pattern: /AKIA[0-9A-Z]{16}/ },
7
+ {
8
+ name: "AWS Secret Key",
9
+ pattern: /(?:aws_secret|AWS_SECRET)[^=]*=\s*['"][A-Za-z0-9/+=]{40}['"]/,
10
+ },
11
+ { name: "GitHub Token (classic)", pattern: /ghp_[A-Za-z0-9]{36}/ },
12
+ {
13
+ name: "GitHub Token (fine-grained)",
14
+ pattern: /github_pat_[A-Za-z0-9_]{22,}/,
15
+ },
16
+ { name: "GitHub OAuth", pattern: /gho_[A-Za-z0-9]{36}/ },
17
+ { name: "Slack Token", pattern: /xox[bpors]-[0-9a-zA-Z-]{10,}/ },
18
+ { name: "Stripe Secret Key", pattern: /sk_live_[0-9a-zA-Z]{24,}/ },
19
+ { name: "Stripe Publishable Key", pattern: /pk_live_[0-9a-zA-Z]{24,}/ },
20
+ {
21
+ name: "OpenAI API Key",
22
+ pattern: /sk-[A-Za-z0-9]{20,}T3BlbkFJ[A-Za-z0-9]{20,}/,
23
+ },
24
+ { name: "Anthropic API Key", pattern: /sk-ant-api\d{2}-[A-Za-z0-9-]{80,}/ },
25
+ { name: "Google API Key", pattern: /AIza[0-9A-Za-z_-]{35}/ },
26
+ {
27
+ name: "Private Key",
28
+ pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/,
29
+ },
30
+ {
31
+ name: "Generic Secret Assignment",
32
+ pattern: /(?:password|secret|api_key|apikey|token|auth)\s*[:=]\s*['"][A-Za-z0-9+/=]{20,}['"]/,
33
+ },
34
+ ];
35
+ export function runSecrets(cwd) {
36
+ const start = Date.now();
37
+ const issues = [];
38
+ const files = [];
39
+ collectFiles(cwd, files);
40
+ for (const file of files) {
41
+ const content = readFileSync(file, "utf-8");
42
+ const relPath = file.replace(cwd + "/", "");
43
+ const lines = content.split("\n");
44
+ for (let i = 0; i < lines.length; i++) {
45
+ const line = lines[i];
46
+ // Skip comments
47
+ if (line.trim().startsWith("//") || line.trim().startsWith("*"))
48
+ continue;
49
+ // Skip test files and mock data
50
+ if (relPath.includes(".test.") || relPath.includes("__mock"))
51
+ continue;
52
+ for (const { name, pattern } of SECRET_PATTERNS) {
53
+ if (pattern.test(line)) {
54
+ issues.push({
55
+ severity: "error",
56
+ message: `Possible ${name}`,
57
+ file: relPath,
58
+ line: i + 1,
59
+ rule: "secret-detected",
60
+ });
61
+ }
62
+ }
63
+ }
64
+ }
65
+ const score = issues.length === 0 ? 100 : Math.max(0, 100 - issues.length * 25);
66
+ return {
67
+ name: "secrets",
68
+ score,
69
+ grade: gradeFromScore(score),
70
+ details: { secretsFound: issues.length },
71
+ issues,
72
+ duration: Date.now() - start,
73
+ };
74
+ }
75
+ function collectFiles(dir, out) {
76
+ for (const entry of readdirSync(dir)) {
77
+ if ([
78
+ "node_modules",
79
+ "dist",
80
+ ".git",
81
+ ".vibe-check",
82
+ "coverage",
83
+ "test-results",
84
+ ].includes(entry))
85
+ continue;
86
+ const full = join(dir, entry);
87
+ const stat = statSync(full);
88
+ if (stat.isDirectory()) {
89
+ collectFiles(full, out);
90
+ }
91
+ else {
92
+ const ext = extname(entry);
93
+ if ([
94
+ ".ts",
95
+ ".tsx",
96
+ ".js",
97
+ ".jsx",
98
+ ".json",
99
+ ".env",
100
+ ".yaml",
101
+ ".yml",
102
+ ".toml",
103
+ ].includes(ext)) {
104
+ out.push(full);
105
+ }
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,3 @@
1
+ /** Security analysis — beyond secrets, checks for vulnerable code patterns. */
2
+ import type { CheckResult } from "../types.js";
3
+ export declare function runSecurity(cwd: string): CheckResult;
@@ -0,0 +1,121 @@
1
+ /** Security analysis — beyond secrets, checks for vulnerable code patterns. */
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { extname, join } from "node:path";
4
+ import { gradeFromScore } from "../types.js";
5
+ const PATTERNS = [
6
+ // XSS
7
+ { name: "innerHTML", pattern: /\.innerHTML\s*=/, severity: "warning", message: "XSS: innerHTML assignment — use textContent or DOM APIs", cwe: "CWE-79" },
8
+ { name: "dangerouslySetInnerHTML", pattern: /dangerouslySetInnerHTML/, severity: "error", message: "XSS: dangerouslySetInnerHTML bypasses React protection", cwe: "CWE-79" },
9
+ { name: "document.write", pattern: /document\.write\s*\(/, severity: "error", message: "XSS: document.write is dangerous", cwe: "CWE-79" },
10
+ { name: "outerHTML", pattern: /\.outerHTML\s*=/, severity: "warning", message: "XSS: outerHTML assignment", cwe: "CWE-79" },
11
+ { name: "insertAdjacentHTML", pattern: /\.insertAdjacentHTML\s*\(/, severity: "warning", message: "XSS: insertAdjacentHTML with user data", cwe: "CWE-79" },
12
+ // Injection
13
+ { name: "eval", pattern: /\beval\s*\(/, severity: "error", message: "Injection: eval() executes arbitrary code", cwe: "CWE-94" },
14
+ { name: "new Function", pattern: /new\s+Function\s*\(/, severity: "error", message: "Injection: new Function() is equivalent to eval()", cwe: "CWE-94" },
15
+ { name: "child_process.exec", pattern: /\bexec(?:Sync)?\s*\((?!.*\{[^}]*encoding)/, severity: "warning", message: "Command injection risk: prefer execFile with argument array", cwe: "CWE-78" },
16
+ { name: "template literal in SQL", pattern: /(?:query|prepare|execute)\s*\(\s*`[^`]*\$\{/, severity: "error", message: "SQL injection: use parameterized queries instead of template literals", cwe: "CWE-89" },
17
+ // Crypto
18
+ { name: "Math.random for security", pattern: /Math\.random\s*\(\).*(?:token|secret|key|password|nonce|salt)/i, severity: "error", message: "Weak randomness: use crypto.randomUUID() or crypto.getRandomValues()", cwe: "CWE-330" },
19
+ { name: "MD5/SHA1", pattern: /\b(?:md5|sha1|SHA1|MD5)\b/, severity: "warning", message: "Weak hash: MD5/SHA1 are broken — use SHA-256+", cwe: "CWE-328" },
20
+ // Prototype pollution
21
+ { name: "Object.assign from user input", pattern: /Object\.assign\s*\(\s*\{\s*\}\s*,\s*(?:req|request|body|params|query)/, severity: "warning", message: "Prototype pollution risk: validate/sanitize before Object.assign", cwe: "CWE-1321" },
22
+ { name: "spread from user input", pattern: /\{\s*\.\.\.(?:req|request|body|params|query)\./, severity: "warning", message: "Prototype pollution: spreading unvalidated user input", cwe: "CWE-1321" },
23
+ // Path traversal
24
+ { name: "path traversal", pattern: /(?:readFile|writeFile|access|stat)(?:Sync)?\s*\([^)]*(?:req|request|body|params|query)/, severity: "warning", message: "Path traversal: validate file paths from user input", cwe: "CWE-22" },
25
+ // SSRF
26
+ { name: "fetch with user URL", pattern: /fetch\s*\(\s*(?:req|request|body|params|query)\.(?:url|href|target)/, severity: "warning", message: "SSRF: validate URLs before fetching user-supplied targets", cwe: "CWE-918" },
27
+ // Sensitive data
28
+ { name: "password in URL", pattern: /(?:password|secret|token|key)=[^&\s'"]+/i, severity: "warning", message: "Sensitive data in URL query string", cwe: "CWE-598" },
29
+ // Missing security headers (in response construction)
30
+ { name: "no-cache header missing", pattern: /new Response\([^)]*\{[^}]*["']Set-Cookie["']/, severity: "warning", message: "Set-Cookie without Cache-Control: no-store", cwe: "CWE-525" },
31
+ ];
32
+ export function runSecurity(cwd) {
33
+ const start = Date.now();
34
+ const issues = [];
35
+ const files = [];
36
+ const dirs = ["src", "web/src"];
37
+ for (const dir of dirs) {
38
+ try {
39
+ collectFiles(join(cwd, dir), files);
40
+ }
41
+ catch { /* dir doesn't exist */ }
42
+ }
43
+ if (files.length === 0) {
44
+ return { name: "security", score: 100, grade: "A", details: { skipped: true, reason: "no source files" }, issues: [], duration: Date.now() - start };
45
+ }
46
+ const cwePrefixes = new Set();
47
+ for (const file of files) {
48
+ const content = readFileSync(file, "utf-8");
49
+ const relPath = file.replace(cwd + "/", "");
50
+ const lines = content.split("\n");
51
+ for (let i = 0; i < lines.length; i++) {
52
+ const line = lines[i];
53
+ const trimmed = line.trim();
54
+ if (trimmed.startsWith("//") || trimmed.startsWith("*"))
55
+ continue;
56
+ for (const p of PATTERNS) {
57
+ if (p.pattern.test(line)) {
58
+ issues.push({
59
+ severity: p.severity,
60
+ message: p.message,
61
+ file: relPath,
62
+ line: i + 1,
63
+ rule: p.cwe || p.name,
64
+ });
65
+ if (p.cwe)
66
+ cwePrefixes.add(p.cwe);
67
+ }
68
+ }
69
+ }
70
+ }
71
+ // Check for security-critical HTML files
72
+ const htmlFiles = ["index.html", "web/index.html", "public/index.html"];
73
+ for (const h of htmlFiles) {
74
+ const full = join(cwd, h);
75
+ if (!existsSync(full))
76
+ continue;
77
+ const html = readFileSync(full, "utf-8");
78
+ // Missing CSP
79
+ if (!html.includes("Content-Security-Policy") && !html.includes("content-security-policy")) {
80
+ issues.push({ severity: "info", message: "No Content-Security-Policy meta tag in HTML", file: h, rule: "CWE-1021" });
81
+ }
82
+ // External scripts without integrity
83
+ const scripts = html.match(/<script[^>]*src=["'][^"']*["'][^>]*>/g) || [];
84
+ for (const s of scripts) {
85
+ if (s.includes("integrity="))
86
+ continue;
87
+ if (s.includes("localhost") || s.includes("/src/"))
88
+ continue; // local dev scripts
89
+ if (!s.includes("integrity")) {
90
+ issues.push({ severity: "info", message: "External script without subresource integrity (SRI)", file: h, rule: "CWE-829" });
91
+ }
92
+ }
93
+ }
94
+ const errors = issues.filter((i) => i.severity === "error").length;
95
+ const warnings = issues.filter((i) => i.severity === "warning").length;
96
+ const score = Math.max(0, Math.min(100, 100 - errors * 15 - warnings * 5));
97
+ return {
98
+ name: "security",
99
+ score,
100
+ grade: gradeFromScore(score),
101
+ details: { filesScanned: files.length, patterns: issues.length, cweCategories: cwePrefixes.size, errors, warnings },
102
+ issues,
103
+ duration: Date.now() - start,
104
+ };
105
+ }
106
+ function collectFiles(dir, out) {
107
+ for (const entry of readdirSync(dir)) {
108
+ if (["node_modules", "dist", ".git", ".vibe-check", "coverage", "test-results"].includes(entry))
109
+ continue;
110
+ const full = join(dir, entry);
111
+ if (statSync(full).isDirectory()) {
112
+ collectFiles(full, out);
113
+ }
114
+ else {
115
+ const ext = extname(entry);
116
+ if ([".ts", ".tsx", ".js", ".jsx"].includes(ext) && !entry.includes(".test.") && !entry.includes(".spec.")) {
117
+ out.push(full);
118
+ }
119
+ }
120
+ }
121
+ }
@@ -0,0 +1,3 @@
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;
@@ -0,0 +1,153 @@
1
+ /** Code standards check — naming conventions, anti-patterns, config hygiene. */
2
+ import { readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { basename, extname, join } from "node:path";
4
+ import { gradeFromScore } from "../types.js";
5
+ const CODE_SMELLS = [
6
+ { name: "console.log", pattern: /\bconsole\.(log|debug|info)\s*\(/, severity: "warning", message: "console.log in production code", exclude: /\/\/ ?ok|eslint-disable|biome-ignore/ },
7
+ { name: "var keyword", pattern: /\bvar\s+\w/, severity: "error", message: "Use const/let instead of var" },
8
+ { name: "loose equality", pattern: /[^!=]==[^=]/, severity: "warning", message: "Use === instead of ==", exclude: /['"]use strict['"]/ },
9
+ { name: "eval()", pattern: /\beval\s*\(/, severity: "error", message: "eval() is a security risk — never use it" },
10
+ { name: "new Function()", pattern: /new\s+Function\s*\(/, severity: "error", message: "new Function() is equivalent to eval()" },
11
+ { name: "innerHTML assignment", pattern: /\.innerHTML\s*=/, severity: "warning", message: "innerHTML is an XSS vector — use textContent or DOM APIs" },
12
+ { name: "dangerouslySetInnerHTML", pattern: /dangerouslySetInnerHTML/, severity: "error", message: "dangerouslySetInnerHTML bypasses React's XSS protection" },
13
+ { name: "document.write", pattern: /document\.write\s*\(/, severity: "error", message: "document.write blocks rendering" },
14
+ { name: "http:// URL", pattern: /['"]http:\/\/(?!localhost|127\.0\.0\.1)/, severity: "warning", message: "Non-HTTPS URL — use https://" },
15
+ { name: "TODO/FIXME", pattern: /\b(TODO|FIXME|HACK|XXX)\b/, severity: "warning", message: "Unresolved TODO/FIXME comment" },
16
+ { name: "magic number", pattern: /(?:timeout|delay|interval|limit|max|min)\s*[:=]\s*\d{4,}(?!\d)/, severity: "warning", message: "Large magic number — consider a named constant" },
17
+ ];
18
+ export function runStandards(cwd, stack) {
19
+ const start = Date.now();
20
+ const issues = [];
21
+ // Collect source files
22
+ const files = [];
23
+ const dirs = ["src", "web/src"];
24
+ for (const dir of dirs) {
25
+ try {
26
+ collectFiles(join(cwd, dir), cwd, files);
27
+ }
28
+ catch { /* dir doesn't exist */ }
29
+ }
30
+ // ── File naming conventions ──
31
+ let namingViolations = 0;
32
+ for (const f of files) {
33
+ const name = basename(f.path);
34
+ const ext = extname(name);
35
+ const base = name.replace(ext, "");
36
+ // React components should be PascalCase
37
+ if ((ext === ".tsx" || ext === ".jsx") && /^[A-Z]/.test(base)) {
38
+ // PascalCase component file — correct
39
+ }
40
+ else if ((ext === ".tsx" || ext === ".jsx") && /^[a-z]/.test(base) && base !== "main" && base !== "index") {
41
+ // lowercase tsx file that's not main/index — check if it exports a component
42
+ if (/export (default )?(function|const) [A-Z]/.test(f.content)) {
43
+ namingViolations++;
44
+ issues.push({ severity: "warning", message: `Component file should be PascalCase: ${name}`, file: f.path, rule: "file-naming" });
45
+ }
46
+ }
47
+ // Non-component TS files should be kebab-case or camelCase
48
+ if (ext === ".ts" && /[A-Z]/.test(base) && base !== "App" && !base.includes(".")) {
49
+ // PascalCase .ts file (not a component) — unusual
50
+ // Only flag if it's not a class file
51
+ if (!/export (default )?class /.test(f.content)) {
52
+ issues.push({ severity: "warning", message: `TS file uses PascalCase but doesn't export a class: ${name}`, file: f.path, rule: "file-naming" });
53
+ }
54
+ }
55
+ }
56
+ // ── Large files ──
57
+ let largeFiles = 0;
58
+ for (const f of files) {
59
+ const lines = f.content.split("\n").length;
60
+ if (lines > 300) {
61
+ largeFiles++;
62
+ issues.push({ severity: "warning", message: `${lines} lines — consider splitting (max 300)`, file: f.path, rule: "large-file" });
63
+ }
64
+ else if (lines > 200) {
65
+ issues.push({ severity: "warning", message: `${lines} lines — getting large`, file: f.path, rule: "large-file" });
66
+ }
67
+ }
68
+ // ── Code smell patterns ──
69
+ let smellCount = 0;
70
+ for (const f of files) {
71
+ const lines = f.content.split("\n");
72
+ for (let i = 0; i < lines.length; i++) {
73
+ const line = lines[i];
74
+ if (line.trim().startsWith("//") || line.trim().startsWith("*"))
75
+ continue;
76
+ for (const check of CODE_SMELLS) {
77
+ if (check.pattern.test(line)) {
78
+ if (check.exclude && check.exclude.test(line))
79
+ continue;
80
+ smellCount++;
81
+ issues.push({ severity: check.severity, message: check.message, file: f.path, line: i + 1, rule: check.name });
82
+ }
83
+ }
84
+ }
85
+ }
86
+ // ── Config hygiene ──
87
+ // tsconfig strict mode
88
+ if (stack.language === "typescript") {
89
+ const tsconfigPaths = ["tsconfig.json", "tsconfig.app.json"];
90
+ let strictFound = false;
91
+ for (const p of tsconfigPaths) {
92
+ try {
93
+ const tsconfig = JSON.parse(readFileSync(join(cwd, p), "utf-8"));
94
+ if (tsconfig.compilerOptions?.strict === true)
95
+ strictFound = true;
96
+ }
97
+ catch { /* no tsconfig */ }
98
+ }
99
+ if (!strictFound) {
100
+ issues.push({ severity: "warning", message: "TypeScript strict mode not enabled — add \"strict\": true to tsconfig", rule: "ts-strict" });
101
+ }
102
+ }
103
+ // Tailwind: check for inline styles when TW is available
104
+ if (stack.framework === "react" && readDeps(cwd).tailwindcss) {
105
+ let inlineStyles = 0;
106
+ for (const f of files) {
107
+ if (!f.path.endsWith(".tsx"))
108
+ continue;
109
+ const matches = f.content.match(/style=\{\{/g);
110
+ if (matches)
111
+ inlineStyles += matches.length;
112
+ }
113
+ if (inlineStyles > 10) {
114
+ issues.push({ severity: "warning", message: `${inlineStyles} inline style objects in TSX — prefer Tailwind classes`, rule: "prefer-tailwind" });
115
+ }
116
+ }
117
+ const errors = issues.filter((i) => i.severity === "error").length;
118
+ const warnings = issues.filter((i) => i.severity === "warning").length;
119
+ const score = Math.max(0, Math.min(100, 100 - errors * 8 - warnings * 3 - largeFiles * 5));
120
+ return {
121
+ name: "standards",
122
+ score,
123
+ grade: gradeFromScore(score),
124
+ details: { filesScanned: files.length, codeSmells: smellCount, largeFiles, namingViolations },
125
+ issues,
126
+ duration: Date.now() - start,
127
+ };
128
+ }
129
+ function collectFiles(dir, cwd, out) {
130
+ for (const entry of readdirSync(dir)) {
131
+ if (entry === "node_modules" || entry === "dist" || entry === ".git")
132
+ continue;
133
+ const full = join(dir, entry);
134
+ if (statSync(full).isDirectory()) {
135
+ collectFiles(full, cwd, out);
136
+ }
137
+ else {
138
+ const ext = extname(entry);
139
+ if ([".ts", ".tsx", ".js", ".jsx"].includes(ext) && !entry.includes(".test.") && !entry.includes(".spec.")) {
140
+ out.push({ path: full.replace(cwd + "/", ""), content: readFileSync(full, "utf-8") });
141
+ }
142
+ }
143
+ }
144
+ }
145
+ function readDeps(cwd) {
146
+ try {
147
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
148
+ return { ...pkg.dependencies, ...pkg.devDependencies };
149
+ }
150
+ catch {
151
+ return {};
152
+ }
153
+ }
@@ -0,0 +1,3 @@
1
+ /** Project structure check — does the repo have standard files and conventions? */
2
+ import type { CheckResult, StackInfo } from "../types.js";
3
+ export declare function runStructure(cwd: string, stack: StackInfo): CheckResult;
@@ -0,0 +1,110 @@
1
+ /** Project structure check — does the repo have standard files and conventions? */
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { join, extname } from "node:path";
4
+ import { gradeFromScore } from "../types.js";
5
+ const EXPECTED_FILES = [
6
+ { name: "package.json", path: "package.json", required: true, description: "Package manifest" },
7
+ { name: "tsconfig.json", path: "tsconfig.json", required: false, description: "TypeScript configuration" },
8
+ { name: "LICENSE", path: "LICENSE", required: true, description: "Open source license" },
9
+ { name: ".gitignore", path: ".gitignore", required: true, description: "Git ignore rules" },
10
+ { name: "README.md", path: "README.md", required: false, description: "Project documentation" },
11
+ ];
12
+ export function runStructure(cwd, stack) {
13
+ const start = Date.now();
14
+ const issues = [];
15
+ const found = [];
16
+ const missing = [];
17
+ // Check standard files
18
+ for (const fc of EXPECTED_FILES) {
19
+ // tsconfig is required only for TS projects
20
+ const required = fc.name === "tsconfig.json" ? stack.language === "typescript" : fc.required;
21
+ if (existsSync(join(cwd, fc.path))) {
22
+ found.push(fc.name);
23
+ }
24
+ else {
25
+ missing.push(fc.name);
26
+ issues.push({
27
+ severity: required ? "error" : "warning",
28
+ message: `Missing ${fc.name} — ${fc.description}`,
29
+ rule: "missing-file",
30
+ });
31
+ }
32
+ }
33
+ // Check for lockfile
34
+ const hasLock = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"].some((f) => existsSync(join(cwd, f)));
35
+ if (hasLock) {
36
+ found.push("lockfile");
37
+ }
38
+ else {
39
+ issues.push({ severity: "warning", message: "No lockfile found — builds may not be reproducible", rule: "missing-lockfile" });
40
+ }
41
+ // Check for src directory
42
+ const hasSrc = existsSync(join(cwd, "src")) || existsSync(join(cwd, "web/src"));
43
+ if (!hasSrc) {
44
+ issues.push({ severity: "error", message: "No src/ directory found", rule: "no-src" });
45
+ }
46
+ // Count source vs test files
47
+ const srcFiles = [];
48
+ const testFiles = [];
49
+ collectAll(cwd, srcFiles, testFiles);
50
+ const srcCount = srcFiles.length;
51
+ const testCount = testFiles.length;
52
+ const testRatio = srcCount > 0 ? testCount / srcCount : 0;
53
+ if (testCount === 0 && srcCount > 0) {
54
+ issues.push({ severity: "error", message: `No test files found (${srcCount} source files with zero tests)`, rule: "no-tests" });
55
+ }
56
+ else if (testRatio < 0.3 && srcCount > 3) {
57
+ issues.push({ severity: "warning", message: `Low test-to-source ratio: ${testCount} tests for ${srcCount} source files (${Math.round(testRatio * 100)}%)`, rule: "low-test-ratio" });
58
+ }
59
+ // Check package.json has essential scripts
60
+ try {
61
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
62
+ const scripts = pkg.scripts || {};
63
+ if (!scripts.test)
64
+ issues.push({ severity: "warning", message: "No 'test' script in package.json", rule: "no-test-script" });
65
+ if (!scripts.build && !scripts.dev)
66
+ issues.push({ severity: "info", message: "No 'build' or 'dev' script in package.json", rule: "no-build-script" });
67
+ }
68
+ catch { /* no package.json or parse error */ }
69
+ const errors = issues.filter((i) => i.severity === "error").length;
70
+ const warnings = issues.filter((i) => i.severity === "warning").length;
71
+ const score = Math.max(0, Math.min(100, 100 - errors * 15 - warnings * 5));
72
+ return {
73
+ name: "structure",
74
+ score,
75
+ grade: gradeFromScore(score),
76
+ details: { found, missing, srcFiles: srcCount, testFiles: testCount, testRatio: Math.round(testRatio * 100) + "%" },
77
+ issues,
78
+ duration: Date.now() - start,
79
+ };
80
+ }
81
+ function collectAll(cwd, src, test) {
82
+ const dirs = ["src", "web/src"];
83
+ for (const dir of dirs) {
84
+ try {
85
+ walk(join(cwd, dir), src, test);
86
+ }
87
+ catch { /* dir doesn't exist */ }
88
+ }
89
+ }
90
+ function walk(dir, src, test) {
91
+ for (const entry of readdirSync(dir)) {
92
+ if (entry === "node_modules" || entry === "dist")
93
+ continue;
94
+ const full = join(dir, entry);
95
+ if (statSync(full).isDirectory()) {
96
+ walk(full, src, test);
97
+ }
98
+ else {
99
+ const ext = extname(entry);
100
+ if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
101
+ if (entry.includes(".test.") || entry.includes(".spec.")) {
102
+ test.push(full);
103
+ }
104
+ else {
105
+ src.push(full);
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,12 @@
1
+ /** Comprehensive testing assessment — pyramid layers, quality, coverage.
2
+ *
3
+ * Dimensions assessed:
4
+ * 1. Pyramid presence — which layers exist? (unit, integration, e2e, component)
5
+ * 2. Test execution — pass/fail from the runner
6
+ * 3. Coverage — statement, branch, function, line
7
+ * 4. File pairing — does each source file have a test file?
8
+ * 5. Test quality — naming, assertions, mocking patterns, snapshot smell
9
+ * 6. Pyramid balance — right ratio of fast-to-slow tests
10
+ */
11
+ import type { CheckResult, StackInfo } from "../types.js";
12
+ export declare function runTesting(cwd: string, stack: StackInfo, skipExec: boolean): CheckResult;