pi-jscpd 0.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/CONTRIBUTING.md +144 -0
  3. package/LICENSE +21 -0
  4. package/README.md +231 -0
  5. package/SECURITY.md +93 -0
  6. package/docs/automatic-checkpoint.md +235 -0
  7. package/docs/compatibility.md +119 -0
  8. package/docs/effect-architecture.md +128 -0
  9. package/docs/fallow-coexistence.md +120 -0
  10. package/docs/overlay-interaction.md +347 -0
  11. package/docs/release.md +115 -0
  12. package/package.json +86 -0
  13. package/scripts/check-compatibility.mjs +103 -0
  14. package/skills/jscpd/SKILL.md +90 -0
  15. package/src/acknowledgements.ts +268 -0
  16. package/src/automatic.ts +396 -0
  17. package/src/baseline.ts +400 -0
  18. package/src/capability.ts +569 -0
  19. package/src/changed-files.ts +372 -0
  20. package/src/changed.ts +548 -0
  21. package/src/clone-identity.ts +373 -0
  22. package/src/config.ts +414 -0
  23. package/src/contract.ts +39 -0
  24. package/src/dispatch.ts +90 -0
  25. package/src/effect/clock.ts +10 -0
  26. package/src/effect/errors.ts +311 -0
  27. package/src/effect/filesystem.ts +240 -0
  28. package/src/effect/runtime-boundary.ts +25 -0
  29. package/src/effect/runtime-contract.ts +18 -0
  30. package/src/effect/services.ts +131 -0
  31. package/src/extension.ts +708 -0
  32. package/src/fallow.ts +479 -0
  33. package/src/finding-presentation.ts +73 -0
  34. package/src/index.ts +8 -0
  35. package/src/jscpd-report.ts +819 -0
  36. package/src/jscpd.ts +748 -0
  37. package/src/overlay.ts +1166 -0
  38. package/src/parser.ts +189 -0
  39. package/src/path-utils.ts +44 -0
  40. package/src/presentation.ts +232 -0
  41. package/src/process.ts +425 -0
  42. package/src/registry.ts +102 -0
  43. package/src/scan.ts +441 -0
  44. package/src/scheduler.ts +434 -0
  45. package/src/session-state.ts +229 -0
  46. package/src/status.ts +534 -0
  47. package/src/types.ts +334 -0
  48. package/src/value-utils.ts +14 -0
  49. package/src/verification.ts +220 -0
