@stacknyu/pi-check 0.0.0-stage → 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.
@@ -0,0 +1,445 @@
1
+ // TypeScript / JavaScript: typecheck (tsc / vue-tsc) per project, lint (eslint / oxlint) per file,
2
+ // tests (vitest / jest) per test file. Inputs come from a static import graph.
3
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { kindOptions, rootLockfiles } from "../repo.mjs";
6
+ import { cleanEnv, baseOf, dirOf, findBin, isUnder, joinRel, loadJsonc, matchAny, normRel, parseDuration, parseJsonc, runProc, sha } from "../util.mjs";
7
+
8
+ const SRC_EXT = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".vue", ".svelte"];
9
+ const LINT_EXT = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|vue)$/;
10
+ const TEST_FILE = /(\.(test|spec)\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$)|(\/__tests__\/.+\.(ts|tsx|js|jsx|mjs|cjs)$)/;
11
+ const DEFAULT_TEST_EXCLUDE = ["**/e2e/**", "**/playwright/**", "**/*.e2e.*", "**/cypress/**", "**/node_modules/**", "**/dist/**"];
12
+ /** Drop runner-internal stack frames and shorten paths, so the failure itself is what the agent reads. */
13
+ function trimStack(text, root) {
14
+ return String(text ?? "")
15
+ .split("\n")
16
+ .filter((l) => !/node_modules\/|node:internal|\(internal\//.test(l))
17
+ .join("\n")
18
+ .split(`${root}/`)
19
+ .join("");
20
+ }
21
+ const isSrc = (f) => SRC_EXT.some((e) => f.endsWith(e)) || f.endsWith(".json") || f.endsWith(".css") || f.endsWith(".scss");
22
+
23
+ function readPkg(rc, dir) {
24
+ const t = rc.text(joinRel(dir, "package.json"));
25
+ try {
26
+ return JSON.parse(t);
27
+ } catch {
28
+ return undefined;
29
+ }
30
+ }
31
+
32
+ /** tsconfig chain (local files only) and merged compilerOptions. */
33
+ function tsconfigInfo(rc, file) {
34
+ const chain = [];
35
+ let opts = {};
36
+ let raw;
37
+ let cur = file;
38
+ for (let i = 0; i < 8 && cur && rc.has(cur); i++) {
39
+ const j = parseJsonc(rc.text(cur)) ?? {};
40
+ if (i === 0) raw = j;
41
+ chain.push(cur);
42
+ const co = j.compilerOptions ?? {};
43
+ // earlier (child) options win; record the dir that defined paths/baseUrl
44
+ const merged = { ...co, ...opts };
45
+ if (co.paths && !opts.paths) merged.__pathsDir = dirOf(cur);
46
+ if (co.baseUrl && !opts.baseUrl) merged.__baseDir = dirOf(cur);
47
+ opts = merged;
48
+ const ext = Array.isArray(j.extends) ? j.extends[0] : j.extends;
49
+ if (!ext || !ext.startsWith(".")) break;
50
+ let next = normRel(joinRel(dirOf(cur), ext));
51
+ if (!next.endsWith(".json")) next += ".json";
52
+ cur = next;
53
+ }
54
+ return { chain, opts, raw: raw ?? {} };
55
+ }
56
+
57
+ function importSpecs(text) {
58
+ const out = new Set();
59
+ let dynamicGlob = false;
60
+ for (const re of [/\bfrom\s+["']([^"']+)["']/g, /\bimport\s+["']([^"']+)["']/g, /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g, /\brequire\s*\(\s*["']([^"']+)["']\s*\)/g, /\b(?:vi|jest)\.(?:mock|requireActual|importActual)\(\s*["']([^"']+)["']/g, /@import\s+(?:url\()?["']([^"']+)["']/g]) {
61
+ for (const m of text.matchAll(re)) out.add(m[1]);
62
+ }
63
+ if (/import\.meta\.glob|require\.context/.test(text)) dynamicGlob = true;
64
+ return { specs: [...out], dynamicGlob };
65
+ }
66
+
67
+ function workspace(rc) {
68
+ let w = rc.memo.get("ts:ws");
69
+ if (w) return w;
70
+ const byName = new Map();
71
+ const dirs = [];
72
+ for (const f of rc.listing().files) {
73
+ if (baseOf(f) !== "package.json") continue;
74
+ const d = dirOf(f);
75
+ dirs.push(d);
76
+ const pj = readPkg(rc, d);
77
+ if (pj?.name) byName.set(pj.name, d);
78
+ }
79
+ w = { byName, dirs };
80
+ rc.memo.set("ts:ws", w);
81
+ return w;
82
+ }
83
+
84
+ /** Files that belong to a project dir, excluding nested package dirs. */
85
+ function projectFiles(rc, dir) {
86
+ const key = `ts:files:${dir}`;
87
+ let v = rc.memo.get(key);
88
+ if (v) return v;
89
+ const nested = workspace(rc).dirs.filter((d) => d !== dir && isUnder(d, dir));
90
+ v = rc.filesUnder(dir).filter((f) => !nested.some((n) => isUnder(f, n)));
91
+ rc.memo.set(key, v);
92
+ return v;
93
+ }
94
+
95
+ class Resolver {
96
+ constructor(rc, project) {
97
+ this.rc = rc;
98
+ this.project = project;
99
+ const tsc = tsconfigInfo(rc, joinRel(project.dir, "tsconfig.json"));
100
+ this.tsconfigChain = tsc.chain;
101
+ this.paths = tsc.opts.paths ?? {};
102
+ this.pathsDir = tsc.opts.__pathsDir ?? project.dir;
103
+ this.baseDir = tsc.opts.baseUrl ? normRel(joinRel(tsc.opts.__baseDir ?? project.dir, tsc.opts.baseUrl)) : undefined;
104
+ if (tsc.opts.baseUrl && tsc.opts.paths) this.pathsDir = this.baseDir;
105
+ this.ws = workspace(rc);
106
+ }
107
+ tryFile(p) {
108
+ const rc = this.rc;
109
+ p = normRel(p);
110
+ if (rc.has(p)) return p;
111
+ for (const e of SRC_EXT) if (rc.has(p + e)) return p + e;
112
+ const js = p.match(/^(.*)\.(js|jsx|mjs|cjs)$/);
113
+ if (js) for (const e of [".ts", ".tsx", ".mts", ".cts"]) if (rc.has(js[1] + e)) return js[1] + e;
114
+ for (const e of SRC_EXT) if (rc.has(`${p}/index${e}`)) return `${p}/index${e}`;
115
+ return undefined;
116
+ }
117
+ /** -> { file } | { pkgDir } | { unknownLocal: true } | undefined (external) */
118
+ resolve(from, spec) {
119
+ spec = spec.split("?")[0];
120
+ if (spec.startsWith("./") || spec.startsWith("../") || spec === "." || spec === "..") {
121
+ const f = this.tryFile(joinRel(dirOf(from), spec));
122
+ return f ? { file: f } : undefined;
123
+ }
124
+ if (spec.startsWith("/")) return undefined;
125
+ for (const [pat, targets] of Object.entries(this.paths)) {
126
+ const star = pat.indexOf("*");
127
+ let rest;
128
+ if (star < 0) {
129
+ if (spec !== pat) continue;
130
+ rest = "";
131
+ } else {
132
+ const pre = pat.slice(0, star);
133
+ const post = pat.slice(star + 1);
134
+ if (!spec.startsWith(pre) || !spec.endsWith(post) || spec.length < pre.length + post.length) continue;
135
+ rest = spec.slice(pre.length, spec.length - post.length);
136
+ }
137
+ for (const t of targets ?? []) {
138
+ const f = this.tryFile(joinRel(this.pathsDir, t.replace("*", rest)));
139
+ if (f) return { file: f };
140
+ }
141
+ }
142
+ const parts = spec.split("/");
143
+ const name = spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
144
+ const pkgDir = this.ws.byName.get(name);
145
+ if (pkgDir !== undefined) return { pkgDir };
146
+ if (this.baseDir) {
147
+ const f = this.tryFile(joinRel(this.baseDir, spec));
148
+ if (f) return { file: f };
149
+ }
150
+ if (/^(@\/|~\/|#|\$)/.test(spec) || spec.startsWith("@/")) return { unknownLocal: true };
151
+ return undefined;
152
+ }
153
+ /** Import closure of the given files (repo-relative). */
154
+ closure(files) {
155
+ const rc = this.rc;
156
+ const seen = new Set();
157
+ const pkgs = new Set();
158
+ let conservative = false;
159
+ const stack = [...files];
160
+ while (stack.length) {
161
+ const f = stack.pop();
162
+ if (seen.has(f)) continue;
163
+ seen.add(f);
164
+ if (!/\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|vue|svelte|css|scss)$/.test(f)) continue;
165
+ const mk = `ts:imp:${rc.hash(f)}`;
166
+ let parsed = rc.hasher.memo.get(mk);
167
+ if (!parsed) {
168
+ parsed = importSpecs(rc.text(f));
169
+ rc.hasher.memo.set(mk, parsed);
170
+ }
171
+ if (parsed.dynamicGlob) conservative = true;
172
+ for (const s of parsed.specs) {
173
+ const r = this.resolve(f, s);
174
+ if (!r) continue;
175
+ if (r.file) stack.push(r.file);
176
+ else if (r.pkgDir !== undefined) pkgs.add(r.pkgDir);
177
+ else if (r.unknownLocal) conservative = true;
178
+ }
179
+ }
180
+ const out = new Set(seen);
181
+ for (const d of pkgs) for (const f of projectFiles(rc, d)) if (isSrc(f) || baseOf(f) === "package.json") out.add(f);
182
+ if (conservative) for (const f of projectFiles(rc, this.project.dir)) if (isSrc(f)) out.add(f);
183
+ return out;
184
+ }
185
+ }
186
+
187
+ function resolver(rc, project) {
188
+ const k = `ts:res:${project.dir}`;
189
+ let r = rc.memo.get(k);
190
+ if (!r) rc.memo.set(k, (r = new Resolver(rc, project)));
191
+ return r;
192
+ }
193
+
194
+ function wsDepsClosure(rc, dir) {
195
+ const ws = workspace(rc);
196
+ const out = new Set();
197
+ const stack = [dir];
198
+ while (stack.length) {
199
+ const d = stack.pop();
200
+ const pj = readPkg(rc, d) ?? {};
201
+ for (const name of Object.keys({ ...pj.dependencies, ...pj.devDependencies, ...pj.peerDependencies })) {
202
+ const dd = ws.byName.get(name);
203
+ if (dd !== undefined && dd !== dir && !out.has(dd)) {
204
+ out.add(dd);
205
+ stack.push(dd);
206
+ }
207
+ }
208
+ }
209
+ return [...out];
210
+ }
211
+
212
+ function binFor(rc, project, name) {
213
+ const abs = rc.path(project.dir);
214
+ const mainAbs = project.dir === "." ? rc.main : join(rc.main, project.dir);
215
+ return findBin(name, abs, rc.abs, [mainAbs, rc.main]);
216
+ }
217
+
218
+ function common(rc, project) {
219
+ const out = [joinRel(project.dir, "package.json"), ...rootLockfiles(rc)];
220
+ if (project.dir !== "." && rc.has("package.json")) out.push("package.json");
221
+ return out;
222
+ }
223
+
224
+ const adapter = {
225
+ lang: "ts",
226
+ version: 1,
227
+
228
+ detect(rc) {
229
+ if (!rc.config.languages.ts) return [];
230
+ const ws = workspace(rc);
231
+ const out = [];
232
+ for (const d of ws.dirs.sort()) {
233
+ const pj = readPkg(rc, d);
234
+ if (!pj) continue;
235
+ const deps = { ...pj.dependencies, ...pj.devDependencies };
236
+ const scripts = pj.scripts ?? {};
237
+ const files = projectFiles(rc, d);
238
+ const local = new Set(files.filter((f) => dirOf(f) === d).map(baseOf));
239
+ const kinds = [];
240
+ const meta = {};
241
+ if (local.has("tsconfig.json")) {
242
+ const tc = parseJsonc(rc.text(joinRel(d, "tsconfig.json"))) ?? {};
243
+ const solutionOnly = Array.isArray(tc.files) && tc.files.length === 0 && tc.references;
244
+ if (!solutionOnly && files.some((f) => /\.(ts|tsx|mts|cts|vue)$/.test(f))) {
245
+ kinds.push("typecheck");
246
+ meta.tsc = deps["vue-tsc"] || deps.vue ? "vue-tsc" : "tsc";
247
+ meta.composite = !!tc.compilerOptions?.composite;
248
+ }
249
+ }
250
+ const lintScript = `${scripts.lint ?? ""}`;
251
+ const hasEslintCfg = [...local].some((f) => /^(eslint\.config\.(js|mjs|cjs|ts|mts)|\.eslintrc(\.(js|cjs|json|yml|yaml))?)$/.test(f));
252
+ const hasOxCfg = local.has(".oxlintrc.json") || local.has("oxlintrc.json") || local.has("oxlint.config.ts");
253
+ if (/oxlint/.test(lintScript) || ((deps.oxlint || hasOxCfg) && !/eslint/.test(lintScript))) meta.linter = "oxlint";
254
+ else if (/eslint/.test(lintScript) || deps.eslint || hasEslintCfg) meta.linter = "eslint";
255
+ if (meta.linter && files.some((f) => LINT_EXT.test(f))) kinds.push("lint");
256
+ const testScript = `${scripts.test ?? ""}`;
257
+ if (deps.vitest || /vitest/.test(testScript) || [...local].some((f) => /^vitest\.(config|workspace)\./.test(f))) meta.tester = "vitest";
258
+ else if (deps.jest || /\bjest\b/.test(testScript) || [...local].some((f) => /^jest\.config\./.test(f))) meta.tester = "jest";
259
+ if (meta.tester && files.some((f) => TEST_FILE.test(f))) kinds.push("test");
260
+ const enabled = kinds.filter((k) => kindOptions(rc.config, d, k) !== false);
261
+ if (enabled.length) out.push({ id: `ts:${d}`, lang: "ts", dir: d, name: pj.name ?? d, kinds: enabled, meta });
262
+ }
263
+ return out;
264
+ },
265
+
266
+ units(rc, project, kind) {
267
+ const opts = kindOptions(rc.config, project.dir, kind) || {};
268
+ const files = projectFiles(rc, project.dir);
269
+ const rel = (f) => (project.dir === "." ? f : f.slice(project.dir.length + 1));
270
+ const extra = (opts.inputs ?? []).length ? files.filter((f) => matchAny(opts.inputs, rel(f))) : [];
271
+ const res = resolver(rc, project);
272
+ const base = [...common(rc, project), ...res.tsconfigChain, ...extra];
273
+ if (kind === "typecheck") {
274
+ const inputs = [...base, ...files.filter((f) => isSrc(f) && !f.endsWith("package-lock.json"))];
275
+ for (const d of wsDepsClosure(rc, project.dir)) inputs.push(...projectFiles(rc, d).filter((f) => isSrc(f) || baseOf(f) === "package.json"));
276
+ return [{ target: project.dir, label: `${project.name} typecheck`, inputs }];
277
+ }
278
+ if (kind === "lint") {
279
+ const cfg = files.filter((f) => dirOf(f) === project.dir && /^(eslint\.config\.|\.eslintrc|\.oxlintrc|oxlintrc|oxlint\.config|\.eslintignore)/.test(baseOf(f)));
280
+ if (project.dir !== ".") for (const f of rc.listing().files) if (dirOf(f) === "." && /^(eslint\.config\.|\.eslintrc|\.oxlintrc|\.eslintignore)/.test(baseOf(f))) cfg.push(f);
281
+ const exclude = opts.exclude ?? [];
282
+ return files.filter((f) => LINT_EXT.test(f) && !matchAny(exclude, rel(f))).map((f) => ({ target: f, label: f, inputs: [f, ...cfg, ...base] }));
283
+ }
284
+ if (kind === "test") {
285
+ const exclude = [...DEFAULT_TEST_EXCLUDE, ...(opts.exclude ?? [])];
286
+ const tests = files.filter((f) => TEST_FILE.test(f) && !matchAny(exclude, rel(f)) && !matchAny(exclude, f));
287
+ const cfgFiles = files.filter((f) => dirOf(f) === project.dir && (/\.config\.(c|m)?(j|t)s$/.test(f) || /^\.env/.test(baseOf(f)) || /^(babel|\.babelrc)/.test(baseOf(f))));
288
+ const setup = files.filter((f) => /setup/i.test(baseOf(f)) && /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(f) && !TEST_FILE.test(f));
289
+ const shared = [...base, ...res.closure([...cfgFiles, ...setup])];
290
+ return tests.map((t) => ({ target: t, label: t, inputs: [...shared, ...res.closure([t])] }));
291
+ }
292
+ return [];
293
+ },
294
+
295
+ group(project, kind, opts) {
296
+ return { name: `ts:${project.dir}:${kind}`, limit: opts?.parallel ?? (kind === "lint" ? 2 : 1) };
297
+ },
298
+
299
+ batchSize: (kind) => (kind === "lint" ? 400 : kind === "test" ? 80 : 1),
300
+
301
+ argsKey(opts, project) {
302
+ return JSON.stringify({ args: opts?.args ?? [], env: opts?.env ?? {}, cmd: opts?.cmd ?? "", tool: project.meta });
303
+ },
304
+
305
+ async run({ rc, project, kind, units, opts, threads, signal, nice, tmpDir }) {
306
+ const cwd = rc.path(project.dir);
307
+ const env = cleanEnv({ CI: "1", FORCE_COLOR: "0", NO_COLOR: "1", ...(opts?.env ?? {}) });
308
+ const results = new Map();
309
+ const relFromProject = (f) => (project.dir === "." ? f : f.slice(project.dir.length + 1));
310
+ const toRepo = (p) => {
311
+ // p: absolute or relative to cwd
312
+ const abs = p.startsWith("/") ? p : join(cwd, p);
313
+ if (abs.startsWith(`${rc.abs}/`)) return normRel(abs.slice(rc.abs.length + 1));
314
+ return normRel(joinRel(project.dir, p));
315
+ };
316
+ const timeoutMs = parseDuration(opts?.timeout, 20 * 60000);
317
+ const fail = (u, failures, output = "") => results.set(u.target, { status: "fail", failures, output });
318
+ const errAll = (title, text) => {
319
+ for (const u of units) results.set(u.target, { status: "error", failures: [{ id: `${u.target}::[error]`, title, output: String(text).slice(-6000) }], output: String(text).slice(-6000) });
320
+ };
321
+
322
+ if (kind === "typecheck") {
323
+ const u = units[0];
324
+ let cmd;
325
+ let args;
326
+ if (opts?.cmd) {
327
+ cmd = "/bin/sh";
328
+ args = ["-c", opts.cmd];
329
+ } else {
330
+ cmd = binFor(rc, project, project.meta.tsc ?? "tsc");
331
+ if (!cmd) return errAll(`${project.meta.tsc ?? "tsc"} not found (install dependencies)`, ""), { results, ms: 0 };
332
+ args = ["--noEmit", "-p", "tsconfig.json", "--pretty", "false"];
333
+ if (!project.meta.composite) {
334
+ mkdirSync(tmpDir, { recursive: true });
335
+ args.push("--incremental", "--tsBuildInfoFile", join(tmpDir, `tsc-${sha(rc.abs + project.dir).slice(0, 12)}.tsbuildinfo`));
336
+ }
337
+ args.push(...(opts?.args ?? []));
338
+ }
339
+ const r = await runProc({ cmd, args, cwd, env, signal, nice, timeoutMs });
340
+ const errs = [];
341
+ for (const line of `${r.stdout}\n${r.stderr}`.split("\n")) {
342
+ const m = line.match(/^(.+?)\((\d+),(\d+)\): error (TS\d+): (.*)$/);
343
+ if (m) errs.push({ file: toRepo(m[1]), line: Number(m[2]), code: m[4], msg: m[5] });
344
+ else {
345
+ const g = line.match(/^error (TS\d+): (.*)$/);
346
+ if (g) errs.push({ file: "", line: 0, code: g[1], msg: g[2] });
347
+ else if (/^\s{2,}\S/.test(line) && errs.length) errs[errs.length - 1].msg += ` ${line.trim()}`;
348
+ }
349
+ }
350
+ if (errs.length) fail(u, errs.map((e) => ({ id: `${e.file}:${e.code}:${e.msg}`, title: `${e.file}${e.line ? `:${e.line}` : ""} ${e.code} ${e.msg}`, output: "" })), `${r.stdout}`.slice(-20000));
351
+ else if (r.code !== 0) results.set(u.target, { status: r.killed ? "error" : "fail", failures: [{ id: `${project.dir}:typecheck:[error]`, title: "typecheck exited with an error", output: `${r.stdout}\n${r.stderr}`.trim().slice(-6000) }], output: "" });
352
+ else results.set(u.target, { status: "pass", failures: [], passed: 1, output: "" });
353
+ return { results, ms: r.ms, killed: r.killed };
354
+ }
355
+
356
+ if (kind === "lint") {
357
+ const linter = project.meta.linter;
358
+ const bin = binFor(rc, project, linter);
359
+ if (!bin) return errAll(`${linter} not found (install dependencies)`, ""), { results, ms: 0 };
360
+ const files = units.map((u) => relFromProject(u.target));
361
+ const args = linter === "eslint" ? ["--format", "json", ...(opts?.args ?? []), ...files] : ["--format", "json", ...(opts?.args ?? []), ...files];
362
+ const r = await runProc({ cmd: bin, args, cwd, env, signal, nice, timeoutMs });
363
+ const per = new Map(units.map((u) => [u.target, []]));
364
+ let parsed = false;
365
+ const text = r.stdout;
366
+ if (linter === "eslint") {
367
+ const start = text.indexOf("[");
368
+ try {
369
+ const arr = JSON.parse(text.slice(start));
370
+ parsed = true;
371
+ for (const f of arr) {
372
+ const file = toRepo(f.filePath);
373
+ for (const m of f.messages ?? []) if (m.severity === 2 || m.fatal) per.get(file)?.push({ rule: m.ruleId ?? "fatal", msg: m.message, line: m.line ?? 0, file });
374
+ }
375
+ } catch {}
376
+ } else {
377
+ try {
378
+ const j = JSON.parse(text.slice(text.indexOf("{") >= 0 && (text.indexOf("[") < 0 || text.indexOf("{") < text.indexOf("[")) ? text.indexOf("{") : text.indexOf("[")));
379
+ parsed = true;
380
+ const diags = Array.isArray(j) ? j : (j.diagnostics ?? []);
381
+ for (const d of diags) {
382
+ if (d.severity && !/error|deny/i.test(String(d.severity))) continue;
383
+ const file = toRepo(d.filename ?? d.file ?? "");
384
+ const line = d.labels?.[0]?.span?.line ?? d.line ?? 0;
385
+ per.get(file)?.push({ rule: d.code ?? "", msg: d.message ?? "", line, file });
386
+ }
387
+ } catch {}
388
+ }
389
+ if (!parsed) {
390
+ if (r.code === 0) for (const u of units) results.set(u.target, { status: "pass", failures: [], passed: 1, output: "" });
391
+ else errAll(`${linter} output could not be read`, `${r.stdout}\n${r.stderr}`);
392
+ return { results, ms: r.ms, killed: r.killed };
393
+ }
394
+ for (const u of units) {
395
+ const d = per.get(u.target) ?? [];
396
+ if (d.length) fail(u, d.map((x) => ({ id: `${x.file}:${x.rule}:${x.msg}`, title: `${x.file}:${x.line} ${x.rule} ${x.msg}`, output: "" })));
397
+ else results.set(u.target, { status: "pass", failures: [], passed: 1, output: "" });
398
+ }
399
+ return { results, ms: r.ms, killed: r.killed };
400
+ }
401
+
402
+ if (kind === "test") {
403
+ const tester = project.meta.tester;
404
+ const bin = binFor(rc, project, tester);
405
+ if (!bin) return errAll(`${tester} not found (install dependencies)`, ""), { results, ms: 0 };
406
+ mkdirSync(tmpDir, { recursive: true });
407
+ const outFile = join(tmpDir, `${tester}-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
408
+ const files = units.map((u) => relFromProject(u.target));
409
+ const args = tester === "vitest" ? ["run", "--reporter=json", `--outputFile=${outFile}`, ...(opts?.args ?? []), ...files] : ["--json", `--outputFile=${outFile}`, "--runTestsByPath", `--maxWorkers=${threads}`, "--ci", ...(opts?.args ?? []), ...files];
410
+ const r = await runProc({ cmd: bin, args, cwd, env, signal, nice, timeoutMs });
411
+ let j;
412
+ try {
413
+ j = JSON.parse(readFileSync(outFile, "utf8"));
414
+ } catch {}
415
+ try {
416
+ rmSync(outFile, { force: true });
417
+ } catch {}
418
+ if (!j) {
419
+ errAll(r.killed ? `${tester} was stopped (timeout or cancelled)` : `${tester} produced no report`, `${r.stdout}\n${r.stderr}`);
420
+ return { results, ms: r.ms, killed: r.killed };
421
+ }
422
+ const byFile = new Map();
423
+ for (const tr of j.testResults ?? []) byFile.set(toRepo(tr.name), tr);
424
+ for (const u of units) {
425
+ const tr = byFile.get(u.target);
426
+ if (!tr) {
427
+ results.set(u.target, { status: "pass", failures: [], passed: 0, output: "", note: `not collected by ${tester} (excluded by its config)` });
428
+ continue;
429
+ }
430
+ const as = tr.assertionResults ?? [];
431
+ const failed = as.filter((a) => a.status === "failed");
432
+ const passed = as.filter((a) => a.status === "passed").length;
433
+ if (failed.length) fail(u, failed.map((a) => ({ id: `${u.target}::${a.fullName ?? a.title}`, title: `${u.target} › ${a.fullName ?? a.title}`, output: trimStack((a.failureMessages ?? []).join("\n"), rc.abs).slice(-6000) })));
434
+ else if (tr.status === "failed") fail(u, [{ id: `${u.target}::[suite]`, title: `${u.target} failed to run`, output: String(tr.message ?? tr.failureMessage ?? "").slice(-6000) }]);
435
+ else results.set(u.target, { status: "pass", failures: [], passed, output: "" });
436
+ if (results.get(u.target)) results.get(u.target).passed = passed;
437
+ }
438
+ return { results, ms: r.ms, killed: r.killed };
439
+ }
440
+ return { results, ms: 0 };
441
+ },
442
+ };
443
+
444
+ export default adapter;
445
+ export { importSpecs, Resolver, tsconfigInfo };