@systemfsoftware/stryker-js-cli 1.2.5

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 (4) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +103 -0
  3. package/dist/main.mjs +1495 -0
  4. package/package.json +72 -0
package/dist/main.mjs ADDED
@@ -0,0 +1,1495 @@
1
+ #!/usr/bin/env node
2
+ import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
3
+ import * as NodeStdio from "@effect/platform-node/NodeStdio";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Layer from "effect/Layer";
6
+ import semver from "semver";
7
+ import { strykerEngines, strykerVersion } from "@systemfsoftware/stryker-js-mutation-run/stryker-package";
8
+ import * as Context from "effect/Context";
9
+ import * as Result from "effect/Result";
10
+ import * as CliError from "effect/unstable/cli/CliError";
11
+ import * as Cause from "effect/Cause";
12
+ import * as Clock from "effect/Clock";
13
+ import * as Deferred from "effect/Deferred";
14
+ import * as Fiber from "effect/Fiber";
15
+ import * as Queue from "effect/Queue";
16
+ import * as Stdio from "effect/Stdio";
17
+ import * as Stream from "effect/Stream";
18
+ import { buildVerdictEnvelope, generateRunId } from "@systemfsoftware/stryker-js-mutation-run/verdict-envelope";
19
+ import * as Console from "effect/Console";
20
+ import * as FileSystem from "effect/FileSystem";
21
+ import * as Option from "effect/Option";
22
+ import * as Path from "effect/Path";
23
+ import * as Ref from "effect/Ref";
24
+ import * as Terminal from "effect/Terminal";
25
+ import * as Argument from "effect/unstable/cli/Argument";
26
+ import * as CliConfig from "effect/unstable/cli/CliConfig";
27
+ import * as Command from "effect/unstable/cli/Command";
28
+ import * as Flag from "effect/unstable/cli/Flag";
29
+ import * as GlobalFlag from "effect/unstable/cli/GlobalFlag";
30
+ import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
31
+ import { ConfigReader, OptionsValidator, defaultOptions } from "@systemfsoftware/stryker-js-mutation-run/config/config-resolution";
32
+ import { PluginKind } from "@systemfsoftware/stryker-js-plugin-api/plugin";
33
+ import { strykerPlugins } from "@systemfsoftware/stryker-js-mutation-report/stryker-plugins";
34
+ import { createHash } from "node:crypto";
35
+ import { readFileSync } from "node:fs";
36
+ import { resolve } from "node:path";
37
+ import { noopLogger } from "@stryker-mutator/util";
38
+ import { Cell, Workflow } from "@systemfsoftware/effect-cell-types";
39
+ import "@systemfsoftware/stryker-js-plugin-api/core";
40
+ import * as Exit from "effect/Exit";
41
+ import { pipe } from "effect/Function";
42
+ import * as Match from "effect/Match";
43
+ import * as S from "effect/Schema";
44
+ import { Stryker } from "@systemfsoftware/stryker-js-mutation-run";
45
+ import { forkCoreSchema } from "@systemfsoftware/stryker-js-mutation-run/config/fork-schema";
46
+ import { ConfigError, retrieveCause } from "@systemfsoftware/stryker-js-mutation-run/errors";
47
+ import { ExitClass, getPendingExitClasses, resolveExitCode } from "@systemfsoftware/stryker-js-mutation-run/exit-classification";
48
+ import { performance } from "node:perf_hooks";
49
+ import { format, inspect } from "node:util";
50
+ import { toRelativeNormalizedFileName } from "@systemfsoftware/stryker-js-mutation-run/mutants/incremental-differ";
51
+ //#region src/OutputMode.ts
52
+ /**
53
+ * The known tool variables. Narrow per the plan — exactly
54
+ * `['CLAUDECODE', 'CODEX_SANDBOX']` — and load-bearing rather than a
55
+ * fallback: they cover the PTY-allocating harnesses a stdin condition would
56
+ * have rescued.
57
+ */
58
+ const TOOL_VARIABLES = ["CLAUDECODE", "CODEX_SANDBOX"];
59
+ /**
60
+ * Resolves the output mode by R4 precedence. Pure — reads nothing, so it is
61
+ * fully testable; the caller supplies every input once at startup. The
62
+ * mutually-exclusive-flags case is a caller error, returned as a `failure` so
63
+ * the function stays total.
64
+ */
65
+ function resolveMode(input) {
66
+ if (input.text === true && input.json === true) return Result.fail(CliError.InvalidValue.make({
67
+ option: "json",
68
+ value: "text",
69
+ expected: "the \"--format text\" and \"--json\" flags are mutually exclusive — use one or the other",
70
+ kind: "flag"
71
+ }));
72
+ if (input.text === true) return Result.succeed({
73
+ mode: "human",
74
+ signal: "flag",
75
+ stdoutIsTTY: input.stdoutIsTTY
76
+ });
77
+ if (input.json === true) return Result.succeed({
78
+ mode: "machine",
79
+ signal: "flag",
80
+ stdoutIsTTY: input.stdoutIsTTY
81
+ });
82
+ if (input.envMode !== void 0 && input.envMode.length > 0) return Result.succeed({
83
+ mode: input.envMode === "machine" ? "machine" : "human",
84
+ signal: "env",
85
+ stdoutIsTTY: input.stdoutIsTTY
86
+ });
87
+ if (!input.stdoutIsTTY) return Result.succeed({
88
+ mode: "machine",
89
+ signal: "tty",
90
+ stdoutIsTTY: false
91
+ });
92
+ if (input.agent !== void 0 && input.agent.length > 0) return Result.succeed({
93
+ mode: "machine",
94
+ signal: "agent",
95
+ stdoutIsTTY: true
96
+ });
97
+ for (const variable of TOOL_VARIABLES) {
98
+ const value = input.toolVars?.[variable];
99
+ if (value !== void 0 && value.length > 0) return Result.succeed({
100
+ mode: "machine",
101
+ signal: "tool",
102
+ stdoutIsTTY: true
103
+ });
104
+ }
105
+ return Result.succeed({
106
+ mode: "human",
107
+ signal: "tty",
108
+ stdoutIsTTY: true
109
+ });
110
+ }
111
+ /**
112
+ * The progress bar's gate. Human mode on a non-TTY stdout (AE1) must not leak
113
+ * its control sequences into a pipe, and machine mode keeps stdout clean for
114
+ * the verdict envelope (R5). Decided from the resolved mode's own detection
115
+ * data — never a second `isTTY` probe.
116
+ */
117
+ function isProgressEnabled(resolved) {
118
+ return resolved.mode === "human" && resolved.stdoutIsTTY;
119
+ }
120
+ /**
121
+ * The log colouriser's gate (R8). Machine mode never emits colour, so a
122
+ * harness merging `2>&1` is not handed escape sequences it must strip, and
123
+ * `NO_COLOR` is honoured for the human path per the convention: any value
124
+ * other than an unset or empty variable disables colour.
125
+ */
126
+ function isColorEnabled(resolved, noColor) {
127
+ return resolved.mode === "human" && (noColor === void 0 || noColor.length === 0);
128
+ }
129
+ //#endregion
130
+ //#region src/OutputModeAdapter.ts
131
+ var OutputModeProbeTag = class extends Context.Service()("@systemfsoftware/stryker-js-cli/OutputModeAdapter/OutputModeProbeTag") {};
132
+ const OutputModeProbe = OutputModeProbeTag;
133
+ const OutputModeProbeLive = Layer.succeed(OutputModeProbe, OutputModeProbe.of({ detectMode: () => {
134
+ const envMode = process.env["STRYKER_MODE"];
135
+ const agent = process.env["AGENT"];
136
+ return Result.getOrThrow(resolveMode({
137
+ stdoutIsTTY: process.stdout.isTTY === true,
138
+ ...envMode !== void 0 ? { envMode } : {},
139
+ ...agent !== void 0 ? { agent } : {},
140
+ toolVars: Object.fromEntries(TOOL_VARIABLES.map((variable) => [variable, process.env[variable]]))
141
+ }));
142
+ } }));
143
+ //#endregion
144
+ //#region src/StreamProtocol.ts
145
+ /**
146
+ * The heartbeat interval (R19), matching Terraform's `apply_progress`
147
+ * cadence: long enough that a slow phase is not noisy, short enough that a
148
+ * consumer can tell "slow" from "hung" without waiting for a mutant event.
149
+ */
150
+ const TICK_INTERVAL_MS = 1e4;
151
+ //#endregion
152
+ //#region src/RunEventStreamAdapter.ts
153
+ var RunEventStreamPortTag = class extends Context.Service()("@systemfsoftware/stryker-js-cli/RunEventStreamAdapter/RunEventStreamPortTag") {};
154
+ const RunEventStreamPort = RunEventStreamPortTag;
155
+ const isTerminalEvent = (event) => event.kind === "verdict" || event.kind === "error" || event.kind === "help" || event.kind === "manifest";
156
+ /**
157
+ * The push adapter from the run's synchronous sink to the callback mailbox.
158
+ * `Queue.offer`/`Queue.end` on an unbounded queue never block or fail, so the
159
+ * sync sink can drive them with `Effect.runSync`. Defined at module scope,
160
+ * outside any Effect expression.
161
+ */
162
+ function queueEmit(queue) {
163
+ return {
164
+ single: (event) => {
165
+ Effect.runSync(Queue.offer(queue, event));
166
+ },
167
+ end: () => {
168
+ Effect.runSync(Queue.end(queue));
169
+ }
170
+ };
171
+ }
172
+ /**
173
+ * The drain writes the framed lines through the platform's `Stdio` stdout
174
+ * sink — `NodeStdio.layer` at the composition root supplies the service, and
175
+ * the sink owns the writable's backpressure, the scoped `'error'` listener a
176
+ * closed consumer raises, and the final `'finish'` wait (`endOnDone`). A
177
+ * write failure surfaces as a `PlatformError` which this catch swallows — a
178
+ * consumer closing the pipe must not replace the run's classed exit code
179
+ * (R31) — and the drain still completes only after every byte was handed to
180
+ * the OS (R30).
181
+ */
182
+ const drainOf = (stdio, framed) => Stream.run(framed, stdio.stdout({ endOnDone: true })).pipe(Effect.ignore);
183
+ /**
184
+ * Creates a run's stream. The drain is an Effect the composition root forks
185
+ * before the run; until then the sink is unbound and every push is dropped,
186
+ * which is what makes human mode — or an absent drain — inert without
187
+ * per-call probing (R2). The run's clock zero is read from the runtime so
188
+ * the adapter never touches the wall clock directly.
189
+ */
190
+ const makeRunEventStream = (stdio, resolved) => Effect.gen(function* () {
191
+ const runId = generateRunId();
192
+ const startedAt = yield* Clock.currentTimeMillis;
193
+ const state = {
194
+ mode: resolved.mode,
195
+ signal: resolved.signal,
196
+ emit: null,
197
+ headerWritten: false,
198
+ terminalWritten: false,
199
+ progress: {
200
+ completed: 0,
201
+ total: null
202
+ }
203
+ };
204
+ const registered = yield* Deferred.make();
205
+ const eventStream = Stream.callback((queue) => Effect.sync(() => {
206
+ state.emit = queueEmit(queue);
207
+ }).pipe(Effect.andThen(Deferred.succeed(registered, void 0))));
208
+ const tickStream = Stream.tick(TICK_INTERVAL_MS).pipe(Stream.filter(() => state.mode === "machine" && state.headerWritten && !state.terminalWritten), Stream.mapEffect(() => Effect.gen(function* () {
209
+ return {
210
+ kind: "tick",
211
+ elapsedMs: (yield* Clock.currentTimeMillis) - startedAt,
212
+ completed: state.progress.completed,
213
+ total: state.progress.total
214
+ };
215
+ })));
216
+ let terminalSeen = false;
217
+ const framed = Stream.merge(eventStream, tickStream, { haltStrategy: "either" }).pipe(Stream.filter((event) => {
218
+ if (terminalSeen) return false;
219
+ if (isTerminalEvent(event)) terminalSeen = true;
220
+ return true;
221
+ }), Stream.map((event) => `${JSON.stringify(event)}\n`));
222
+ const drain = drainOf(stdio, framed);
223
+ let drainFiber = null;
224
+ const sink = (event) => {
225
+ const emit = state.emit;
226
+ if (emit === null || state.terminalWritten || state.mode !== "machine") return;
227
+ if (!state.headerWritten) {
228
+ state.headerWritten = true;
229
+ emit.single({
230
+ kind: "stream",
231
+ schemaVersion: "1.0",
232
+ runId,
233
+ mode: state.mode,
234
+ signal: state.signal
235
+ });
236
+ }
237
+ switch (event.kind) {
238
+ case "stream":
239
+ case "tick": return;
240
+ case "phase": break;
241
+ case "plan":
242
+ state.progress = {
243
+ ...state.progress,
244
+ total: event.total
245
+ };
246
+ break;
247
+ case "mutant":
248
+ state.progress = {
249
+ completed: event.completed,
250
+ total: event.total
251
+ };
252
+ break;
253
+ case "verdict":
254
+ case "error":
255
+ case "help":
256
+ case "manifest":
257
+ emit.single(event);
258
+ state.terminalWritten = true;
259
+ emit.end();
260
+ return;
261
+ }
262
+ emit.single(event);
263
+ };
264
+ return {
265
+ sink,
266
+ runId,
267
+ startedAt,
268
+ isOpen: () => state.emit !== null && state.mode === "machine" && !state.terminalWritten,
269
+ ensureOpen: (openResolved) => {
270
+ if (state.headerWritten) return;
271
+ state.mode = openResolved.mode;
272
+ state.signal = openResolved.signal;
273
+ },
274
+ open: Effect.gen(function* () {
275
+ if (drainFiber === null) {
276
+ drainFiber = yield* Effect.forkDetach(drain);
277
+ yield* Effect.race(Deferred.await(registered), Fiber.await(drainFiber));
278
+ }
279
+ }),
280
+ closeAndDrain: Effect.gen(function* () {
281
+ state.terminalWritten = true;
282
+ state.emit?.end();
283
+ if (drainFiber !== null) yield* Fiber.join(drainFiber);
284
+ })
285
+ };
286
+ });
287
+ const RunEventStreamLive = Layer.effect(RunEventStreamPort, Effect.map(Stdio.Stdio, (stdio) => RunEventStreamPort.of({ createRunEventStream: (resolved) => makeRunEventStream(stdio, resolved) })));
288
+ function isObject(value) {
289
+ return typeof value === "object" && value !== null && !Array.isArray(value);
290
+ }
291
+ function stringField(node, key) {
292
+ const value = node[key];
293
+ return typeof value === "string" ? value : void 0;
294
+ }
295
+ function stringArrayField(node, key) {
296
+ const value = node[key];
297
+ if (!Array.isArray(value)) return [];
298
+ const strings = [];
299
+ for (const item of value) if (typeof item === "string") strings.push(item);
300
+ return strings;
301
+ }
302
+ /** The compiled shapes discriminate on `_tag`; read it once, off the record. */
303
+ function tagOf(node) {
304
+ return node["_tag"];
305
+ }
306
+ function walkParam(param, isOptional, out) {
307
+ if (!isObject(param)) return;
308
+ switch (tagOf(param)) {
309
+ case "Single":
310
+ describeSingle(param, isOptional, out);
311
+ return;
312
+ case "Map":
313
+ case "Transform":
314
+ walkParam(param["param"], isOptional, out);
315
+ return;
316
+ case "Optional":
317
+ walkParam(param["param"], true, out);
318
+ return;
319
+ case "Variadic":
320
+ walkParam(param["param"], isOptional, out);
321
+ return;
322
+ default: return;
323
+ }
324
+ }
325
+ const PRIMITIVE_KIND = {
326
+ Boolean: "boolean",
327
+ Choice: "choice",
328
+ Date: "date",
329
+ FileParse: "file",
330
+ FileSchema: "file",
331
+ FileText: "file",
332
+ Float: "float",
333
+ Integer: "integer",
334
+ KeyValuePair: "key=value",
335
+ None: "none",
336
+ Path: "path",
337
+ Redacted: "redacted",
338
+ String: "text"
339
+ };
340
+ function kindOf(primitive) {
341
+ const tag = stringField(primitive, "_tag");
342
+ return tag === void 0 ? "unknown" : PRIMITIVE_KIND[tag] ?? tag;
343
+ }
344
+ function choiceValues(primitive) {
345
+ const keys = primitive["choiceKeys"];
346
+ if (!Array.isArray(keys)) return;
347
+ const values = [];
348
+ for (const key of keys) if (typeof key === "string") values.push(key);
349
+ return values;
350
+ }
351
+ /** The allowed reporter names, read from the U9 registry — the same list the plugin loader accepts. */
352
+ const REPORTER_NAMES = strykerPlugins.filter((plugin) => plugin.kind === PluginKind.Reporter).map((plugin) => plugin.name);
353
+ /**
354
+ * v4 option descriptions are stored as `Option.some(string)` on the compiled
355
+ * `Single`; the walker unwraps the option.
356
+ */
357
+ function descriptionOf(single) {
358
+ const description = single["description"];
359
+ if (!isObject(description)) return "";
360
+ switch (tagOf(description)) {
361
+ case "Some": {
362
+ const value = description["value"];
363
+ return typeof value === "string" ? value : "";
364
+ }
365
+ default: return "";
366
+ }
367
+ }
368
+ function describeSingle(single, isOptional, out) {
369
+ const name = stringField(single, "name") ?? "";
370
+ const primitive = isObject(single["primitiveType"]) ? single["primitiveType"] : {};
371
+ const kind = kindOf(primitive);
372
+ const choices = name === "reporters" ? REPORTER_NAMES : kind === "choice" ? choiceValues(primitive) : void 0;
373
+ const description = descriptionOf(single);
374
+ const required = kind !== "boolean" && !isOptional;
375
+ const described = {
376
+ name,
377
+ aliases: stringArrayField(single, "aliases"),
378
+ kind,
379
+ required,
380
+ ...choices !== void 0 ? { choices } : {},
381
+ description
382
+ };
383
+ if (single["kind"] === "argument") {
384
+ out.args.push({
385
+ name,
386
+ kind,
387
+ required,
388
+ description
389
+ });
390
+ return;
391
+ }
392
+ out.flags.push(described);
393
+ }
394
+ function walkConfigNode(node, orderedParams, out) {
395
+ if (!isObject(node)) return;
396
+ switch (tagOf(node)) {
397
+ case "Param": {
398
+ const index = node["index"];
399
+ const param = typeof index === "number" ? orderedParams[index] : void 0;
400
+ if (param !== void 0) walkParam(param, false, out);
401
+ return;
402
+ }
403
+ case "Array":
404
+ if (Array.isArray(node["children"])) for (const child of node["children"]) walkConfigNode(child, orderedParams, out);
405
+ return;
406
+ case "Nested":
407
+ if (isObject(node["tree"])) walkConfigTree(node["tree"], orderedParams, out);
408
+ return;
409
+ default: return;
410
+ }
411
+ }
412
+ function walkConfigTree(tree, orderedParams, out) {
413
+ for (const key of Object.keys(tree)) walkConfigNode(tree[key], orderedParams, out);
414
+ }
415
+ function describeCommandNode(node) {
416
+ if (!isObject(node)) return;
417
+ const out = {
418
+ flags: [],
419
+ args: []
420
+ };
421
+ const config = node["config"];
422
+ if (isObject(config) && isObject(config["tree"])) {
423
+ const orderedParams = Array.isArray(config["orderedParams"]) ? config["orderedParams"] : [];
424
+ walkConfigTree(config["tree"], orderedParams, out);
425
+ }
426
+ const subcommands = [];
427
+ const grouped = node["subcommands"];
428
+ if (Array.isArray(grouped)) for (const group of grouped) {
429
+ if (!isObject(group) || !Array.isArray(group["commands"])) continue;
430
+ for (const child of group["commands"]) {
431
+ const described = describeCommandNode(child);
432
+ if (described !== void 0) subcommands.push(described);
433
+ }
434
+ }
435
+ return {
436
+ name: stringField(node, "name") ?? "",
437
+ description: typeof node["description"] === "string" ? node["description"] : "",
438
+ options: out.flags,
439
+ args: out.args,
440
+ subcommands
441
+ };
442
+ }
443
+ /**
444
+ * Builds the manifest document for a command, walking its compiled form (the
445
+ * same structure the parser matches against). `version` is the tool version,
446
+ * passed in so this module stays free of package state.
447
+ */
448
+ function buildLLMSManifest(command, version) {
449
+ const root = describeCommandNode(command) ?? {
450
+ name: "",
451
+ description: "",
452
+ options: [],
453
+ args: [],
454
+ subcommands: []
455
+ };
456
+ return {
457
+ schemaVersion: "1.0",
458
+ tool: root.name,
459
+ version,
460
+ commands: [root]
461
+ };
462
+ }
463
+ /** The manifest as one JSON document, ready for stdout — the U4 convention. */
464
+ function emitLLMSManifest(command, version) {
465
+ return JSON.stringify(buildLLMSManifest(command, version));
466
+ }
467
+ //#endregion
468
+ //#region src/OutputModeConsoleState.ts
469
+ /**
470
+ * U6 — the machine-mode `Console` layer (KTD3, R7).
471
+ *
472
+ * The v4 CLI renders help and errors through an ANSI renderer and prints them
473
+ * with the `Console` reference — and there is no seam to intercept: the
474
+ * document is written *before* the failure propagates. Machine mode therefore
475
+ * replaces the `Console` reference itself with a capturing implementation:
476
+ * every write lands in an in-memory buffer instead of the real stdout/stderr,
477
+ * and the terminating bootstrap (StrykerCliHandler.ts) emits the buffer as
478
+ * one JSON envelope at teardown. Human mode keeps the default console so the
479
+ * framework's prose rendering is untouched.
480
+ *
481
+ * The v4 `Console.Console` interface is the sync `globalThis.console` shape
482
+ * (the v3 service's effect-returning surface became the module-level
483
+ * wrapper functions), so the capture implementation needs no `unsafe`
484
+ * mirror — every method stores into the buffer directly.
485
+ */
486
+ const capturedConsoleChunks = [];
487
+ const countByLabel = /* @__PURE__ */ new Map();
488
+ const timeByLabel = /* @__PURE__ */ new Map();
489
+ function formatArgs(args) {
490
+ return format(...args);
491
+ }
492
+ function captureSync(args) {
493
+ capturedConsoleChunks.push(formatArgs(args));
494
+ }
495
+ function captureAssert(condition, args) {
496
+ if (!condition) capturedConsoleChunks.push(`Assertion failed: ${formatArgs(args)}`);
497
+ }
498
+ function captureCount(label) {
499
+ const key = label ?? "default";
500
+ const next = (countByLabel.get(key) ?? 0) + 1;
501
+ countByLabel.set(key, next);
502
+ capturedConsoleChunks.push(`${key}: ${next}`);
503
+ }
504
+ function captureTimeEnd(label, now) {
505
+ const key = label ?? "default";
506
+ const started = timeByLabel.get(key);
507
+ if (started !== void 0) {
508
+ timeByLabel.delete(key);
509
+ capturedConsoleChunks.push(`${key}: ${now - started}ms`);
510
+ }
511
+ }
512
+ function captureTrace(args) {
513
+ capturedConsoleChunks.push(`Trace: ${formatArgs(args)}\n${(/* @__PURE__ */ new Error()).stack ?? ""}`);
514
+ }
515
+ const capturingConsole = {
516
+ assert: (condition, ...args) => captureAssert(condition, args),
517
+ clear: () => {},
518
+ count: (label) => captureCount(label),
519
+ countReset: (label) => countByLabel.delete(label ?? "default"),
520
+ debug: (...args) => captureSync(args),
521
+ dir: (item, options) => capturedConsoleChunks.push(inspect(item, options)),
522
+ dirxml: (item) => capturedConsoleChunks.push(inspect(item)),
523
+ error: (...args) => captureSync(args),
524
+ group: () => {},
525
+ groupCollapsed: () => {},
526
+ groupEnd: () => {},
527
+ info: (...args) => captureSync(args),
528
+ log: (...args) => captureSync(args),
529
+ table: (tabularData) => capturedConsoleChunks.push(inspect(tabularData, {
530
+ colors: false,
531
+ depth: null
532
+ })),
533
+ time: (label) => timeByLabel.set(label ?? "default", performance.now()),
534
+ timeEnd: (label) => captureTimeEnd(label, performance.now()),
535
+ timeLog: (label, ...args) => {
536
+ const key = label ?? "default";
537
+ const started = timeByLabel.get(key);
538
+ if (started !== void 0) capturedConsoleChunks.push(`${key}: ${performance.now() - started}ms ${formatArgs(args)}`);
539
+ },
540
+ trace: (...args) => captureTrace(args),
541
+ warn: (...args) => captureSync(args)
542
+ };
543
+ /**
544
+ * The machine-mode `Console` layer. Building it clears the capture buffer so
545
+ * every run starts empty; the terminating bootstrap reads the buffer back
546
+ * through `readCapturedConsole` at teardown. A `Layer` is already lazy, so
547
+ * the layer is a value: the reset effect runs when the layer is built.
548
+ *
549
+ * The layer must replace the `Console` reference the module-level
550
+ * `Console.log`/`Console.error` wrappers read through their fiber context;
551
+ * v4 reads the override the same way it reads any provided service, so a
552
+ * plain provide is sufficient — no special `setConsole`-style primitive
553
+ * exists any more. The reference's identifier is `never` (it carries no
554
+ * requirement), so the layer's type is too.
555
+ *
556
+ * Human mode provides no Console binding at all: effect's own default
557
+ * console delegates every method to the global console, which is exactly the
558
+ * prose rendering a human-mode run uses. Mirroring it here would reimplement
559
+ * the library default (V.7).
560
+ */
561
+ const machineConsoleLayer = Layer.effect(Console.Console, Effect.sync(() => {
562
+ resetCapturedConsole();
563
+ return capturingConsole;
564
+ }));
565
+ /**
566
+ * The text captured so far by the machine console layer, joined into one
567
+ * document the way a terminal would have rendered it (one console call per
568
+ * line). Empty in human mode.
569
+ */
570
+ function readCapturedConsole() {
571
+ return capturedConsoleChunks.join("\n");
572
+ }
573
+ /**
574
+ * Clears the capture buffer and the count/time tables. Called when the
575
+ * machine layer is constructed so every run starts empty.
576
+ */
577
+ function resetCapturedConsole() {
578
+ capturedConsoleChunks.length = 0;
579
+ countByLabel.clear();
580
+ timeByLabel.clear();
581
+ }
582
+ //#endregion
583
+ //#region src/Survivors.workflow.ts
584
+ /**
585
+ * The decision's helper closure reaches three language built-ins the purity
586
+ * gate cannot resolve as globals, so each is bound at module scope and the
587
+ * helpers reference the bindings: `isArray` keeps `Array.isArray`'s narrow,
588
+ * `objectEntries`/`objectFromEntries`/`objectKeys` keep `Object`'s trio, and
589
+ * `stringify` keeps `JSON.stringify`'s exact text.
590
+ */
591
+ const isArray = Array.isArray;
592
+ const { entries: objectEntries, fromEntries: objectFromEntries, keys: objectKeys } = Object;
593
+ const stringify = JSON.stringify;
594
+ /**
595
+ * U8 — survivor re-run admission (R10, R11, KTD6, KTD7).
596
+ *
597
+ * The `--survivors` run re-tests exactly the mutants that survived a previous
598
+ * run. Its input is the previous run's mutation report, and the run is
599
+ * admitted only when a single structural hash of the resolved options, the
600
+ * recorded framework version, and the per-file source content all match the
601
+ * current run (KTD6). Because thresholds live inside the resolved options, a
602
+ * threshold-only change is caught for free. Every rejection exits 2 with a
603
+ * remediation naming the full run to do first; zero survivors exits 0 with a
604
+ * null score and writes no new report (AE3); and a report written by a
605
+ * survivors run is never admitted as the input of another survivors run
606
+ * (KTD7), so chaining two survivors runs fails loudly instead of re-testing a
607
+ * shrunken or stale set.
608
+ *
609
+ * All functions here are pure over their inputs — no file I/O — so the
610
+ * admission logic is fixture-testable in seconds.
611
+ */
612
+ /** The path a `--survivors` run reads when no `survivorsPriorReport` is configured. */
613
+ const DEFAULT_SURVIVORS_PRIOR_REPORT = "reports/mutation-report.json";
614
+ /**
615
+ * The remediation every rejection carries (R10): name the full run to do
616
+ * first, never the survivors run itself.
617
+ */
618
+ const SURVIVORS_RUN_FIRST_REMEDIATION = "run a full `stryker run` first, then re-run with --survivors";
619
+ /**
620
+ * The survivors-run bookkeeping keys carried in the resolved options. They
621
+ * are run mechanics, not configuration: a survivors run adds them, so without
622
+ * stripping them the current run's hash would differ from the prior full
623
+ * run's hash for the very same configuration. Their presence in a report's
624
+ * embedded config is also the marker that the report was produced by a
625
+ * survivors run (KTD7).
626
+ */
627
+ const SURVIVORS_BOOKKEEPING_KEYS = ["survivorsPriorReport"];
628
+ function isRecord(value) {
629
+ return typeof value === "object" && value !== null && !isArray(value);
630
+ }
631
+ /**
632
+ * The resolved options without the survivors-run bookkeeping keys, so both
633
+ * sides of the admission hash describe the same configuration.
634
+ */
635
+ function stripSurvivorsKeys(config) {
636
+ if (!isRecord(config)) return {};
637
+ const rest = { ...config };
638
+ for (const key of SURVIVORS_BOOKKEEPING_KEYS) delete rest[key];
639
+ return rest;
640
+ }
641
+ /**
642
+ * A report written by a survivors run embeds the bookkeeping key in its
643
+ * `config` (the report helper writes the resolved options). Such a report is
644
+ * never a valid input for another survivors run (KTD7): without this check
645
+ * the second run would either re-read a shrunken set or re-test a stale one.
646
+ */
647
+ function wasProducedBySurvivorsRun(priorReport) {
648
+ const config = priorReport.config;
649
+ return isRecord(config) && "survivorsPriorReport" in config;
650
+ }
651
+ /**
652
+ * The sha256 content hash both sides of the admission comparison use: the
653
+ * current run's source files and the sources the prior report embeds. The
654
+ * digest capability is supplied by the caller.
655
+ */
656
+ function sourceContentHash(content, hash) {
657
+ return hash(content);
658
+ }
659
+ /**
660
+ * Serializes the hash input with keys sorted at every level, so the hash is a
661
+ * pure function of the data and not of object key insertion order. The shape
662
+ * is pinned by a golden snapshot test: a serialization change here fails
663
+ * loudly instead of silently invalidating every prior report in the wild.
664
+ */
665
+ function serializeSurvivorsHashInput(input) {
666
+ return stringify(sortKeys(input));
667
+ }
668
+ function sortKeys(value) {
669
+ if (isArray(value)) return value.map(sortKeys);
670
+ if (isRecord(value)) return objectFromEntries(objectKeys(value).sort().map((key) => [key, sortKeys(value[key])]));
671
+ return value;
672
+ }
673
+ /** The single structural hash the admission compares (KTD6). */
674
+ function structuralHash(input, hash) {
675
+ return hash(serializeSurvivorsHashInput(input));
676
+ }
677
+ /**
678
+ * Converts a report mutant (1-based schema location) into the internal mutant
679
+ * shape a run consumes (0-based positions, absolute file name) — the exact
680
+ * inverse of `objectUtils.toSchemaLocation` and the same shift the
681
+ * incremental report reader applies (`project-reader.ts`). Mutants without a
682
+ * replacement fall back to their mutator name, the same convention the
683
+ * incremental differ uses.
684
+ */
685
+ function reportMutantToMutant(file, mutant, resolveAbsolutePath) {
686
+ return {
687
+ id: mutant.id,
688
+ fileName: resolveAbsolutePath(file),
689
+ mutatorName: mutant.mutatorName,
690
+ replacement: mutant.replacement ?? mutant.mutatorName,
691
+ location: {
692
+ start: {
693
+ line: mutant.location.start.line - 1,
694
+ column: mutant.location.start.column - 1
695
+ },
696
+ end: {
697
+ line: mutant.location.end.line - 1,
698
+ column: mutant.location.end.column - 1
699
+ }
700
+ }
701
+ };
702
+ }
703
+ /**
704
+ * The survivors of the prior report: exactly the mutants whose status is
705
+ * `Survived`, converted to the internal mutant shape so a run can re-test
706
+ * them.
707
+ */
708
+ function extractSurvivors(priorReport, resolveAbsolutePath) {
709
+ const survivors = [];
710
+ for (const [file, fileResult] of objectEntries(priorReport.files)) for (const mutant of fileResult.mutants) if (mutant.status === "Survived") survivors.push(reportMutantToMutant(file, mutant, resolveAbsolutePath));
711
+ return survivors;
712
+ }
713
+ /**
714
+ * The survivor spans as `file:startLine:startCol-endLine:endCol` mutate
715
+ * ranges: the report's 1-based lines with the internal 0-based columns,
716
+ * relative file names, deduplicated in first-seen order.
717
+ */
718
+ function survivorMutateSpans(survivors) {
719
+ const spans = [];
720
+ const seen = /* @__PURE__ */ new Set();
721
+ for (const survivor of survivors) {
722
+ const file = toRelativeNormalizedFileName(survivor.fileName);
723
+ const { start, end } = survivor.location;
724
+ const span = `${file}:${start.line + 1}:${start.column}-${end.line + 1}:${end.column}`;
725
+ if (!seen.has(span)) {
726
+ seen.add(span);
727
+ spans.push(span);
728
+ }
729
+ }
730
+ return spans;
731
+ }
732
+ const NO_REPORT_DETAIL = "No prior mutation report found — a --survivors run needs the report of a previous run.";
733
+ const SURVIVORS_RUN_SOURCE_DETAIL = "The prior mutation report was itself produced by a --survivors run, so it is not a valid input for another one.";
734
+ const MISMATCH_DETAIL = "The prior mutation report does not match the current run (resolved options, framework version, or source content differ).";
735
+ /** The per-file source hashes of the sources the prior report embeds. */
736
+ function priorSourceHashes(priorReport, hashContent) {
737
+ return objectFromEntries(objectEntries(priorReport.files).map(([file, fileResult]) => [file, sourceContentHash(fileResult.source, hashContent)]));
738
+ }
739
+ /**
740
+ * Whether the admission hashes agree: the prior report's embedded resolved
741
+ * options, framework version and source content against the current run's.
742
+ */
743
+ function hashesMatch(priorReport, input) {
744
+ return structuralHash({
745
+ resolvedOptions: stripSurvivorsKeys(priorReport.config),
746
+ frameworkVersion: priorReport.framework?.version,
747
+ sourceContentHashes: priorSourceHashes(priorReport, input.hashContent)
748
+ }, input.hashContent) === structuralHash({
749
+ resolvedOptions: stripSurvivorsKeys(input.currentConfig),
750
+ frameworkVersion: input.frameworkVersion,
751
+ sourceContentHashes: input.sourceContentHashes
752
+ }, input.hashContent);
753
+ }
754
+ const rejection = (reason, detail) => ({
755
+ kind: "reject",
756
+ reason,
757
+ remediation: `${detail} ${SURVIVORS_RUN_FIRST_REMEDIATION}`
758
+ });
759
+ function admissionVerdict(input) {
760
+ const priorReport = input.priorReport;
761
+ if (priorReport === void 0) return rejection("no-report", NO_REPORT_DETAIL);
762
+ if (wasProducedBySurvivorsRun(priorReport)) return rejection("mismatch", SURVIVORS_RUN_SOURCE_DETAIL);
763
+ const survivors = extractSurvivors(priorReport, input.resolveAbsolutePath);
764
+ if (survivors.length === 0) return { kind: "no-survivors" };
765
+ if (!hashesMatch(priorReport, input)) return rejection("mismatch", MISMATCH_DETAIL);
766
+ return {
767
+ kind: "admit",
768
+ survivors
769
+ };
770
+ }
771
+ const SurvivorsAdmissionTypeId = Symbol.for("@systemfsoftware/stryker-js-cli/SurvivorsAdmission");
772
+ var Admitted = class extends S.TaggedClass()("Admitted", { survivors: S.Array(S.Struct({
773
+ id: S.String,
774
+ fileName: S.String,
775
+ mutatorName: S.String,
776
+ replacement: S.String,
777
+ location: S.Struct({
778
+ start: S.Struct({
779
+ line: S.Finite,
780
+ column: S.Finite
781
+ }),
782
+ end: S.Struct({
783
+ line: S.Finite,
784
+ column: S.Finite
785
+ })
786
+ })
787
+ })) }) {
788
+ [SurvivorsAdmissionTypeId] = SurvivorsAdmissionTypeId;
789
+ };
790
+ var NoSurvivors = class extends S.TaggedClass()("NoSurvivors", {}) {
791
+ [SurvivorsAdmissionTypeId] = SurvivorsAdmissionTypeId;
792
+ };
793
+ S.Union([Admitted, NoSurvivors]);
794
+ var SurvivorsRejection = class extends S.TaggedError()("SurvivorsRejection", {
795
+ reason: S.Literals(["no-report", "mismatch"]),
796
+ remediation: S.String
797
+ }) {
798
+ [SurvivorsAdmissionTypeId] = SurvivorsAdmissionTypeId;
799
+ };
800
+ /**
801
+ * The survivors admission decision: the classification `admissionVerdict`
802
+ * produces, assigned to the workflow channels — one arm per kind, no guard
803
+ * chain. A missing report, a survivors-sourced report and a hash mismatch are
804
+ * the same reject outcome with different reasons; only the rejection's
805
+ * remediation names the full run to do first (R10).
806
+ */
807
+ const admitSurvivorsRun = Workflow.make((command) => Match.value(admissionVerdict(command)).pipe(Match.discriminator("kind")("reject", (verdict) => Result.fail(SurvivorsRejection.make({
808
+ reason: verdict.reason,
809
+ remediation: verdict.remediation
810
+ }))), Match.discriminator("kind")("no-survivors", () => Result.succeed(NoSurvivors.make())), Match.discriminator("kind")("admit", (verdict) => Result.succeed(Admitted.make({ survivors: verdict.survivors }))), Match.exhaustive));
811
+ //#endregion
812
+ //#region src/SurvivorsExit.ts
813
+ /** The exit class a rejected survivors run exits with (R6: exit 2). */
814
+ const SURVIVORS_REJECT_EXIT_CLASS = ExitClass.ConfigError;
815
+ S.Union([S.TaggedStruct("run", {
816
+ options: S.Any,
817
+ survivors: S.Boolean
818
+ }), S.TaggedStruct("llms", { document: S.Any })]);
819
+ //#endregion
820
+ //#region src/StrykerCliExecutor.ts
821
+ /**
822
+ * The default run: binds the host-resolved run options (the sink, the mode,
823
+ * the timing) to a fresh `Stryker` and runs mutation testing.
824
+ */
825
+ const defaultRunMutationTest = (hostOptions) => (options) => new Stryker(options, hostOptions).runMutationTest();
826
+ /**
827
+ * The machine-mode `Console` layer, bundled so the transport (which resolves
828
+ * the mode) can provide it without importing the state cell. Human mode
829
+ * provides no layer — effect's own default console is the prose rendering
830
+ * (OutputModeConsoleState.ts).
831
+ */
832
+ const strykerCliConsoleLayers = { machine: machineConsoleLayer };
833
+ const SIGNAL_NUMBERS = Object.freeze({
834
+ SIGINT: 2,
835
+ SIGTERM: 15
836
+ });
837
+ const hashContent = (content) => createHash("sha256").update(content, "utf-8").digest("hex");
838
+ const resolveAbsolutePath = (file) => resolve(file);
839
+ /**
840
+ * The host options a run is bound to: the sink, the mode, the timing and the
841
+ * log descriptor chosen by the mode — machine mode keeps stdout exclusively
842
+ * for the NDJSON stream, so the logging backend is pointed at stderr; human
843
+ * mode keeps the stdout sink. The fix is the descriptor, never the log level.
844
+ */
845
+ function hostOptionsOf(mode, stream) {
846
+ return {
847
+ loggerConsoleOut: mode.mode === "machine" ? process.stderr : process.stdout,
848
+ showColors: isColorEnabled(mode, process.env["NO_COLOR"]),
849
+ runEventSink: stream.sink,
850
+ runId: stream.runId,
851
+ resolvedMode: mode,
852
+ progressEnabled: isProgressEnabled(mode),
853
+ clearTextEnabled: mode.mode === "human",
854
+ runStartedAt: stream.startedAt,
855
+ reporterPluginModules: [import.meta.resolve("@systemfsoftware/stryker-js-mutation-report/stryker-plugins")]
856
+ };
857
+ }
858
+ /**
859
+ * The survivors admission, as a description whose phases chain by type and
860
+ * read in the order they run. The read gathers the admission's whole input
861
+ * product — resolved options, prior report and the current source hashes —
862
+ * across its interior and stashes the shell context the write dispatches on
863
+ * into the executor-owned `runContext` ref; `decode` packages exactly the
864
+ * workflow input; `admitSurvivorsRun` is the decide phase; `encode` is the
865
+ * identity because write receives the outcome as-is; the write reads the
866
+ * stashed context back and dispatches the decision to the verdict/run,
867
+ * failing the run with a rejection.
868
+ */
869
+ const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext) => pipe(Cell.read((cliOptions) => Effect.promise(() => resolveSurvivorsRunOptions(cliOptions)).pipe(Effect.flatMap((resolvedOptions) => {
870
+ const priorReportPath = priorReportPathOf(resolvedOptions);
871
+ const priorReport = readPriorReport(priorReportPath);
872
+ return Ref.set(runContext, {
873
+ resolvedOptions,
874
+ priorReportPath
875
+ }).pipe(Effect.as({
876
+ resolvedOptions,
877
+ priorReport,
878
+ priorReportPath,
879
+ sourceContentHashes: sourceContentHashesOf(priorReport)
880
+ }));
881
+ }))), Cell.decode(({ resolvedOptions, priorReport, sourceContentHashes }) => Result.succeed({
882
+ priorReport,
883
+ currentConfig: resolvedOptions,
884
+ frameworkVersion: strykerVersion,
885
+ sourceContentHashes,
886
+ hashContent,
887
+ resolveAbsolutePath
888
+ })), Cell.decide(admitSurvivorsRun), Cell.encode((outcome) => outcome), Cell.write((outcome) => Effect.flatMap(Ref.get(runContext), (context) => {
889
+ if (context === void 0) return Effect.die("the survivors admission read must run before its write");
890
+ const { resolvedOptions, priorReportPath } = context;
891
+ return Result.match(outcome, {
892
+ onSuccess: (decision) => Match.value(decision).pipe(Match.tag("NoSurvivors", () => Effect.sync(() => emitEmptySurvivorsVerdict(stream, mode, resolvedOptions))), Match.tag("Admitted", (admitted) => {
893
+ const restricted = {
894
+ ...resolvedOptions,
895
+ survivors: admitted.survivors,
896
+ mutate: survivorMutateSpans(admitted.survivors),
897
+ survivorsPriorReport: priorReportPath,
898
+ incremental: false
899
+ };
900
+ return Effect.promise(() => runMutationTest(restricted));
901
+ }), Match.orElse(() => Effect.die("unreachable admission decision variant"))),
902
+ onFailure: (rejection) => Effect.fail(rejection)
903
+ });
904
+ })));
905
+ /**
906
+ * The `--survivors` request: re-test exactly the prior report's survivor set.
907
+ * The survivors flag was parsed as a boolean; the admission decides between
908
+ * running the survivors and the plain pipeline. The chain's order is carried by
909
+ * the description's phase types; the run's resolved context cell is created
910
+ * here, beside the description it feeds.
911
+ */
912
+ function runSurvivorsAdmission(runMutationTest, stream, mode, cliOptions) {
913
+ return Effect.gen(function* () {
914
+ const admissionContext = yield* Ref.make(void 0);
915
+ return yield* Cell.apply(survivorsAdmissionDescription(runMutationTest, stream, mode, admissionContext), cliOptions);
916
+ });
917
+ }
918
+ /**
919
+ * Resolves the current options the same way the pipeline does — defaults +
920
+ * config file + CLI, validated against the fork schema (which carries the
921
+ * survivors-run properties). The admission hash compares these resolved
922
+ * options against the prior report's embedded config.
923
+ */
924
+ function resolveSurvivorsRunOptions(cliOptions) {
925
+ return new ConfigReader(noopLogger, new OptionsValidator(forkCoreSchema, noopLogger)).readConfig(cliOptions);
926
+ }
927
+ /**
928
+ * The prior report a `--survivors` run reads: the `survivorsPriorReport`
929
+ * config option when set, else the default path. The report path is run
930
+ * bookkeeping, never a CLI flag.
931
+ */
932
+ function priorReportPathOf(resolved) {
933
+ const configured = resolved["survivorsPriorReport"];
934
+ return typeof configured === "string" ? configured : DEFAULT_SURVIVORS_PRIOR_REPORT;
935
+ }
936
+ /**
937
+ * A report is usable only when it parses and carries a `files` dictionary; a
938
+ * missing, unreadable or misshapen report is the `no-report` rejection.
939
+ */
940
+ function isMutationTestResultShape(value) {
941
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
942
+ return "files" in value && typeof value.files === "object" && value.files !== null && !Array.isArray(value.files);
943
+ }
944
+ function readPriorReport(priorReportPath) {
945
+ try {
946
+ const raw = readFileSync(priorReportPath, "utf-8");
947
+ const parsed = JSON.parse(raw);
948
+ return isMutationTestResultShape(parsed) ? parsed : void 0;
949
+ } catch {
950
+ return;
951
+ }
952
+ }
953
+ function readSourceFile(file) {
954
+ try {
955
+ return readFileSync(file, "utf-8");
956
+ } catch {
957
+ return "";
958
+ }
959
+ }
960
+ /**
961
+ * The per-file content hashes of the current sources, keyed by the relative
962
+ * file names the prior report uses — the current side of the admission
963
+ * comparison (`admitSurvivorsRun` hashes the prior side from the sources the
964
+ * report embeds).
965
+ */
966
+ function sourceContentHashesOf(priorReport) {
967
+ const hashes = {};
968
+ if (priorReport === void 0) return hashes;
969
+ for (const file of Object.keys(priorReport.files)) hashes[file] = sourceContentHash(readSourceFile(file), hashContent);
970
+ return hashes;
971
+ }
972
+ /**
973
+ * Machine mode emits the U4 verdict envelope for a run that produced no
974
+ * mutants and no report file: a `--survivors` run with zero survivors (AE3)
975
+ * or a successful `--dryRunOnly` run that ended before the mutation
976
+ * pipeline. The envelope carries a null score and an empty mutant list and is
977
+ * written as the terminal `verdict` line of the stdout stream (U6), carrying
978
+ * the run id the stream header already opened with (KTD11 — never a fresh
979
+ * id). Human mode prints nothing (the sink drops in human mode).
980
+ */
981
+ function emitNullScoreVerdict(stream, mode, thresholds, config) {
982
+ const report = {
983
+ schemaVersion: "1.0",
984
+ files: {},
985
+ thresholds,
986
+ projectRoot: process.cwd(),
987
+ config,
988
+ framework: {
989
+ name: "StrykerJS",
990
+ version: strykerVersion
991
+ }
992
+ };
993
+ const envelope = buildVerdictEnvelope(report, mode.mode, mode.signal, stream.runId);
994
+ stream.sink({
995
+ kind: "verdict",
996
+ ...envelope
997
+ });
998
+ }
999
+ /**
1000
+ * The `--survivors` zero-survivor path: the prior report held no survivors,
1001
+ * so the run emits the null-score verdict without starting the pipeline. The
1002
+ * full resolved options ride along as the report's embedded config (KTD7).
1003
+ */
1004
+ function emitEmptySurvivorsVerdict(stream, mode, resolved) {
1005
+ emitNullScoreVerdict(stream, mode, resolved.thresholds, resolved);
1006
+ }
1007
+ /**
1008
+ * The contextual remediation for a failure, picked from the cause's shape:
1009
+ * signal terminations (POSIX `128 + n`) are called out as interruptions,
1010
+ * usage/parse errors point at `--help`, config errors name the offending file
1011
+ * (ConfigError messages carry it), and rejected survivors runs name the full
1012
+ * run to do first. Everything else points at the report file and the verdict
1013
+ * envelope, which is where a runtime failure's detail already is.
1014
+ */
1015
+ function remediationFor(exit, code) {
1016
+ if (code > 128) return "the run was interrupted by a signal; re-run it to continue";
1017
+ const value = failureValue(exit);
1018
+ if (value !== void 0) {
1019
+ if (CliError.isCliError(value)) return "re-run with --help to see the full usage";
1020
+ if (value instanceof ConfigError) return `check the config file: ${value.message}`;
1021
+ if (S.is(SurvivorsRejection)(value)) return value.remediation;
1022
+ }
1023
+ return "see --reportFile or the verdict envelope on stdout";
1024
+ }
1025
+ /**
1026
+ * The failure's own text, used when the capture buffer is empty — a failure
1027
+ * stryker reported through its own logger rather than the framework's
1028
+ * `Console`. Falls back to a rendered cause.
1029
+ */
1030
+ function describeFailure(exit) {
1031
+ if (Exit.isFailure(exit)) {
1032
+ const value = failureValue(exit);
1033
+ if (value !== void 0) {
1034
+ if (S.is(SurvivorsRejection)(value)) return value.remediation;
1035
+ if (value instanceof Error) return value.message;
1036
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") return String(value);
1037
+ return Object.prototype.toString.call(value);
1038
+ }
1039
+ return Cause.pretty(exit.cause);
1040
+ }
1041
+ return "";
1042
+ }
1043
+ /**
1044
+ * The argument the framework reports it does not know, named the way the wire
1045
+ * contract spells it. The v4 parser fails wrapped in a ShowHelp whose errors
1046
+ * carry the offending flag or operand; when the unrecognized flag was given a
1047
+ * separate value (`--format text`), the value is the token the old parser
1048
+ * reported, so the token after the flag is named when one was given.
1049
+ */
1050
+ function unrecognizedArgumentOf(exit, argv) {
1051
+ if (!Exit.isFailure(exit)) return;
1052
+ const value = failureValue(exit);
1053
+ if (value === void 0 || !CliError.isCliError(value)) return;
1054
+ const errors = S.is(CliError.ShowHelp)(value) ? value.errors : [value];
1055
+ for (const error of errors) {
1056
+ if (S.is(CliError.UnrecognizedOption)(error)) {
1057
+ const at = argv.indexOf(error.option);
1058
+ const next = at >= 0 ? argv[at + 1] : void 0;
1059
+ return next !== void 0 && !next.startsWith("-") ? next : error.option;
1060
+ }
1061
+ if (S.is(CliError.UnexpectedArgument)(error)) return error.arguments[0];
1062
+ if (S.is(CliError.UnknownSubcommand)(error)) return error.subcommand;
1063
+ }
1064
+ }
1065
+ /**
1066
+ * The first typed error in the exit's cause. The framework fails with
1067
+ * `Cause.fail` (usage errors); the run handler is `Effect.promise`, whose
1068
+ * rejected promises surface as *defects* (`Die` reasons) rather than
1069
+ * failures — so stryker's own ConfigError/StrykerError values arrive there
1070
+ * and must be read from the cause's `Die` reasons.
1071
+ */
1072
+ function failureValue(exit) {
1073
+ if (!Exit.isFailure(exit)) return;
1074
+ const failure = Cause.findErrorOption(exit.cause);
1075
+ if (Option.isSome(failure)) return failure.value;
1076
+ const dieReason = exit.cause.reasons.find(Cause.isDieReason);
1077
+ return dieReason === void 0 ? void 0 : dieReason.defect;
1078
+ }
1079
+ function buildErrorEnvelope(exit, code, captured, argv) {
1080
+ const unrecognized = unrecognizedArgumentOf(exit, argv);
1081
+ return {
1082
+ schemaVersion: "1.0",
1083
+ code,
1084
+ error: unrecognized !== void 0 ? `Received unknown argument: '${unrecognized}'` : captured.length > 0 ? captured : describeFailure(exit),
1085
+ remediation: remediationFor(exit, code)
1086
+ };
1087
+ }
1088
+ /**
1089
+ * Emits the machine-mode output from the run's finalizer — it runs on
1090
+ * success, failure and interruption alike (R30): a failed run writes the
1091
+ * `error` terminal event as the last line of the stdout stream; a successful
1092
+ * run whose only console output was the framework's help/version rendering
1093
+ * emits that captured document as the `help` terminal event, so `--help` in
1094
+ * machine mode never leaks an ANSI document. A successful run with an empty
1095
+ * buffer (the normal verdict path) emits nothing extra — the run already
1096
+ * wrote its terminal `verdict` line through the same module — unless the
1097
+ * stream is still open, which means the run never reached a verdict (the
1098
+ * `--dryRunOnly` early return): then a null-score `verdict` closes the
1099
+ * stream so the last stdout line is always a terminal event (R5).
1100
+ */
1101
+ function emitMachineModeOutput(stream, mode, exit, code, argv) {
1102
+ const captured = readCapturedConsole();
1103
+ const value = failureValue(exit);
1104
+ if (Exit.isFailure(exit) && S.is(CliError.ShowHelp)(value) && value.errors.length === 0) {
1105
+ const document = {
1106
+ kind: "help",
1107
+ schemaVersion: "1.0",
1108
+ code: 0,
1109
+ help: captured
1110
+ };
1111
+ stream.sink(document);
1112
+ return;
1113
+ }
1114
+ if (Exit.isFailure(exit)) {
1115
+ stream.sink({
1116
+ kind: "error",
1117
+ ...buildErrorEnvelope(exit, code, captured, argv)
1118
+ });
1119
+ return;
1120
+ }
1121
+ if (captured.length > 0) {
1122
+ const document = {
1123
+ kind: "help",
1124
+ schemaVersion: "1.0",
1125
+ code: 0,
1126
+ help: captured
1127
+ };
1128
+ stream.sink(document);
1129
+ return;
1130
+ }
1131
+ if (stream.isOpen()) emitNullScoreVerdict(stream, mode, defaultOptions.thresholds, {});
1132
+ }
1133
+ /**
1134
+ * A rejected config reaches the finalizer as a typed failure or as a defect
1135
+ * depending on where the validator threw, and typed-inject may have wrapped
1136
+ * it, so both channels are searched and each candidate is unwrapped.
1137
+ */
1138
+ function carriesConfigError(cause) {
1139
+ for (const reason of cause.reasons) {
1140
+ const candidate = Cause.isFailReason(reason) ? reason.error : Cause.isDieReason(reason) ? reason.defect : void 0;
1141
+ if (candidate !== void 0 && (candidate instanceof ConfigError || retrieveCause(candidate) instanceof ConfigError)) return true;
1142
+ }
1143
+ return false;
1144
+ }
1145
+ /**
1146
+ * Classifies a failed run for the finalizer: usage/parse failures
1147
+ * (`CliError` — except a bare help request, which exits 0), rejected
1148
+ * survivors runs (`SurvivorsRejection`) and a rejected config (`ConfigError`)
1149
+ * all exit 2, all other failures exit 1 (the framework's default). A
1150
+ * successful run exits 0; the verdict gates (U5) then resolve the final
1151
+ * classed code.
1152
+ */
1153
+ function resolveCliExitCode(exit) {
1154
+ if (Exit.isSuccess(exit)) return 0;
1155
+ if (Cause.hasInterruptsOnly(exit.cause)) return 1;
1156
+ const failure = Cause.findErrorOption(exit.cause);
1157
+ if (Option.isSome(failure)) {
1158
+ const value = failure.value;
1159
+ if (S.is(CliError.ShowHelp)(value)) return value.errors.length > 0 ? 2 : 0;
1160
+ if (CliError.isCliError(value)) return 2;
1161
+ if (S.is(SurvivorsRejection)(value)) return SURVIVORS_REJECT_EXIT_CLASS;
1162
+ }
1163
+ if (carriesConfigError(exit.cause)) return ExitClass.ConfigError;
1164
+ return 1;
1165
+ }
1166
+ /**
1167
+ * The single operation of the CLI's executor cell: the impure shell that
1168
+ * wraps the transport's command effect with the run bootstrap. It creates the
1169
+ * run's stream from the resolved mode, binds the host options a run is
1170
+ * executed with, opens the stream, runs the command effect, dispatches the
1171
+ * request the handlers left, and on every outcome — success, failure and
1172
+ * interruption alike — emits the machine-mode terminal event (error/help/
1173
+ * null verdict) and drains the stream, returning the classed exit code as its
1174
+ * value. SIGINT/SIGTERM interrupt the current fiber so the finalizer runs
1175
+ * before the process exits; the code is resolved exactly once (R6), in the
1176
+ * finalizer, where the terminal event's `code` is chosen from the same inputs
1177
+ * the teardown used before.
1178
+ */
1179
+ const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
1180
+ const stream = yield* createRunEventStream(input.mode);
1181
+ const runMutationTest = input.runMutationTest ?? defaultRunMutationTest(hostOptionsOf(input.mode, stream));
1182
+ let currentFiber = null;
1183
+ let lastSignal = null;
1184
+ const resolveClassedExitCode = (exit) => {
1185
+ const signal = lastSignal;
1186
+ if (signal !== null) return 128 + signal;
1187
+ if (Exit.isFailure(exit)) return resolveCliExitCode(exit);
1188
+ return resolveExitCode(getPendingExitClasses(), null);
1189
+ };
1190
+ const onSignal = (signal) => {
1191
+ lastSignal = SIGNAL_NUMBERS[signal] ?? null;
1192
+ process.removeListener("SIGINT", onSignal);
1193
+ process.removeListener("SIGTERM", onSignal);
1194
+ if (currentFiber !== null) currentFiber.interruptUnsafe(currentFiber.id);
1195
+ };
1196
+ const dispatch = (request) => Match.value(request).pipe(Match.tag("run", (runRequest) => runRequest.survivors ? runSurvivorsAdmission(runMutationTest, stream, input.mode, runRequest.options) : Effect.promise(() => runMutationTest(runRequest.options))), Match.tag("llms", (llmsRequest) => Effect.sync(() => {
1197
+ stream.ensureOpen({
1198
+ mode: "machine",
1199
+ signal: "flag",
1200
+ stdoutIsTTY: process.stdout.isTTY === true
1201
+ });
1202
+ stream.sink(llmsRequest.document);
1203
+ })), Match.orElse(() => Effect.die("unreachable cli request variant")));
1204
+ const program = Effect.acquireUseRelease(Effect.sync(() => {
1205
+ currentFiber = Fiber.getCurrent() ?? null;
1206
+ process.on("SIGINT", onSignal);
1207
+ process.on("SIGTERM", onSignal);
1208
+ }), () => Effect.gen(function* () {
1209
+ yield* stream.open;
1210
+ yield* input.program;
1211
+ const request = yield* Ref.get(input.requestRef);
1212
+ yield* Option.match(request, {
1213
+ onNone: () => Effect.void,
1214
+ onSome: (cliRequest) => dispatch(cliRequest)
1215
+ });
1216
+ }), () => Effect.sync(() => {
1217
+ process.removeListener("SIGINT", onSignal);
1218
+ process.removeListener("SIGTERM", onSignal);
1219
+ }));
1220
+ return yield* Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
1221
+ const exit = yield* Effect.exit(restore(program));
1222
+ const code = resolveClassedExitCode(exit);
1223
+ input.recordExitCode(code);
1224
+ if (input.mode.mode === "machine") emitMachineModeOutput(stream, input.mode, exit, code, input.argv);
1225
+ yield* stream.closeAndDrain;
1226
+ return code;
1227
+ }));
1228
+ });
1229
+ //#endregion
1230
+ //#region src/StrykerCliHandler.ts
1231
+ function createSplitter(separator) {
1232
+ return (value) => value.split(separator).filter(Boolean);
1233
+ }
1234
+ const splitOnComma = createSplitter(",");
1235
+ const splitOnSpace = createSplitter(" ");
1236
+ /**
1237
+ * Commander characterization: `always` stays a string, everything else is a
1238
+ * boolean where `false`/`0` (case-insensitively) mean `false` — a tri-state a
1239
+ * plain boolean or choice would flatten.
1240
+ */
1241
+ function parseCleanDirOption(value) {
1242
+ const v = value.toLocaleLowerCase();
1243
+ return v === "always" ? v : v !== "false" && v !== "0";
1244
+ }
1245
+ /**
1246
+ * Commander characterization: a pure integer is parsed as a number, anything
1247
+ * else (e.g. `"50%"`) stays a string.
1248
+ */
1249
+ function parseConcurrency(value) {
1250
+ if (/^\d+$/.test(value)) return parseInt(value, 10);
1251
+ return value;
1252
+ }
1253
+ const optional = (option) => Flag.optional(option);
1254
+ /**
1255
+ * Commander left an omitted flag out of the parsed options; `deepMerge` treats
1256
+ * `undefined` as absent but an explicit `false` would override a config-file
1257
+ * `true`. The framework's boolean defaults to `false` when absent, so map that
1258
+ * back to `undefined` (KTD4).
1259
+ */
1260
+ const absentWhenFalse = (value) => value ? true : void 0;
1261
+ const LOG_LEVELS = [
1262
+ "fatal",
1263
+ "error",
1264
+ "warn",
1265
+ "info",
1266
+ "debug",
1267
+ "trace",
1268
+ "off"
1269
+ ];
1270
+ const LOG_LEVEL_LOOKUP = {
1271
+ fatal: true,
1272
+ error: true,
1273
+ warn: true,
1274
+ info: true,
1275
+ debug: true,
1276
+ trace: true,
1277
+ off: true
1278
+ };
1279
+ function isLogLevel(value) {
1280
+ return LOG_LEVEL_LOOKUP[value] === true;
1281
+ }
1282
+ function setLogLevel(target, key, value) {
1283
+ const unwrapped = unwrap(value);
1284
+ if (unwrapped !== void 0 && isLogLevel(unwrapped)) target[key] = unwrapped;
1285
+ }
1286
+ const runOptions = {
1287
+ ignorePatterns: Flag.string("ignorePatterns").pipe(Flag.withDescription("A comma separated list of patterns used for specifying which files need to be ignored. This should only be used in cases where you experience a slow Stryker startup, because too many (or too large) files are copied to the sandbox that are not needed to run the tests. For example, image or movie directories. Note: This option will have NO effect when using the `--inPlace` option. The directories `node_modules`, `.git` and some others are always ignored. Example: `--ignorePatterns dist`. These patterns are ALWAYS ignored: [`node_modules`, `.git`, `/reports`, `*.tsbuildinfo`, `/stryker.log`, `.stryker-tmp`]. Because Stryker always ignores these, you should rarely have to adjust the `ignorePatterns` setting at all. This is useful to speed up Stryker by reducing the size of the sandbox directory which has a positive effect on performance."), Flag.map(splitOnComma), optional),
1288
+ ignoreStatic: Flag.map(Flag.boolean("ignoreStatic"), absentWhenFalse).pipe(Flag.withDescription("Ignore static mutants. Static mutants are mutants which are only executed during the loading of a file.")),
1289
+ incremental: Flag.map(Flag.boolean("incremental"), absentWhenFalse).pipe(Flag.withDescription("Enable 'incremental mode'. Stryker will store results in a file and use that file to speed up the next --incremental run")),
1290
+ allowEmpty: Flag.map(Flag.boolean("allowEmpty"), absentWhenFalse).pipe(Flag.withDescription("Allows stryker to exit without any errors in cases where no tests are found")),
1291
+ incrementalFile: Flag.string("incrementalFile").pipe(Flag.withDescription("Specify the file to use for incremental mode."), optional),
1292
+ force: Flag.map(Flag.boolean("force"), absentWhenFalse).pipe(Flag.withDescription("Run all mutants, even if --incremental is provided and an incremental file exists. Can be used to force a rebuild of the incremental file.")),
1293
+ mutate: Flag.string("mutate").pipe(Flag.withAlias("m"), Flag.withDescription("With `mutate` you configure the subset of files or just one specific file to be mutated. These should be your _production code files_, and definitely not your test files. (Whereas with `ignorePatterns` you prevent non-relevant files from being copied to the sandbox directory in the first place)\nThe default will try to guess your production code files based on sane defaults. It reads like this:\n- Include all js-like files inside the `src` or `lib` dir\n- Except files inside `__tests__` directories and file names ending with `test` or `spec`.\nIf the defaults are not sufficient for you, for example in a angular project you might want to **exclude** not only the `*.spec.ts` files but other files too, just like the default already does.\nIt is possible to override the defaults by: - supplying one or more [glob patterns](https://github.com/isaacs/minimatch) to include (e.g. `src/**/*.js`) - or one or more comma separated glob patterns preceded with `!` to exclude (e.g. `!src/**/*.spec.js`) - or both (e.g. `src/**/*.js,!src/**/*.spec.js`).\nNote: Stryker will use [minimatch](https://github.com/isaacs/minimatch) for parsing these patterns, see minimatch for the exact syntax."), Flag.map(splitOnComma), optional),
1294
+ testFiles: Flag.string("testFiles").pipe(Flag.withAlias("t"), Flag.withDescription("With `testFiles` you can limit which test files are executed during mutation testing. When specified, only tests from these files will be run. This allows you to verify that a module's dedicated unit tests can kill all its mutants independently."), Flag.map(splitOnComma), optional),
1295
+ buildCommand: Flag.string("buildCommand").pipe(Flag.withAlias("b"), Flag.withDescription("Configure a build command to run after mutating the code, but before mutants are tested. This is generally used to transpile your code before testing. Only configure this if your test runner doesn't take care of this already and you're not using just-in-time transpiler like `babel/register` or `ts-node`."), optional),
1296
+ dryRunOnly: Flag.map(Flag.boolean("dryRunOnly"), absentWhenFalse).pipe(Flag.withDescription("Execute the initial test run only, without doing actual mutation testing. Doing a dry run only can be used to test that StrykerJS can run your test setup, for example, in CI pipelines.")),
1297
+ checkers: Flag.string("checkers").pipe(Flag.withDescription("A comma separated list of checkers to use, for example --checkers typescript"), Flag.map(splitOnComma), optional),
1298
+ checkerNodeArgs: Flag.string("checkerNodeArgs").pipe(Flag.withDescription("A list of node args to be passed to checker child processes. Split on spaces (commander characterization): `--checkerNodeArgs \"--inspect-brk --trace-warnings\"`."), Flag.map(splitOnSpace), optional),
1299
+ coverageAnalysis: Flag.choice("coverageAnalysis", [
1300
+ "perTest",
1301
+ "all",
1302
+ "off"
1303
+ ]).pipe(Flag.withDescription(`The coverage analysis strategy you want to use. Default value: "${defaultOptions.coverageAnalysis}"`), optional),
1304
+ testRunner: Flag.string("testRunner").pipe(Flag.withDescription("The name of the test runner you want to use"), optional),
1305
+ testRunnerNodeArgs: Flag.string("testRunnerNodeArgs").pipe(Flag.withDescription("A list of node args to be passed to test runner child processes. Split on spaces (commander characterization): `--testRunnerNodeArgs \"--inspect-brk --trace-warnings\"`."), Flag.map(splitOnSpace), optional),
1306
+ reporters: Flag.string("reporters").pipe(Flag.withDescription("A comma separated list of the names of the reporter(s) you want to use"), Flag.map(splitOnComma), optional),
1307
+ plugins: Flag.string("plugins").pipe(Flag.withDescription("A list of plugins you want stryker to load (`require`)."), Flag.map(splitOnComma), optional),
1308
+ appendPlugins: Flag.string("appendPlugins").pipe(Flag.withDescription("A list of additional plugins you want Stryker to load (`require`) without overwriting the (default) `plugins`."), Flag.map(splitOnComma), optional),
1309
+ timeoutMS: Flag.integer("timeoutMS").pipe(Flag.withDescription("Tweak the absolute timeout used to wait for a test runner to complete"), optional),
1310
+ timeoutFactor: Flag.float("timeoutFactor").pipe(Flag.withDescription("Tweak the standard deviation relative to the normal test run of a mutated test"), optional),
1311
+ dryRunTimeoutMinutes: Flag.float("dryRunTimeoutMinutes").pipe(Flag.withDescription("Configure an absolute timeout for the initial test run. (It can take a while.)"), optional),
1312
+ maxConcurrentTestRunners: Flag.integer("maxConcurrentTestRunners").pipe(Flag.withDescription("Set the number of max concurrent test runner to spawn (default: cpuCount)"), optional),
1313
+ concurrency: Flag.string("concurrency").pipe(Flag.withAlias("c"), Flag.withDescription("Set the concurrency of workers. Stryker will always run checkers and test runners in parallel by creating worker processes (default: cpuCount - 1)"), Flag.map(parseConcurrency), optional),
1314
+ disableBail: Flag.map(Flag.boolean("disableBail"), absentWhenFalse).pipe(Flag.withDescription("Force the test runner to keep running tests, even when a mutant is already killed.")),
1315
+ maxTestRunnerReuse: Flag.integer("maxTestRunnerReuse").pipe(Flag.withDescription("Restart each test runner worker process after `n` runs. Not recommended unless you are experiencing memory leaks that you are unable to resolve. Configuring `0` here means infinite reuse."), optional),
1316
+ logLevel: Flag.choice("logLevel", LOG_LEVELS).pipe(Flag.withDescription(`Set the log level for the console. Possible values: fatal, error, warn, info, debug, trace and off. Default is "${defaultOptions.logLevel}"`), optional),
1317
+ fileLogLevel: Flag.choice("fileLogLevel", LOG_LEVELS).pipe(Flag.withDescription(`Set the log level for the "stryker.log" file. Possible values: fatal, error, warn, info, debug, trace and off. Default is "${defaultOptions.fileLogLevel}"`), optional),
1318
+ inPlace: Flag.map(Flag.boolean("inPlace"), absentWhenFalse).pipe(Flag.withDescription("Determines whether or not Stryker should mutate your files in place. Note: mutating your files in place is generally not needed for mutation testing, unless you have a dependency in your project that is really dependent on the file locations (like \"app-root-path\" for example).\nWhen `true`, Stryker will override your files, but it will keep a copy of the originals in the temp directory (using `tempDirName`) and it will place the originals back after it is done. Also with `true` the `ignorePatterns` has no effect any more.\nWhen `false` (default) Stryker will work in the copy of your code inside the temp directory.")),
1319
+ tempDirName: Flag.string("tempDirName").pipe(Flag.withDescription("Set the name of the directory that is used by Stryker as a working directory. This directory will be cleaned after a successful run"), optional),
1320
+ cleanTempDir: Flag.string("cleanTempDir").pipe(Flag.withDescription(`Choose whether or not to clean the temp dir (which is "${defaultOptions.tempDirName}" inside the current working directory by default) after a run.\n- false: Never delete the temp dir;\n- true: Delete the tmp dir after a successful run;\n- always: Always delete the temp dir, regardless of whether the run was successful.`), Flag.map(parseCleanDirOption), optional),
1321
+ survivors: Flag.map(Flag.boolean("survivors"), absentWhenFalse).pipe(Flag.withDescription("Re-run only the mutants that survived a previous run. Admits against the previous run's mutation report (the `survivorsPriorReport` config option, default `reports/mutation-report.json`) and re-tests exactly the survivor set. Exits 2 with a remediation naming a full run when the report is missing, drifted, or the configuration changed; exits 0 with a null score when the report has no survivors."))
1322
+ };
1323
+ const runArgs = { configFile: Argument.optional(Argument.string("configFile")) };
1324
+ const runConfig = {
1325
+ ...runOptions,
1326
+ ...runArgs
1327
+ };
1328
+ const rootConfig = { llms: Flag.map(Flag.boolean("llms"), absentWhenFalse).pipe(Flag.withDescription("Print the agent-facing command manifest as one JSON object on stdout: every option, alias, kind, default, allowed value set and description, plus the subcommands and positional arguments, walked from the command descriptors.")) };
1329
+ function unwrap(value) {
1330
+ if (Option.isOption(value)) return Option.match(value, {
1331
+ onNone: () => void 0,
1332
+ onSome: (v) => v
1333
+ });
1334
+ return value;
1335
+ }
1336
+ function setIfPresent(target, key, value) {
1337
+ const unwrapped = unwrap(value);
1338
+ if (unwrapped !== void 0) target[key] = unwrapped;
1339
+ }
1340
+ /**
1341
+ * Builds the full command tree — root plus the `run` subcommand — from the
1342
+ * same option/arg records the parser matches against. Each handler leaves the
1343
+ * request the executor runs: the run handler writes the parsed options (and
1344
+ * the survivors flag, which the admission consumes and the pipeline must not
1345
+ * see), the `--llms` handler writes the pre-rendered manifest document, and
1346
+ * the bare root writes nothing — `helpRequested` makes the framework render
1347
+ * help, which the executor's finalizer turns into the `help` terminal event.
1348
+ */
1349
+ function makeStrykerCommand(requestRef) {
1350
+ const runCommand = Command.make("run", runConfig, (config) => {
1351
+ const configFile = Option.getOrUndefined(config.configFile);
1352
+ if (configFile !== void 0 && configFile.startsWith("-")) return Console.error(`Received unknown argument: '${configFile}'`).pipe(Effect.andThen(Effect.failSync(() => CliError.UnexpectedArgument.make({ arguments: [configFile] }))));
1353
+ return Ref.set(requestRef, Option.some({
1354
+ _tag: "run",
1355
+ options: readStrykerOptions(config),
1356
+ survivors: config.survivors === true
1357
+ }));
1358
+ }).pipe(Command.withDescription("Run mutation testing"));
1359
+ /**
1360
+ * Rebuilds the `PartialStrykerOptions` object commander produced: only options
1361
+ * actually given on the command line become keys (KTD4). `survivors` is
1362
+ * deliberately not forwarded — the survivor re-run logic (U8) consumes it.
1363
+ */
1364
+ function readStrykerOptions(config) {
1365
+ const options = {};
1366
+ setIfPresent(options, "ignorePatterns", config.ignorePatterns);
1367
+ setIfPresent(options, "ignoreStatic", config.ignoreStatic);
1368
+ setIfPresent(options, "incremental", config.incremental);
1369
+ setIfPresent(options, "allowEmpty", config.allowEmpty);
1370
+ setIfPresent(options, "incrementalFile", config.incrementalFile);
1371
+ setIfPresent(options, "force", config.force);
1372
+ setIfPresent(options, "mutate", config.mutate);
1373
+ setIfPresent(options, "testFiles", config.testFiles);
1374
+ setIfPresent(options, "buildCommand", config.buildCommand);
1375
+ setIfPresent(options, "dryRunOnly", config.dryRunOnly);
1376
+ setIfPresent(options, "checkers", config.checkers);
1377
+ setIfPresent(options, "checkerNodeArgs", config.checkerNodeArgs);
1378
+ setIfPresent(options, "coverageAnalysis", config.coverageAnalysis);
1379
+ setIfPresent(options, "testRunner", config.testRunner);
1380
+ setIfPresent(options, "testRunnerNodeArgs", config.testRunnerNodeArgs);
1381
+ setIfPresent(options, "reporters", config.reporters);
1382
+ setIfPresent(options, "plugins", config.plugins);
1383
+ setIfPresent(options, "appendPlugins", config.appendPlugins);
1384
+ setIfPresent(options, "timeoutMS", config.timeoutMS);
1385
+ setIfPresent(options, "timeoutFactor", config.timeoutFactor);
1386
+ setIfPresent(options, "dryRunTimeoutMinutes", config.dryRunTimeoutMinutes);
1387
+ setIfPresent(options, "maxConcurrentTestRunners", config.maxConcurrentTestRunners);
1388
+ setIfPresent(options, "concurrency", config.concurrency);
1389
+ setIfPresent(options, "disableBail", config.disableBail);
1390
+ setIfPresent(options, "maxTestRunnerReuse", config.maxTestRunnerReuse);
1391
+ setLogLevel(options, "logLevel", config.logLevel);
1392
+ setLogLevel(options, "fileLogLevel", config.fileLogLevel);
1393
+ setIfPresent(options, "inPlace", config.inPlace);
1394
+ setIfPresent(options, "tempDirName", config.tempDirName);
1395
+ setIfPresent(options, "cleanTempDir", config.cleanTempDir);
1396
+ if (Option.isSome(config["configFile"])) options["configFile"] = config["configFile"].value;
1397
+ return options;
1398
+ }
1399
+ const strykerCommand = Command.make("stryker", rootConfig, (config) => {
1400
+ if (config.llms === true) {
1401
+ const document = {
1402
+ kind: "manifest",
1403
+ schemaVersion: "1.0",
1404
+ code: 0,
1405
+ manifest: emitLLMSManifest(strykerCommand, strykerVersion)
1406
+ };
1407
+ return Ref.set(requestRef, Option.some({
1408
+ _tag: "llms",
1409
+ document
1410
+ }));
1411
+ }
1412
+ return Effect.failSync(() => CliError.ShowHelp.make({
1413
+ commandPath: ["stryker"],
1414
+ errors: []
1415
+ }));
1416
+ }).pipe(Command.withSubcommands([runCommand]));
1417
+ return strykerCommand;
1418
+ }
1419
+ /**
1420
+ * The CLI parses only text/number/choice options, so the framework's platform
1421
+ * services are never read at runtime; the v4 runner still demands them in its
1422
+ * environment. The bootstrap provides `Path.layer` (the universal
1423
+ * implementation in `effect/Path`), an *empty* file system (`layerNoop`:
1424
+ * every operation reports not-found), and a process-stdio `Terminal` whose
1425
+ * interactive input primitives fail loudly — the run-only surface (R14) has
1426
+ * no prompts. `@effect/platform-node` is a declared dependency of this
1427
+ * package (it provides the `NodeRuntime` the bin runs through), but no
1428
+ * platform-node service is wired into the command environment: the parser
1429
+ * never reads a real file system at run time, so the noop layers are
1430
+ * sufficient.
1431
+ */
1432
+ const terminalLayer = Layer.succeed(Terminal.Terminal, Terminal.make({
1433
+ columns: Effect.sync(() => process.stdout.columns),
1434
+ rows: Effect.sync(() => process.stdout.rows),
1435
+ readInput: Effect.die(/* @__PURE__ */ new Error("stryker has no interactive prompts: Terminal.readInput is not supported")),
1436
+ readLine: Effect.die(/* @__PURE__ */ new Error("stryker has no interactive prompts: Terminal.readLine is not supported")),
1437
+ display: (text) => Effect.sync(() => {
1438
+ process.stdout.write(text);
1439
+ })
1440
+ }));
1441
+ const cliLayer = Layer.mergeAll(CliConfig.layer({ builtIns: [
1442
+ GlobalFlag.Help,
1443
+ GlobalFlag.action({
1444
+ flag: Flag.boolean("version").pipe(Flag.withAlias("v"), Flag.withDescription("Show version information")),
1445
+ run: () => Console.log(strykerVersion)
1446
+ }),
1447
+ GlobalFlag.Wizard,
1448
+ GlobalFlag.Completions,
1449
+ GlobalFlag.LogLevel
1450
+ ] }), Path.layer, FileSystem.layerNoop({}), terminalLayer, NodeStdio.layer, Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make(() => Effect.die("no child processes"))));
1451
+ /**
1452
+ * The transport entry: builds the command tree, resolves the mode once at
1453
+ * the edge (never a second probe), provides the CLI and Console layers the
1454
+ * framework renders through, delegates the whole run to the executor cell and
1455
+ * returns the classed exit code it computes. The executor is the I/O
1456
+ * sandwich; this function only frames it.
1457
+ */
1458
+ function strykerCliEffect(argv, runMutationTest, recordExitCode, detectMode, createRunEventStream) {
1459
+ return Effect.gen(function* () {
1460
+ const mode = detectMode();
1461
+ const requestRef = yield* Ref.make(Option.none());
1462
+ const command = makeStrykerCommand(requestRef);
1463
+ const cliEffect = Command.runWith(command, { version: strykerVersion })(argv).pipe(Effect.provide(Layer.mergeAll(mode.mode === "machine" ? strykerCliConsoleLayers.machine : Layer.empty, cliLayer)));
1464
+ const outcome = yield* Effect.result(runStrykerCli({
1465
+ program: cliEffect,
1466
+ requestRef,
1467
+ mode,
1468
+ runMutationTest,
1469
+ recordExitCode,
1470
+ argv
1471
+ }, createRunEventStream));
1472
+ return Result.isFailure(outcome) ? outcome.failure : outcome.success;
1473
+ });
1474
+ }
1475
+ //#endregion
1476
+ //#region src/main.ts
1477
+ const EXIT_CODE_RUN_NEVER_REACHED_ITS_FINALIZER = 1;
1478
+ process.title = "stryker";
1479
+ if (!semver.satisfies(process.version, strykerEngines.node)) throw new Error(`Node.js version ${process.version} detected. StrykerJS requires version to match ${strykerEngines.node}. Please update your Node.js version or visit https://nodejs.org/ for additional instructions`);
1480
+ const resolvedExitCode = { current: EXIT_CODE_RUN_NEVER_REACHED_ITS_FINALIZER };
1481
+ const program = Effect.gen(function* () {
1482
+ const outputMode = yield* OutputModeProbe;
1483
+ const runEvents = yield* RunEventStreamPort;
1484
+ return yield* strykerCliEffect(process.argv.slice(2), void 0, (code) => {
1485
+ resolvedExitCode.current = code;
1486
+ }, outputMode.detectMode, runEvents.createRunEventStream);
1487
+ }).pipe(Effect.provide(Layer.merge(OutputModeProbeLive, RunEventStreamLive).pipe(Layer.provide(NodeStdio.layer))));
1488
+ NodeRuntime.runMain(program, {
1489
+ disableErrorReporting: true,
1490
+ teardown: (_exit, onExit) => {
1491
+ onExit(resolvedExitCode.current);
1492
+ }
1493
+ });
1494
+ //#endregion
1495
+ export {};