@@ -0,0 +1,569 @@
1
+ import { delimiter, dirname, join, parse, resolve } from "node:path";
2
+ import { Cause, Context, Effect, Layer } from "effect";
3
+ import type { JscpdProcess } from "./effect/services.js";
4
+ import {
5
+ type BoundedProcessResult,
6
+ createProcessEnvironmentWithPath,
7
+ runBoundedProcessEffect,
8
+ } from "./process.js";
9
+
10
+ export const JSCPD_SUPPORTED_MAJOR = 5;
11
+ export const JSCPD_VERSION_TIMEOUT_MS = 2_000;
12
+ export const JSCPD_VERSION_MAX_OUTPUT_BYTES = 4_096;
13
+
14
+ const VERSION_ARGUMENTS = ["--version"] as const;
15
+ const EXECUTABLES = ["jscpd", "cpd"] as const;
16
+ const PACKAGE_ROOT = resolve(import.meta.dirname, "..");
17
+ const MAX_PROJECT_BIN_DIRECTORIES = 64;
18
+ const MAX_VERSION_LINE_LENGTH = 128;
19
+ const VERSION_PATTERN =
20
+ /^(?:(?:jscpd|cpd)(?:\s+version)?\s*[:=]?\s*)?v?((0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/i;
21
+
22
+ export type JscpdExecutable = (typeof EXECUTABLES)[number];
23
+ export type JscpdCapabilitySource = "project-or-path" | "bundled";
24
+
25
+ export type JscpdProbeFailureReason =
26
+ | "malformed-version"
27
+ | "nonzero-exit"
28
+ | "output-limit"
29
+ | "execution-error"
30
+ | "service-disposed";
31
+
32
+ export type JscpdCapabilityResult =
33
+ | {
34
+ status: "available";
35
+ executable: JscpdExecutable;
36
+ version: string;
37
+ major: typeof JSCPD_SUPPORTED_MAJOR;
38
+ source?: JscpdCapabilitySource;
39
+ }
40
+ | {
41
+ status: "missing";
42
+ checked: readonly JscpdExecutable[];
43
+ }
44
+ | {
45
+ status: "incompatible";
46
+ executable: JscpdExecutable;
47
+ version: string;
48
+ major: number;
49
+ supportedMajor: typeof JSCPD_SUPPORTED_MAJOR;
50
+ source?: JscpdCapabilitySource;
51
+ }
52
+ | {
53
+ status: "cancelled";
54
+ executable: JscpdExecutable;
55
+ }
56
+ | {
57
+ status: "timed-out";
58
+ executable: JscpdExecutable;
59
+ timeoutMs: number;
60
+ }
61
+ | {
62
+ status: "failed";
63
+ executable: JscpdExecutable;
64
+ reason: JscpdProbeFailureReason;
65
+ exitCode?: number;
66
+ };
67
+
68
+ export type JscpdProbeExecutionResult =
69
+ | { status: "completed"; exitCode: number; stdout: string; stderr: string }
70
+ | { status: "missing" }
71
+ | { status: "cancelled" }
72
+ | { status: "timed-out" }
73
+ | { status: "output-limit" }
74
+ | { status: "failed" };
75
+
76
+ export interface JscpdProbeExecutionRequest {
77
+ executable: JscpdExecutable;
78
+ args: readonly string[];
79
+ cwd: string;
80
+ path: string;
81
+ signal: AbortSignal;
82
+ timeoutMs: number;
83
+ maxOutputBytes: number;
84
+ }
85
+
86
+ export interface JscpdProbeExecutor {
87
+ runEffect: (
88
+ request: JscpdProbeExecutionRequest,
89
+ ) => Effect.Effect<JscpdProbeExecutionResult, never, JscpdProcess>;
90
+ }
91
+
92
+ export interface JscpdCapabilityRequest {
93
+ cwd: string;
94
+ signal?: AbortSignal;
95
+ /** Overrides PATH resolution and is primarily useful for deterministic hosts and tests. */
96
+ path?: string;
97
+ }
98
+
99
+ interface JscpdCapabilityEffectService {
100
+ probe(request: JscpdCapabilityRequest): Effect.Effect<JscpdCapabilityResult, never, JscpdProcess>;
101
+ invalidate(): Effect.Effect<void>;
102
+ dispose(): Effect.Effect<void>;
103
+ }
104
+
105
+ export const JscpdCapability = Context.GenericTag<JscpdCapabilityEffectService>(
106
+ "pi-jscpd/effect/JscpdCapability",
107
+ );
108
+
109
+ export interface JscpdCapabilityService {
110
+ probeEffect: (
111
+ request: JscpdCapabilityRequest,
112
+ ) => Effect.Effect<JscpdCapabilityResult, never, JscpdProcess>;
113
+ invalidate(): void;
114
+ dispose(): void;
115
+ }
116
+
117
+ interface ParsedVersion {
118
+ version: string;
119
+ major: number;
120
+ }
121
+
122
+ interface CachedCapability {
123
+ key: string;
124
+ result: JscpdCapabilityResult;
125
+ }
126
+
127
+ interface CapabilityProbeContext {
128
+ cwd: string;
129
+ path: string;
130
+ executionPath: string;
131
+ key: string;
132
+ }
133
+
134
+ interface VersionProbeContext {
135
+ cwd: string;
136
+ path: string;
137
+ executionPath: string;
138
+ signal: AbortSignal;
139
+ }
140
+
141
+ interface LinkedAbortController {
142
+ controller: AbortController;
143
+ detach: () => void;
144
+ }
145
+
146
+ type StartedProbeExecutionResult = Exclude<JscpdProbeExecutionResult, { status: "missing" }>;
147
+
148
+ /** Parse the deliberately small set of version lines emitted by supported jscpd CLIs. */
149
+ export function parseJscpdVersion(output: string): ParsedVersion | undefined {
150
+ const line = output.trim();
151
+ if (line.length === 0 || line.length > MAX_VERSION_LINE_LENGTH || line.includes("\n")) {
152
+ return undefined;
153
+ }
154
+
155
+ const match = VERSION_PATTERN.exec(line);
156
+ if (!match?.[1] || !match[2]) {
157
+ return undefined;
158
+ }
159
+
160
+ const major = Number(match[2]);
161
+ if (!Number.isSafeInteger(major)) {
162
+ return undefined;
163
+ }
164
+
165
+ return { version: match[1], major };
166
+ }
167
+
168
+ export function createNodeProbeExecutor(): JscpdProbeExecutor {
169
+ return { runEffect: executeNodeProbeEffect };
170
+ }
171
+
172
+ export function createJscpdCapabilityService(
173
+ executor: JscpdProbeExecutor = createNodeProbeExecutor(),
174
+ ): JscpdCapabilityService {
175
+ return new DefaultJscpdCapabilityService(executor);
176
+ }
177
+
178
+ /** Scoped capability layer whose cache and active probes belong to one service instance. */
179
+ export function createJscpdCapabilityLayer(
180
+ executor: JscpdProbeExecutor = createNodeProbeExecutor(),
181
+ ) {
182
+ return Layer.scoped(
183
+ JscpdCapability,
184
+ Effect.acquireRelease(
185
+ Effect.sync(() => new DefaultJscpdCapabilityService(executor)),
186
+ (owner) => owner.disposeEffect(),
187
+ ).pipe(Effect.map(capabilityEffectServiceFor)),
188
+ );
189
+ }
190
+
191
+ class DefaultJscpdCapabilityService implements JscpdCapabilityService {
192
+ readonly #executor: JscpdProbeExecutor;
193
+ readonly #activeControllers = new Set<AbortController>();
194
+ #cache: CachedCapability | undefined;
195
+ #resolutionKey: string | undefined;
196
+ #generation = 0;
197
+ #disposed = false;
198
+
199
+ constructor(executor: JscpdProbeExecutor) {
200
+ this.#executor = executor;
201
+ }
202
+
203
+ probeEffect(
204
+ request: JscpdCapabilityRequest,
205
+ ): Effect.Effect<JscpdCapabilityResult, never, JscpdProcess> {
206
+ return Effect.suspend(() => {
207
+ if (this.#disposed) return Effect.succeed(serviceDisposedResult());
208
+ const context = createCapabilityProbeContext(request);
209
+ this.#selectResolutionKey(context.key);
210
+ const cached = this.#cachedResult(context.key);
211
+ return cached ? Effect.succeed(cached) : this.#probeUncachedEffect(context, request.signal);
212
+ }).pipe(
213
+ Effect.catchAllCause((cause) =>
214
+ Cause.isInterruptedOnly(cause)
215
+ ? Effect.failCause(cause)
216
+ : Effect.succeed({
217
+ status: "failed",
218
+ executable: "jscpd",
219
+ reason: "execution-error",
220
+ } as const),
221
+ ),
222
+ );
223
+ }
224
+
225
+ #probeUncachedEffect(
226
+ context: CapabilityProbeContext,
227
+ requestSignal: AbortSignal | undefined,
228
+ ): Effect.Effect<JscpdCapabilityResult, never, JscpdProcess> {
229
+ const generation = this.#generation;
230
+ const linkedAbort = createLinkedAbortController(requestSignal);
231
+ this.#activeControllers.add(linkedAbort.controller);
232
+ const probe = probeExecutablesEffect(this.#executor, {
233
+ cwd: context.cwd,
234
+ path: context.path,
235
+ executionPath: context.executionPath,
236
+ signal: linkedAbort.controller.signal,
237
+ }).pipe(
238
+ Effect.tap((result) => Effect.sync(() => this.#cacheResult(context.key, generation, result))),
239
+ );
240
+ return Effect.acquireUseRelease(
241
+ Effect.succeed(linkedAbort),
242
+ () => Effect.raceFirst(probe, awaitProbeCancellation(linkedAbort.controller.signal)),
243
+ () =>
244
+ Effect.sync(() => {
245
+ linkedAbort.detach();
246
+ this.#activeControllers.delete(linkedAbort.controller);
247
+ }),
248
+ );
249
+ }
250
+
251
+ #cachedResult(key: string): JscpdCapabilityResult | undefined {
252
+ return this.#cache?.key === key ? this.#cache.result : undefined;
253
+ }
254
+
255
+ #cacheResult(key: string, generation: number, result: JscpdCapabilityResult): void {
256
+ if (
257
+ isCacheable(result) &&
258
+ !this.#disposed &&
259
+ this.#generation === generation &&
260
+ this.#resolutionKey === key
261
+ ) {
262
+ this.#cache = { key, result };
263
+ }
264
+ }
265
+
266
+ invalidate(): void {
267
+ this.#abortActiveProbes();
268
+ this.#generation += 1;
269
+ this.#resolutionKey = undefined;
270
+ this.#cache = undefined;
271
+ }
272
+
273
+ invalidateEffect(): Effect.Effect<void> {
274
+ return Effect.sync(() => this.invalidate());
275
+ }
276
+
277
+ dispose(): void {
278
+ if (this.#disposed) return;
279
+ this.#disposed = true;
280
+ this.invalidate();
281
+ }
282
+
283
+ disposeEffect(): Effect.Effect<void> {
284
+ return Effect.sync(() => this.dispose());
285
+ }
286
+
287
+ #selectResolutionKey(key: string): void {
288
+ if (this.#resolutionKey === undefined) {
289
+ this.#resolutionKey = key;
290
+ return;
291
+ }
292
+ if (this.#resolutionKey !== key) {
293
+ this.invalidate();
294
+ this.#resolutionKey = key;
295
+ }
296
+ }
297
+
298
+ #abortActiveProbes(): void {
299
+ for (const controller of this.#activeControllers) controller.abort();
300
+ this.#activeControllers.clear();
301
+ }
302
+ }
303
+
304
+ function capabilityEffectServiceFor(
305
+ owner: DefaultJscpdCapabilityService,
306
+ ): JscpdCapabilityEffectService {
307
+ return {
308
+ probe: (request) => owner.probeEffect(request),
309
+ invalidate: () => owner.invalidateEffect(),
310
+ dispose: () => owner.disposeEffect(),
311
+ };
312
+ }
313
+
314
+ function awaitProbeCancellation(signal: AbortSignal): Effect.Effect<JscpdCapabilityResult> {
315
+ if (signal.aborted) return Effect.succeed({ status: "cancelled", executable: "jscpd" });
316
+ return Effect.async((resume) => {
317
+ const cancel = () => resume(Effect.succeed({ status: "cancelled", executable: "jscpd" }));
318
+ signal.addEventListener("abort", cancel, { once: true });
319
+ if (signal.aborted) cancel();
320
+ return Effect.sync(() => signal.removeEventListener("abort", cancel));
321
+ });
322
+ }
323
+
324
+ function probeExecutablesEffect(
325
+ executor: JscpdProbeExecutor,
326
+ context: VersionProbeContext,
327
+ ): Effect.Effect<JscpdCapabilityResult, never, JscpdProcess> {
328
+ return Effect.gen(function* () {
329
+ let externalFailure: JscpdCapabilityResult | undefined;
330
+ for (const executable of EXECUTABLES) {
331
+ const execution = yield* runVersionProbeEffect(executor, executable, context, context.path);
332
+ if (execution.status === "missing") continue;
333
+ const capability = capabilityFromStartedProbe(executable, execution, "project-or-path");
334
+ if (capability.status === "available" || capability.status === "cancelled") return capability;
335
+ externalFailure = capability;
336
+ break;
337
+ }
338
+
339
+ const bundled = yield* runVersionProbeEffect(executor, "jscpd", context, context.executionPath);
340
+ return bundled.status === "missing"
341
+ ? (externalFailure ?? { status: "missing", checked: EXECUTABLES })
342
+ : capabilityFromStartedProbe("jscpd", bundled, "bundled");
343
+ });
344
+ }
345
+
346
+ function runVersionProbeEffect(
347
+ executor: JscpdProbeExecutor,
348
+ executable: JscpdExecutable,
349
+ context: VersionProbeContext,
350
+ path: string,
351
+ ): Effect.Effect<JscpdProbeExecutionResult, never, JscpdProcess> {
352
+ const request = {
353
+ executable,
354
+ args: VERSION_ARGUMENTS,
355
+ cwd: context.cwd,
356
+ path,
357
+ signal: context.signal,
358
+ timeoutMs: JSCPD_VERSION_TIMEOUT_MS,
359
+ maxOutputBytes: JSCPD_VERSION_MAX_OUTPUT_BYTES,
360
+ } satisfies JscpdProbeExecutionRequest;
361
+ return executor.runEffect(request);
362
+ }
363
+
364
+ function capabilityFromStartedProbe(
365
+ executable: JscpdExecutable,
366
+ execution: StartedProbeExecutionResult,
367
+ source: JscpdCapabilitySource,
368
+ ): JscpdCapabilityResult {
369
+ switch (execution.status) {
370
+ case "completed":
371
+ return capabilityFromCompletedProbe(executable, execution, source);
372
+ case "cancelled":
373
+ return { status: "cancelled", executable };
374
+ case "timed-out":
375
+ return { status: "timed-out", executable, timeoutMs: JSCPD_VERSION_TIMEOUT_MS };
376
+ case "output-limit":
377
+ return { status: "failed", executable, reason: "output-limit" };
378
+ case "failed":
379
+ return { status: "failed", executable, reason: "execution-error" };
380
+ }
381
+ }
382
+
383
+ function capabilityFromCompletedProbe(
384
+ executable: JscpdExecutable,
385
+ execution: Extract<JscpdProbeExecutionResult, { status: "completed" }>,
386
+ source: JscpdCapabilitySource,
387
+ ): JscpdCapabilityResult {
388
+ if (execution.exitCode !== 0) {
389
+ return {
390
+ status: "failed",
391
+ executable,
392
+ reason: "nonzero-exit",
393
+ exitCode: normalizeExitCode(execution.exitCode),
394
+ };
395
+ }
396
+ if (outputExceedsLimit(execution.stdout, execution.stderr)) {
397
+ return { status: "failed", executable, reason: "output-limit" };
398
+ }
399
+
400
+ const parsed = parseJscpdVersion(selectVersionOutput(execution));
401
+ if (!parsed) {
402
+ return { status: "failed", executable, reason: "malformed-version" };
403
+ }
404
+ return capabilityFromParsedVersion(executable, parsed, source);
405
+ }
406
+
407
+ function capabilityFromParsedVersion(
408
+ executable: JscpdExecutable,
409
+ parsed: ParsedVersion,
410
+ source: JscpdCapabilitySource,
411
+ ): JscpdCapabilityResult {
412
+ if (parsed.major !== JSCPD_SUPPORTED_MAJOR) {
413
+ return {
414
+ status: "incompatible",
415
+ executable,
416
+ version: parsed.version,
417
+ major: parsed.major,
418
+ supportedMajor: JSCPD_SUPPORTED_MAJOR,
419
+ source,
420
+ };
421
+ }
422
+ return {
423
+ status: "available",
424
+ executable,
425
+ version: parsed.version,
426
+ major: JSCPD_SUPPORTED_MAJOR,
427
+ source,
428
+ };
429
+ }
430
+
431
+ function selectVersionOutput(
432
+ execution: Extract<JscpdProbeExecutionResult, { status: "completed" }>,
433
+ ): string {
434
+ return execution.stdout.trim().length > 0 ? execution.stdout : execution.stderr;
435
+ }
436
+
437
+ function serviceDisposedResult(): JscpdCapabilityResult {
438
+ return {
439
+ status: "failed",
440
+ executable: "jscpd",
441
+ reason: "service-disposed",
442
+ };
443
+ }
444
+
445
+ function createCapabilityProbeContext(request: JscpdCapabilityRequest): CapabilityProbeContext {
446
+ const configuredPath = request.path ?? process.env.PATH ?? "";
447
+ return {
448
+ cwd: request.cwd,
449
+ path: createExternalJscpdPath(request.cwd, configuredPath),
450
+ executionPath: createJscpdExecutionPath(request.cwd, configuredPath, "bundled"),
451
+ key: createResolutionKey(request.cwd, configuredPath),
452
+ };
453
+ }
454
+
455
+ /** Build the deterministic PATH used for scans after capability resolution. */
456
+ export function createJscpdExecutionPath(
457
+ cwd: string,
458
+ configuredPath: string = process.env.PATH ?? "",
459
+ source: JscpdCapabilitySource = "project-or-path",
460
+ ): string {
461
+ const external = [...projectBinDirectories(cwd), configuredPath];
462
+ const bundled = [join(PACKAGE_ROOT, "node_modules", ".bin"), join(dirname(PACKAGE_ROOT), ".bin")];
463
+ return joinPathEntries(
464
+ source === "bundled" ? [...bundled, ...external] : [...external, ...bundled],
465
+ );
466
+ }
467
+
468
+ function createExternalJscpdPath(cwd: string, configuredPath: string): string {
469
+ return joinPathEntries([...projectBinDirectories(cwd), configuredPath]);
470
+ }
471
+
472
+ function projectBinDirectories(cwd: string): string[] {
473
+ const directories: string[] = [];
474
+ let current = resolve(cwd);
475
+ for (let depth = 0; depth < MAX_PROJECT_BIN_DIRECTORIES; depth += 1) {
476
+ directories.push(join(current, "node_modules", ".bin"));
477
+ const parent = dirname(current);
478
+ if (parent === current || current === parse(current).root) break;
479
+ current = parent;
480
+ }
481
+ return directories;
482
+ }
483
+
484
+ function joinPathEntries(entries: readonly string[]): string {
485
+ return entries.filter((entry) => entry.length > 0).join(delimiter);
486
+ }
487
+
488
+ function createLinkedAbortController(
489
+ ...signals: readonly (AbortSignal | undefined)[]
490
+ ): LinkedAbortController {
491
+ const controller = new AbortController();
492
+ const abort = () => controller.abort();
493
+ for (const signal of signals) {
494
+ if (signal?.aborted) controller.abort();
495
+ else signal?.addEventListener("abort", abort, { once: true });
496
+ }
497
+
498
+ return {
499
+ controller,
500
+ detach: () => {
501
+ for (const signal of signals) signal?.removeEventListener("abort", abort);
502
+ },
503
+ };
504
+ }
505
+
506
+ function createResolutionKey(cwd: string, path: string): string {
507
+ return `${cwd.length}:${cwd}${path}`;
508
+ }
509
+
510
+ function isCacheable(result: JscpdCapabilityResult): boolean {
511
+ return (
512
+ result.status === "available" || result.status === "missing" || result.status === "incompatible"
513
+ );
514
+ }
515
+
516
+ function outputExceedsLimit(stdout: string, stderr: string): boolean {
517
+ return (
518
+ Buffer.byteLength(stdout, "utf8") + Buffer.byteLength(stderr, "utf8") >
519
+ JSCPD_VERSION_MAX_OUTPUT_BYTES
520
+ );
521
+ }
522
+
523
+ function normalizeExitCode(exitCode: number): number {
524
+ return Number.isSafeInteger(exitCode) ? exitCode : 1;
525
+ }
526
+
527
+ function executeNodeProbeEffect(
528
+ request: JscpdProbeExecutionRequest,
529
+ ): Effect.Effect<JscpdProbeExecutionResult, never, JscpdProcess> {
530
+ const process = runBoundedProcessEffect({
531
+ stage: "probe",
532
+ executable: request.executable,
533
+ args: request.args,
534
+ cwd: request.cwd,
535
+ environment: createProcessEnvironmentWithPath(request.path),
536
+ timeoutMs: request.timeoutMs,
537
+ maxOutputBytes: request.maxOutputBytes,
538
+ }).pipe(Effect.map(probeExecutionResult));
539
+ const cancelled = { status: "cancelled" } as const;
540
+ return Effect.suspend(() =>
541
+ request.signal.aborted
542
+ ? Effect.succeed(cancelled)
543
+ : Effect.raceFirst(
544
+ process,
545
+ awaitProbeCancellation(request.signal).pipe(Effect.as(cancelled)),
546
+ ),
547
+ );
548
+ }
549
+
550
+ function probeExecutionResult(result: BoundedProcessResult): JscpdProbeExecutionResult {
551
+ switch (result.status) {
552
+ case "completed":
553
+ return {
554
+ status: "completed",
555
+ exitCode: result.exitCode,
556
+ stdout: result.stdout.toString("utf8"),
557
+ stderr: result.stderr.toString("utf8"),
558
+ };
559
+ case "not-found":
560
+ return { status: "missing" };
561
+ case "cancelled":
562
+ case "timed-out":
563
+ case "output-limit":
564
+ return { status: result.status };
565
+ case "invalid-request":
566
+ case "spawn-failed":
567
+ return { status: "failed" };
568
+ }
569
+ }