codevet-cli 1.1.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/.gitleaks.toml +91 -0
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/config.js +44 -0
- package/dist/detectors/detectStack.js +28 -0
- package/dist/fixLibrary/templates.js +311 -0
- package/dist/index.js +354 -0
- package/dist/promptConfirm.js +12 -0
- package/dist/report.js +155 -0
- package/dist/resolveTarget.js +119 -0
- package/dist/scanners/bearerScanner.js +108 -0
- package/dist/scanners/ensureBinary.js +101 -0
- package/dist/scanners/gitleaksScanner.js +109 -0
- package/dist/scanners/hygieneScanner.js +156 -0
- package/dist/scanners/npmAuditScanner.js +118 -0
- package/dist/scanners/npmFixActions.js +50 -0
- package/dist/scanners/pipAuditScanner.js +73 -0
- package/dist/scanners/pythonAuditScanner.js +66 -0
- package/package.json +57 -0
- package/scripts/generateTemplates.mjs +48 -0
- package/scripts/postPrComment.mjs +113 -0
- package/scripts/postinstall.mjs +20 -0
- package/scripts/runTests.mjs +39 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
export class PipAuditScanFailedError extends Error {
|
|
5
|
+
constructor(reason) {
|
|
6
|
+
super(`pip-audit did not actually run: ${reason}. This is being reported as a failure rather than "no vulnerabilities" — a scan that never ran is not the same as a clean result.`);
|
|
7
|
+
this.name = "PipAuditScanFailedError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class PipAuditNotInstalledError extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("pip-audit isn't installed on this machine, so the Python dependency check couldn't run. Install it with 'pip install pip-audit' — secrets, hygiene, and other checks are unaffected.");
|
|
13
|
+
this.name = "PipAuditNotInstalledError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function findRequirementsFile(projectRoot) {
|
|
17
|
+
for (const name of ["requirements.txt", "pyproject.toml"]) {
|
|
18
|
+
const path = join(projectRoot, name);
|
|
19
|
+
if (existsSync(path))
|
|
20
|
+
return path;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Runs real pip-audit against requirements.txt or pyproject.toml, checking
|
|
26
|
+
* PyPA's published advisory database (known, disclosed CVEs) — same
|
|
27
|
+
* "known issues only, not a malware/zero-day scanner" honesty as the npm
|
|
28
|
+
* audit wrapper. Returns [] (not an error) if no Python project is
|
|
29
|
+
* detected — that's a real "nothing to check" case, not a failure.
|
|
30
|
+
*/
|
|
31
|
+
export async function runPipAuditScan(projectRoot) {
|
|
32
|
+
const reqFile = findRequirementsFile(projectRoot);
|
|
33
|
+
if (!reqFile)
|
|
34
|
+
return [];
|
|
35
|
+
const isRequirementsTxt = reqFile.endsWith("requirements.txt");
|
|
36
|
+
const args = isRequirementsTxt
|
|
37
|
+
? ["-r", reqFile, "-f", "json"]
|
|
38
|
+
: [projectRoot, "-f", "json"]; // pyproject.toml: pip-audit reads the project dir directly
|
|
39
|
+
const result = await execa("pip-audit", args, { cwd: projectRoot, reject: false });
|
|
40
|
+
if (result.failed && result.exitCode === undefined) {
|
|
41
|
+
// execa couldn't even spawn the process — pip-audit isn't installed.
|
|
42
|
+
throw new PipAuditNotInstalledError();
|
|
43
|
+
}
|
|
44
|
+
let report;
|
|
45
|
+
try {
|
|
46
|
+
report = JSON.parse(result.stdout);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Confirmed in testing: a genuine pip-audit failure (e.g. an
|
|
50
|
+
// unresolvable requirement) produces NO valid JSON on stdout and
|
|
51
|
+
// writes errors to stderr instead — unlike npm audit, there's no
|
|
52
|
+
// ambiguous "valid JSON shaped like an error" case here, so any
|
|
53
|
+
// parse failure means the scan genuinely didn't complete.
|
|
54
|
+
throw new PipAuditScanFailedError(result.stderr.split("\n").find((l) => l.trim().length > 0) || "no readable output produced");
|
|
55
|
+
}
|
|
56
|
+
if (!report.dependencies)
|
|
57
|
+
return [];
|
|
58
|
+
const findings = [];
|
|
59
|
+
for (const dep of report.dependencies) {
|
|
60
|
+
if (dep.vulns.length === 0)
|
|
61
|
+
continue;
|
|
62
|
+
findings.push({
|
|
63
|
+
packageName: dep.name,
|
|
64
|
+
installedVersion: dep.version,
|
|
65
|
+
advisories: dep.vulns.map((v) => ({
|
|
66
|
+
id: v.id,
|
|
67
|
+
description: v.description.split("\n")[0].slice(0, 200),
|
|
68
|
+
fixVersions: v.fix_versions,
|
|
69
|
+
})),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return findings;
|
|
73
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
export class PipAuditNotAvailableError extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("pip-audit isn't installed, so Python dependency scanning is unavailable for this project. Install it with 'pip install pip-audit' and re-run — secrets and hygiene checks are unaffected.");
|
|
7
|
+
this.name = "PipAuditNotAvailableError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* pip-audit can be invoked directly (if on PATH) or via `python3 -m
|
|
12
|
+
* pip_audit` / `python -m pip_audit` (more portable — works whenever the
|
|
13
|
+
* package is pip-installed regardless of PATH setup, especially common
|
|
14
|
+
* on Windows). Tries each in order, returns null if none work.
|
|
15
|
+
*/
|
|
16
|
+
async function findPipAuditInvocation() {
|
|
17
|
+
const candidates = [["pip-audit"], ["python3", "-m", "pip_audit"], ["python", "-m", "pip_audit"]];
|
|
18
|
+
for (const candidate of candidates) {
|
|
19
|
+
const [cmd, ...args] = candidate;
|
|
20
|
+
const check = await execa(cmd, [...args, "--version"], { reject: false }).catch(() => null);
|
|
21
|
+
if (check?.exitCode === 0)
|
|
22
|
+
return candidate;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Only requirements.txt is supported for now — pip-audit's pyproject.toml
|
|
28
|
+
* support requires resolving against an actual environment/lockfile,
|
|
29
|
+
* which is a meaningfully different (and heavier) integration than the
|
|
30
|
+
* requirements.txt case. Documented as a known gap rather than attempted
|
|
31
|
+
* partially and silently missing edge cases.
|
|
32
|
+
*/
|
|
33
|
+
export async function runPythonAuditScan(projectRoot) {
|
|
34
|
+
const requirementsPath = join(projectRoot, "requirements.txt");
|
|
35
|
+
if (!existsSync(requirementsPath))
|
|
36
|
+
return [];
|
|
37
|
+
const invocation = await findPipAuditInvocation();
|
|
38
|
+
if (!invocation) {
|
|
39
|
+
throw new PipAuditNotAvailableError();
|
|
40
|
+
}
|
|
41
|
+
const [cmd, ...baseArgs] = invocation;
|
|
42
|
+
const result = await execa(cmd, [...baseArgs, "-r", requirementsPath, "--format", "json", "--progress-spinner", "off"], { reject: false });
|
|
43
|
+
// pip-audit exits non-zero both when vulnerabilities are found AND on a
|
|
44
|
+
// genuine internal failure — distinguish by whether stdout is valid
|
|
45
|
+
// JSON, same pattern as the bearer scan-failure fix.
|
|
46
|
+
let parsed;
|
|
47
|
+
try {
|
|
48
|
+
parsed = JSON.parse(result.stdout);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new Error(`pip-audit did not produce a valid report: ${result.stderr || result.stdout || "no output"}`);
|
|
52
|
+
}
|
|
53
|
+
const findings = [];
|
|
54
|
+
for (const dep of parsed.dependencies ?? []) {
|
|
55
|
+
for (const vuln of dep.vulns ?? []) {
|
|
56
|
+
findings.push({
|
|
57
|
+
packageName: dep.name,
|
|
58
|
+
version: dep.version,
|
|
59
|
+
vulnerabilityId: vuln.id,
|
|
60
|
+
fixVersions: vuln.fix_versions,
|
|
61
|
+
description: vuln.description.split("\n")[0].slice(0, 200),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return findings;
|
|
66
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codevet-cli",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Vet your code before it ships \u2014 a free, open-source security co-pilot that checks for leaked secrets, vulnerable dependencies, missing security middleware, and personal data flow, with real working fixes shown inline.",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc",
|
|
7
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
8
|
+
"dev": "tsx src/index.ts",
|
|
9
|
+
"test": "npm run build && node scripts/runTests.mjs",
|
|
10
|
+
"generate-templates": "node scripts/generateTemplates.mjs"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"security",
|
|
14
|
+
"security-scanner",
|
|
15
|
+
"secrets",
|
|
16
|
+
"vulnerability-scanner",
|
|
17
|
+
"sast",
|
|
18
|
+
"gitleaks",
|
|
19
|
+
"dependency-audit",
|
|
20
|
+
"cli",
|
|
21
|
+
"devsecops"
|
|
22
|
+
],
|
|
23
|
+
"author": "Anurag Aryan",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"chalk": "^6.0.0",
|
|
27
|
+
"commander": "^15.0.0",
|
|
28
|
+
"execa": "^9.6.1",
|
|
29
|
+
"fast-glob": "^3.3.3"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^26.2.0",
|
|
33
|
+
"tsx": "^4.23.12",
|
|
34
|
+
"typescript": "^7.0.2"
|
|
35
|
+
},
|
|
36
|
+
"type": "module",
|
|
37
|
+
"bin": {
|
|
38
|
+
"codevet": "dist/index.js"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
".gitleaks.toml",
|
|
43
|
+
"scripts"
|
|
44
|
+
],
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/i-akb25/codevet.git"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/i-akb25/codevet#readme",
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/i-akb25/codevet/issues"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": "^18.19.0 || >=20.5.0"
|
|
55
|
+
},
|
|
56
|
+
"main": "dist/index.js"
|
|
57
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Regenerates the human-browsable templates/ folder from
|
|
2
|
+
// src/fixLibrary/templates.ts — the single source of truth. Run via
|
|
3
|
+
// `npm run generate-templates`. Never hand-edit files under templates/
|
|
4
|
+
// directly; edit templates.ts and regenerate.
|
|
5
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import {
|
|
9
|
+
HELMET_CONFIG,
|
|
10
|
+
CORS_CONFIG,
|
|
11
|
+
RATE_LIMITER,
|
|
12
|
+
ACCOUNT_BACKOFF,
|
|
13
|
+
ERROR_HANDLER,
|
|
14
|
+
HASH_PASSWORD,
|
|
15
|
+
JWT_TOKEN,
|
|
16
|
+
VALIDATION_SCHEMA,
|
|
17
|
+
ENV_EXAMPLE,
|
|
18
|
+
SECURITY_CHECK_WORKFLOW,
|
|
19
|
+
PRE_COMMIT_HOOK,
|
|
20
|
+
FILE_UPLOAD,
|
|
21
|
+
RLS_ENABLE,
|
|
22
|
+
} from "../dist/fixLibrary/templates.js";
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const ROOT = join(__dirname, "..", "templates");
|
|
26
|
+
|
|
27
|
+
const FILES = [
|
|
28
|
+
["security/helmet.config.js", HELMET_CONFIG],
|
|
29
|
+
["security/cors.config.js", CORS_CONFIG],
|
|
30
|
+
["security/rateLimiter.middleware.js", RATE_LIMITER],
|
|
31
|
+
["security/accountBackoff.js", ACCOUNT_BACKOFF],
|
|
32
|
+
["security/errorHandler.middleware.js", ERROR_HANDLER],
|
|
33
|
+
["security/auth/hashPassword.js", HASH_PASSWORD],
|
|
34
|
+
["security/auth/generateToken.js", JWT_TOKEN],
|
|
35
|
+
["security/validation/authSchemas.js", VALIDATION_SCHEMA],
|
|
36
|
+
[".env.example", ENV_EXAMPLE],
|
|
37
|
+
[".github/workflows/security-check.yml", SECURITY_CHECK_WORKFLOW],
|
|
38
|
+
[".husky/pre-commit", PRE_COMMIT_HOOK],
|
|
39
|
+
["security/fileUpload.middleware.js", FILE_UPLOAD],
|
|
40
|
+
["enable-rls.sql", RLS_ENABLE],
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
for (const [relativePath, content] of FILES) {
|
|
44
|
+
const fullPath = join(ROOT, relativePath);
|
|
45
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
46
|
+
await writeFile(fullPath, content);
|
|
47
|
+
console.log(`wrote templates/${relativePath}`);
|
|
48
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Runs inside GitHub Actions after `codevet scan --json`. Formats the
|
|
2
|
+
// report as a Markdown comment and posts it to the PR via the GitHub REST
|
|
3
|
+
// API using fetch() — no @actions/github or @actions/core dependency,
|
|
4
|
+
// since this only needs one API call and pulling in the full toolkit
|
|
5
|
+
// isn't justified for that (matches the "every dependency must justify
|
|
6
|
+
// itself" rule).
|
|
7
|
+
//
|
|
8
|
+
// Required env vars (all provided automatically inside a GitHub Actions
|
|
9
|
+
// PR-triggered workflow, except CODEVET_REPORT_PATH which the workflow sets):
|
|
10
|
+
// GITHUB_TOKEN - auto-provided by Actions
|
|
11
|
+
// GITHUB_REPOSITORY - "owner/repo", auto-provided
|
|
12
|
+
// GITHUB_EVENT_PATH - path to the event payload JSON, auto-provided
|
|
13
|
+
// CODEVET_REPORT_PATH - path to the JSON report written by `codevet scan --json`
|
|
14
|
+
|
|
15
|
+
import { readFile } from "node:fs/promises";
|
|
16
|
+
|
|
17
|
+
function severityEmoji(severity) {
|
|
18
|
+
if (severity === "critical" || severity === "high") return "🔴";
|
|
19
|
+
if (severity === "moderate") return "🟠";
|
|
20
|
+
return "🟡";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatComment(report) {
|
|
24
|
+
const lines = ["## CodeVet security scan", ""];
|
|
25
|
+
|
|
26
|
+
if (report.secrets.length === 0 && report.dependencies.length === 0 && report.hygiene.length === 0) {
|
|
27
|
+
lines.push("✔ No issues found by CodeVet's current checks.");
|
|
28
|
+
return lines.join("\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (report.secrets.length > 0) {
|
|
32
|
+
lines.push(`### 🔴 ${report.secrets.length} exposed secret(s)`, "");
|
|
33
|
+
for (const s of report.secrets) {
|
|
34
|
+
lines.push(`- **${s.file}:${s.line}** — ${s.description}`);
|
|
35
|
+
}
|
|
36
|
+
lines.push("");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (report.dependencies.length > 0) {
|
|
40
|
+
lines.push(`### Dependency vulnerabilities`, "");
|
|
41
|
+
for (const d of report.dependencies) {
|
|
42
|
+
lines.push(
|
|
43
|
+
`- ${severityEmoji(d.severity)} **${d.packageName}** (${d.severity}${d.isDirect ? "" : ", transitive"}) — ${d.advisories[0].title}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
lines.push("");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (report.hygiene.length > 0) {
|
|
50
|
+
lines.push(`### Missing security middleware`, "");
|
|
51
|
+
for (const h of report.hygiene) {
|
|
52
|
+
const marker = h.confidence === "high" ? "🔴" : "🟡 (advisory)";
|
|
53
|
+
lines.push(`- ${marker} **${h.title}** — ${h.description}`);
|
|
54
|
+
}
|
|
55
|
+
lines.push("");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
lines.push(
|
|
59
|
+
"---",
|
|
60
|
+
"_Run `npx codevet scan` locally for full details and suggested fix code. This comment is generated automatically on every push to this PR — see [CodeVet](https://github.com/codevet/codevet)._",
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return lines.join("\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function main() {
|
|
67
|
+
const token = process.env.GITHUB_TOKEN;
|
|
68
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
69
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
70
|
+
const reportPath = process.env.CODEVET_REPORT_PATH;
|
|
71
|
+
|
|
72
|
+
if (!token || !repo || !eventPath || !reportPath) {
|
|
73
|
+
console.error(
|
|
74
|
+
"[codevet-pr-comment] Missing required env var(s) — this script is meant to run inside a GitHub Actions PR workflow.",
|
|
75
|
+
);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const event = JSON.parse(await readFile(eventPath, "utf-8"));
|
|
80
|
+
const prNumber = event.pull_request?.number ?? event.number;
|
|
81
|
+
if (!prNumber) {
|
|
82
|
+
console.log("[codevet-pr-comment] Not a pull request event — skipping comment.");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const report = JSON.parse(await readFile(reportPath, "utf-8"));
|
|
87
|
+
const body = formatComment(report);
|
|
88
|
+
|
|
89
|
+
const response = await fetch(
|
|
90
|
+
`https://api.github.com/repos/${repo}/issues/${prNumber}/comments`,
|
|
91
|
+
{
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: {
|
|
94
|
+
Authorization: `Bearer ${token}`,
|
|
95
|
+
Accept: "application/vnd.github+json",
|
|
96
|
+
"Content-Type": "application/json",
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify({ body }),
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
console.error(`[codevet-pr-comment] Failed to post comment: ${response.status} ${await response.text()}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log("[codevet-pr-comment] Comment posted successfully.");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main().catch((err) => {
|
|
111
|
+
console.error("[codevet-pr-comment] Error:", err);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Best-effort eager download at install time — NOT the only safety net
|
|
2
|
+
// anymore. Each scanner now calls ensureBinary() itself at runtime and
|
|
3
|
+
// self-heals if this never ran or was blocked (confirmed real: npm's
|
|
4
|
+
// "allowScripts" feature silently blocks this exact script by default on
|
|
5
|
+
// many machines). This means postinstall failing, being skipped, or
|
|
6
|
+
// simply not existing yet (e.g. a contributor's fresh clone before their
|
|
7
|
+
// first `npm run build`) is no longer a hard failure for the tool overall.
|
|
8
|
+
try {
|
|
9
|
+
const { ensureBinary } = await import("../dist/scanners/ensureBinary.js");
|
|
10
|
+
await ensureBinary("gitleaks");
|
|
11
|
+
await ensureBinary("bearer");
|
|
12
|
+
} catch (err) {
|
|
13
|
+
// dist/ may not exist yet (fresh clone, pre-build) or the download may
|
|
14
|
+
// have failed/been blocked — either way, this is fine. The real
|
|
15
|
+
// guarantee now lives in ensureBinary() being called again at the
|
|
16
|
+
// moment a scan actually needs the binary.
|
|
17
|
+
console.log(
|
|
18
|
+
"[codevet] Skipped eager binary download at install time (this is fine — binaries download automatically on first use instead).",
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Runs the test suite by explicitly enumerating *.test.ts files via
|
|
2
|
+
// Node's fs module, then passing that explicit file list to `node --test`.
|
|
3
|
+
//
|
|
4
|
+
// Why this exists: `node --test tests/*.test.ts` relies on the SHELL to
|
|
5
|
+
// expand the glob before node ever sees it. bash does this automatically
|
|
6
|
+
// (confirmed working in CI on ubuntu/macos), but PowerShell/cmd.exe do
|
|
7
|
+
// NOT expand globs for arguments passed to a child process — Windows CI
|
|
8
|
+
// failed with "Could not find 'tests/*.test.ts'" because node received
|
|
9
|
+
// the literal, unexpanded string. Node's own --test directory-scanning
|
|
10
|
+
// was tried as an alternative and confirmed NOT to reliably discover
|
|
11
|
+
// .test.ts files under the tsx loader either (found 1 instead of the
|
|
12
|
+
// real 17 test cases). Enumerating via fs.readdirSync sidesteps both
|
|
13
|
+
// problems — it's just JavaScript, identical behavior on every OS.
|
|
14
|
+
import { readdirSync } from "node:fs";
|
|
15
|
+
import { join, dirname } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const testsDir = join(__dirname, "..", "tests");
|
|
21
|
+
|
|
22
|
+
const testFiles = readdirSync(testsDir)
|
|
23
|
+
.filter((f) => f.endsWith(".test.ts"))
|
|
24
|
+
.map((f) => join(testsDir, f));
|
|
25
|
+
|
|
26
|
+
if (testFiles.length === 0) {
|
|
27
|
+
console.error("[codevet] no *.test.ts files found in tests/ — something is wrong.");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const child = spawn(
|
|
32
|
+
process.execPath,
|
|
33
|
+
["--import", "tsx", "--test", ...testFiles],
|
|
34
|
+
{ stdio: "inherit" },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
child.on("exit", (code) => {
|
|
38
|
+
process.exit(code ?? 1);
|
|
39
|
+
});
|