@orkestrel/process 0.0.3 → 0.0.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.
@@ -2,7 +2,13 @@ import { Buffer as Buffer_2 } from 'node:buffer';
2
2
  import { DetachOptions } from '@orkestrel/process';
3
3
  import { EmitterInterface } from '@orkestrel/emitter';
4
4
  import { ExecutableOptions } from '@orkestrel/process';
5
+ import { ExecuteInput } from '@orkestrel/process';
6
+ import { ExecuteOptions } from '@orkestrel/process';
7
+ import { ExecuteResult } from '@orkestrel/process';
8
+ import { ExecuteResult as ExecuteResult_2 } from '@orkestrel/process';
9
+ import { ExecuteSyncOptions } from '@orkestrel/process';
5
10
  import { ProcessCommand } from '@orkestrel/process';
11
+ import { ProcessCommand as ProcessCommand_2 } from '@orkestrel/process';
6
12
  import { ProcessEventMap } from '@orkestrel/process';
7
13
  import { ProcessExit } from '@orkestrel/process';
8
14
  import { ProcessInterface } from '@orkestrel/process';
@@ -10,19 +16,43 @@ import { ProcessManagerEventMap } from '@orkestrel/process';
10
16
  import { ProcessManagerInterface } from '@orkestrel/process';
11
17
  import { ProcessManagerOptions } from '@orkestrel/process';
12
18
  import { ProcessOptions } from '@orkestrel/process';
13
- import { RunInput } from '@orkestrel/process';
14
- import { RunOptions } from '@orkestrel/process';
15
- import { RunResult } from '@orkestrel/process';
16
- import { RunSyncOptions } from '@orkestrel/process';
17
19
  import { SpawnInput } from '@orkestrel/process';
18
20
 
19
21
  /**
20
- * Builds one settled {@link RunResult} from a completed run's captured bytes and terminal facts.
22
+ * Builds the executable candidates an explicit platform would search.
23
+ *
24
+ * @remarks
25
+ * Windows alone needs this list: the host searches the working directory before `PATH` and applies
26
+ * `PATHEXT`, and Node reproduces neither for a direct spawn. Within each searched directory the
27
+ * literal name is tried first and each `PATHEXT` candidate after it, whether or not the name already
28
+ * carries an extension — so `report.txt` resolves to a `report.txt` file where one exists and to
29
+ * `report.txt.cmd` where none does. The lookup reads the child's effective environment, so an
30
+ * overridden `PATH` selects the executable the child would have found. A POSIX host resolves the
31
+ * file itself, so the candidate list there is empty. An appended extension is spelled the way
32
+ * `PATHEXT` spells it rather than the way the directory entry does, which the case-insensitive host
33
+ * treats as the same file. A non-Windows platform returns an empty list because `execvp` performs
34
+ * its own lookup.
35
+ *
36
+ * @param file - The command executable name or path
37
+ * @param workspace - The directory searched first
38
+ * @param environment - The child's effective environment
39
+ * @param platform - The platform whose lookup rules apply
40
+ * @returns The ordered absolute candidate paths
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * buildExecutableCandidates('git', 'C:\\work', { PATH: 'C:\\bin' }, 'win32')
45
+ * ```
46
+ */
47
+ export declare function buildExecutableCandidates(file: string, workspace: string, environment: Readonly<Record<string, string | undefined>>, platform: NodeJS.Platform): readonly string[];
48
+
49
+ /**
50
+ * Builds one settled {@link ExecuteResult} from a completed run's captured bytes and terminal facts.
21
51
  *
22
52
  * @remarks
23
53
  * `failed` is derived: a run failed when it timed out, was aborted, ended on a host fault, was ended
24
54
  * by a signal, or exited with a code other than `0` — a `null` code from a spawn fault is therefore
25
- * a failure. Both byte fields are bounded by `limit` on a code-point boundary here, which is the one
55
+ * a failure. Each byte field is bounded by `limit` on a code-point boundary here, which is the one
26
56
  * place the captured bytes are decoded.
27
57
  *
28
58
  * @param input - The captured bytes, terminal state, and capture limit
@@ -30,7 +60,7 @@ import { SpawnInput } from '@orkestrel/process';
30
60
  *
31
61
  * @example
32
62
  * ```ts
33
- * buildRunResult({
63
+ * buildExecuteResult({
34
64
  * command: 'git status',
35
65
  * stdout: Buffer.from('ok'),
36
66
  * stderr: Buffer.alloc(0),
@@ -43,20 +73,37 @@ import { SpawnInput } from '@orkestrel/process';
43
73
  * }).failed // false
44
74
  * ```
45
75
  */
46
- export declare function buildRunResult(input: RunInput): RunResult;
76
+ export declare function buildExecuteResult(input: ExecuteInput): ExecuteResult;
47
77
 
48
78
  /**
49
- * Builds the resolved spawn form of one command.
79
+ * Builds a spawn form from a resolved file and an explicit platform.
50
80
  *
51
81
  * @remarks
52
- * The executable is resolved against the child's effective environment. A resolved `.cmd` or `.bat`
53
- * script cannot be spawned directly **on Windows**, so it runs there through an explicitly quoted
54
- * `cmd.exe /d /s /c` command line with the argument vector passed verbatim. No path uses a shell,
55
- * so a metacharacter in an argument is never interpreted. `cmd.exe` expands `%NAME%` before it
56
- * parses quotes, so no quoting can carry a percent sign through to a Windows batch target: an
57
- * argument carrying one is refused there rather than silently rewritten. The whole batch path is
58
- * Windows-only. A POSIX host has no `cmd.exe` and no restriction on spawning a file directly, so a
59
- * target named `worker.cmd` spawns directly there and receives a percent sign as literal text.
82
+ * A resolved `.cmd` or `.bat` script cannot be spawned directly **on Windows**, so it runs there
83
+ * through an explicitly quoted `cmd.exe /d /s /c` command line with the argument vector passed
84
+ * verbatim. No path uses a shell, so a metacharacter in an argument is never interpreted. `cmd.exe`
85
+ * expands `%NAME%` before it parses quotes, so no quoting can carry a percent sign through to a
86
+ * Windows batch target: an argument carrying one is refused there rather than silently rewritten.
87
+ * The whole batch path is Windows-only. A POSIX host has no `cmd.exe` and no restriction on spawning
88
+ * a file directly, so a target named `worker.cmd` spawns directly there and receives a percent sign
89
+ * as literal text.
90
+ *
91
+ * @param command - The executable and its argument vector
92
+ * @param file - The resolved executable file
93
+ * @param environment - The child's effective environment
94
+ * @param platform - The platform whose batch rules apply
95
+ * @returns The file, argument vector, and verbatim flag to spawn with
96
+ * @throws A {@link ProcessError} coded `invalid` when `platform` is `win32`, the resolved target is a batch script, and an argument carries a percent sign
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * buildPlatformSpawn({ file: 'node', arguments: ['--version'] }, 'node', {}, 'linux').verbatim // false
101
+ * ```
102
+ */
103
+ export declare function buildPlatformSpawn(command: ProcessCommand, file: string, environment: Readonly<Record<string, string | undefined>>, platform: NodeJS.Platform): SpawnInput;
104
+
105
+ /**
106
+ * Builds the resolved spawn form of one command for the current host.
60
107
  *
61
108
  * @param command - The executable and its argument vector
62
109
  * @param options - The directory searched first and the child's effective environment
@@ -123,13 +170,74 @@ export declare function createProcessManager(options?: ProcessManagerOptions): P
123
170
  * detach({ file: 'node', arguments: ['daemon.js'] }, { workspace: process.cwd() })
124
171
  * ```
125
172
  */
126
- export declare function detach(command: ProcessCommand, options?: DetachOptions): void;
173
+ export declare function detach(command: ProcessCommand_2, options?: DetachOptions): void;
174
+
175
+ /**
176
+ * Runs one command to completion, buffering its output, and settles with the outcome.
177
+ *
178
+ * @remarks
179
+ * The executable is resolved through {@link buildSpawn}, so no run uses a shell. On a POSIX host the
180
+ * child is detached, which is what lets its whole process group be terminated. Standard output and
181
+ * error are each byte-bounded by `limit` (default {@link PROCESS_OUTPUT}) and `truncated` reports
182
+ * whether either stream exceeded it. A positive `timeout` and an aborting `signal` both terminate the
183
+ * child through the same bounded stop, and only the earliest is recorded: a timeout reports
184
+ * `expired` and an abort reports `aborted`, never both. After termination the outcome is awaited for
185
+ * a bounded window, so a descendant holding the child's stdio cannot keep the run pending. That bound
186
+ * covers a terminated run alone: a run with no `timeout` and no `signal` settles on stdio completion
187
+ * rather than on process exit, so a descendant that inherited the child's stdio holds the run open
188
+ * after the child itself has gone. Give such a run a `timeout`. The child's `environment` merges over
189
+ * the parent unless the command is `isolated`, then `options.environment` on top, and `options.input`
190
+ * overrides `command.input`. A host fault while writing that input terminates the run by design and
191
+ * marks its result failed; a strict rejection carries the host fault as its cause. This differs
192
+ * from `Process` constructor input, whose package-initiated input phase stays quiet. Unless `strict`
193
+ * is `false`, a failed run rejects with a {@link ProcessError} carrying the {@link ExecuteResult};
194
+ * {@link createExecuteError} constructs every rejection outside the input-fault door. An invalid
195
+ * option or command string rejects before the child is spawned, because an async function cannot
196
+ * throw synchronously.
197
+ *
198
+ * @param command - The executable, arguments, and optional environment and input
199
+ * @param options - Working directory, timeout, grace, signal, capture limit, and failure delivery
200
+ * @returns The settled run outcome
201
+ * @throws A {@link ProcessError} coded `invalid` for a malformed option, command string, or batch-bound argument, or one carrying the {@link ExecuteResult} when the run failed and `strict` is not `false`
202
+ *
203
+ * @example
204
+ * ```ts
205
+ * const result = await execute({ file: 'git', arguments: ['status'] }, { workspace: process.cwd() })
206
+ * ```
207
+ */
208
+ export declare function execute(command: ProcessCommand_2, options?: ExecuteOptions): Promise<ExecuteResult_2>;
209
+
210
+ /**
211
+ * Runs one command to completion synchronously, buffering its output, and returns the outcome.
212
+ *
213
+ * @remarks
214
+ * The synchronous counterpart of {@link execute}, spawned through the same resolver and never through a
215
+ * shell. The host offers no cooperative termination window and no in-flight cancellation, so this
216
+ * contract carries neither. A positive `timeout` ends only the root process and can leave
217
+ * descendants running; use {@link execute} or {@link Process} when timeout must terminate the tree. A
218
+ * timeout and an output overflow both end the root with `SIGKILL`: an overflow reports `truncated`
219
+ * and `failed` together and trims the partial output to `limit`, where {@link execute} keeps reading and
220
+ * reports `truncated` without failing. The environment and input follow the same merge as
221
+ * {@link execute}. Unless `strict` is `false`, a failed run throws a {@link createExecuteError}
222
+ * carrying the {@link ExecuteResult}.
223
+ *
224
+ * @param command - The executable, arguments, and optional environment and input
225
+ * @param options - Working directory, timeout, capture limit, and failure delivery
226
+ * @returns The run outcome
227
+ * @throws A {@link ProcessError} coded `invalid` for a malformed option, command string, or batch-bound argument, or one carrying the {@link ExecuteResult} when the run failed and `strict` is not `false`
228
+ *
229
+ * @example
230
+ * ```ts
231
+ * const result = executeSync({ file: 'git', arguments: ['--version'] }, { strict: false })
232
+ * ```
233
+ */
234
+ export declare function executeSync(command: ProcessCommand_2, options?: ExecuteSyncOptions): ExecuteResult_2;
127
235
 
128
236
  /**
129
237
  * Renders one command into its diagnostic command line.
130
238
  *
131
239
  * @param command - The executable and its argument vector
132
- * @returns The space-joined command line, for a {@link RunResult} and error messages
240
+ * @returns The space-joined command line, for an {@link ExecuteResult} and error messages
133
241
  *
134
242
  * @example
135
243
  * ```ts
@@ -178,10 +286,11 @@ export declare function isFile(target: string): boolean;
178
286
  *
179
287
  * @remarks
180
288
  * On a POSIX host a child spawned `detached` leads its own process group, so signalling the
181
- * negated pid reaches the whole tree. On Windows, or when no pid is available, the signal goes to
182
- * the child directly. A throw during signalling is swallowed: the process can exit between the
183
- * caller's liveness check and this call, and the child's native exit remains the authoritative
184
- * terminal state.
289
+ * negated pid reaches the whole tree. When no group owns the pid and the host reports `ESRCH`, the
290
+ * signal falls back to the child directly. On Windows, or when no pid is available, the signal also
291
+ * goes to the child directly. Every other throw during signalling is swallowed: the process can
292
+ * exit between the caller's liveness check and this call, and the child's native exit remains the
293
+ * authoritative terminal state.
185
294
  *
186
295
  * @param child - The child boundary
187
296
  * @param signal - The signal to deliver
@@ -216,13 +325,30 @@ export declare function killProcess(child: Pick<ProcessChild, 'pid' | 'kill'>, s
216
325
  export declare function killTree(pid: number, timeout: number): Promise<boolean>;
217
326
 
218
327
  /**
219
- * Merges environment overrides into the environment one child receives.
328
+ * Merges environment overrides into the environment one child receives on the current host.
329
+ *
330
+ * @param isolated - If `true`, the parent environment is excluded; if `false`, the overrides layer over it
331
+ * @param base - The command's own environment overrides
332
+ * @param override - Per-invocation overrides applied last
333
+ * @returns The environment for a spawn, carrying no unset key
334
+ *
335
+ * @example
336
+ * ```ts
337
+ * mergeEnvironment(true, { TOKEN: 'a' }) // { TOKEN: 'a' }
338
+ * ```
339
+ */
340
+ export declare function mergeEnvironment(isolated: boolean, base?: Readonly<Record<string, string | undefined>>, override?: Readonly<Record<string, string | undefined>>): NodeJS.ProcessEnv;
341
+
342
+ /**
343
+ * Merges environment layers under an explicit platform's key rules.
220
344
  *
221
345
  * @remarks
222
- * Later maps override earlier ones and an `undefined` value unsets a key. On Windows the keys fold
346
+ * Later maps override earlier ones and an `undefined` value unsets a key. For `win32` the keys fold
223
347
  * case-insensitively and the last writer wins, so `PATH` followed by `Path` yields one variable
224
348
  * rather than two the host would resolve unpredictably.
225
349
  *
350
+ * @param platform - The platform whose key rules apply
351
+ * @param parent - The parent environment layer
226
352
  * @param isolated - If `true`, the parent environment is excluded; if `false`, the overrides layer over it
227
353
  * @param base - The command's own environment overrides
228
354
  * @param override - Per-invocation overrides applied last
@@ -230,30 +356,35 @@ export declare function killTree(pid: number, timeout: number): Promise<boolean>
230
356
  *
231
357
  * @example
232
358
  * ```ts
233
- * mergeEnvironment(false, { TOKEN: 'a' }, { TOKEN: undefined }) // TOKEN unset
359
+ * mergePlatformEnvironment('linux', {}, false, { TOKEN: 'a' }, { TOKEN: undefined }) // TOKEN unset
234
360
  * ```
235
361
  */
236
- export declare function mergeEnvironment(isolated: boolean, base?: Readonly<Record<string, string | undefined>>, override?: Readonly<Record<string, string | undefined>>): NodeJS.ProcessEnv;
362
+ export declare function mergePlatformEnvironment(platform: NodeJS.Platform, parent: Readonly<Record<string, string | undefined>>, isolated: boolean, base?: Readonly<Record<string, string | undefined>>, override?: Readonly<Record<string, string | undefined>>): NodeJS.ProcessEnv;
237
363
 
238
364
  /**
239
365
  * One supervised child process with framed output, a bounded backlog, and bounded termination.
240
366
  *
241
367
  * @remarks
242
368
  * Standard output is framed through `readline` into a single-consumer stream: each line goes to
243
- * exactly one waiting iterator, so two iterators split the output rather than each receiving all of
244
- * it. While no consumer has ever requested an iterator,
245
- * stdout keeps draining so `exit` still resolves, and retention stops at the `backlog` mark: a
246
- * consumer attaching later receives the retained head, a gap, then the live stream. Once an iterator
369
+ * exactly one waiting iterator, so concurrent iterators split the output rather than each receiving
370
+ * all of it. While no consumer has ever requested an iterator, stdout keeps draining so `exit` still
371
+ * resolves, and retention stops at the `backlog` mark: a
372
+ * consumer attaching later receives the retained head, a gap, then the live stream. After an iterator
247
373
  * has been requested, stdout pauses at the mark and resumes at half of it, so that consumer loses
248
- * nothing and the child feels real backpressure. The mark is soft — the host delivers whole chunks,
249
- * so the backlog overshoots it by up to one delivered chunk plus the drain that termination
250
- * releases. Standard error is decoded and forwarded live as the `stderr` event while a byte-bounded
251
- * raw tail is retained as `evidence`. The typed `emitter` also carries the child `error` cause on a
252
- * spawn fault and the terminal `exit`, alongside the `exit` promise. `stop` ends the whole tree and
253
- * reports whether the native exit was observed; `destroy` stops the child, destroys stdin so every
254
- * pending `send` settles, and destroys the emitter last. `destroy` resolving does not imply the
255
- * child's stdio has closed, so `exit` and `lines` can still be outstanding when a descendant holds
256
- * an inherited pipe.
374
+ * nothing before termination and the child feels real backpressure. The mark is soft — the ordinary backlog can
375
+ * overshoot it by the line that crossed it plus the rest of its delivered chunk. Termination never
376
+ * reapplies backpressure, and retained lines are capped at twice `backlog`; later lines are dropped
377
+ * and `truncated` reports the omission. Standard error is decoded and forwarded live as the `stderr`
378
+ * event while a byte-bounded raw tail is retained as `evidence`. The typed `emitter` also carries a
379
+ * child `error` cause on a spawn fault, a `protocol` {@link ProcessError} on a host-reported stdin
380
+ * fault after the constructor input phase, and the terminal `exit`, alongside the `exit` promise.
381
+ * A fault arising from the constructor-supplied `input` write or its closing `end` stays quiet
382
+ * because the package initiated that sequence. `pid`,
383
+ * `code`, and `signal` read the spawned child directly, so the native exit is readable before the
384
+ * `exit` promise settles on stdio close. `stop` ends the whole tree and reports whether the native
385
+ * exit was observed; `destroy` stops the child, destroys stdin so every pending `send` settles, and
386
+ * destroys the emitter last. `destroy` resolving does not imply the child's stdio has closed, so
387
+ * `exit` and `lines` can still be outstanding when a descendant holds an inherited pipe.
257
388
  *
258
389
  * @example
259
390
  * ```ts
@@ -276,25 +407,38 @@ export declare class Process implements ProcessInterface {
276
407
  * @throws A {@link ProcessError} coded `invalid` when an option or command string is malformed
277
408
  */
278
409
  constructor(options: ProcessOptions);
410
+ /** The host process id, fixed when construction returns, or `undefined` when the spawn produced none. */
411
+ get pid(): number | undefined;
412
+ /** The exit code the host recorded, or `null` while the child has not exited and when a signal ended it. */
413
+ get code(): number | null;
414
+ /** The terminating signal name the host recorded, or `null` while the child has not exited and when it exited on its own. */
415
+ get signal(): string | null;
279
416
  /** The typed lifecycle observation surface. */
280
417
  get emitter(): EmitterInterface<ProcessEventMap>;
281
418
  /** The captured stdout lines, in arrival order, ending when the child's stdout closes. */
282
419
  get lines(): AsyncIterable<string>;
283
420
  /** The decoded byte-bounded stderr tail. */
284
421
  get evidence(): string;
422
+ /** True when the `lines` stream omitted output after a retention bound was reached. */
423
+ get truncated(): boolean;
285
424
  /** The terminal child state, observed once from the close event. */
286
425
  get exit(): Promise<ProcessExit>;
287
426
  /**
288
427
  * Write one line to the open standard-input channel.
289
428
  *
290
429
  * @remarks
291
- * Never rejects. The promise settles when the host reports the line handled, so a line written to
292
- * a child that is not reading stays pending until the child drains it or the channel closes. Every
293
- * terminal teardown path destroys stdin, so a pending write always settles by teardown; a caller
294
- * that needs its own deadline arms a timer and calls `stop`.
430
+ * Never rejects. `true` means the host accepted the bytes without reporting a fault; it does not
431
+ * prove that the child read them. An ordinary write settles when the kernel accepts it. Only a
432
+ * full pipe can hold the write unconfirmed. The `delivery` option can bound that wait, and every
433
+ * terminal teardown path settles pending writes. On Windows 11 with Node v24.18.1, measured on
434
+ * 2026-08-21, a child that closes its own file descriptor 0 can leave the parent pipe writable:
435
+ * the write can settle `true` without a callback error or a stream error while the child remains
436
+ * alive. After `stop` or `destroy` begins, a later call settles `false`. Version 0.0.4 could settle
437
+ * that call `true` before teardown destroyed the pipe; returning `false` avoids claiming delivery
438
+ * for bytes the package is about to discard.
295
439
  *
296
440
  * @param text - The line text without its trailing newline
297
- * @returns True when the line reached the host without error; false when the channel was closed, destroyed, ended, or the write failed
441
+ * @returns True when the host accepted the bytes without reporting a fault; false when the channel was closed, destroyed, ended, failed, or remained unconfirmed through `delivery`
298
442
  */
299
443
  send(text: string): Promise<boolean>;
300
444
  /**
@@ -329,11 +473,16 @@ export declare class Process implements ProcessInterface {
329
473
  * The child boundary the termination helpers drive.
330
474
  *
331
475
  * @remarks
332
- * A `ChildProcess` satisfies this structurally, and so does any object carrying the same four
476
+ * A `ChildProcess` satisfies this structurally, and so does any object carrying the same
333
477
  * members, which is what lets a caller drive `stopChild` over a child it spawned itself. Each helper
334
478
  * takes the slice of this contract it reads: `exitCode` and `signalCode` are the host's
335
479
  * authoritative liveness answer, `pid` addresses a POSIX process group, `kill` delivers one signal,
336
- * and `once` reports the native exit.
480
+ * `once` reports the native exit, and `off` releases that listener when the wait ends at its
481
+ * deadline instead.
482
+ *
483
+ * A non-detached child stays in its caller's process group, so no group carries its pid. `kill` is
484
+ * the route that reaches it: `killProcess` signals the negated `pid` first and falls back to `kill`
485
+ * when the host reports that no group owns that pid.
337
486
  */
338
487
  export declare interface ProcessChild {
339
488
  /** The process id the host assigned, or `undefined` when the spawn never produced one. */
@@ -353,10 +502,22 @@ export declare interface ProcessChild {
353
502
  * Register a one-shot listener for the native exit.
354
503
  *
355
504
  * @param event - The `exit` event name
356
- * @param listener - The listener invoked once the process exits
505
+ * @param listener - The listener invoked after the process exits
357
506
  * @returns Whatever the emitter returns, which the helpers ignore
358
507
  */
359
508
  once(event: 'exit', listener: () => void): unknown;
509
+ /**
510
+ * Release one previously registered exit listener.
511
+ *
512
+ * @remarks
513
+ * `waitForExit` calls this when its deadline elapses before the exit, so a child that outlives
514
+ * several bounded waits accumulates no listeners.
515
+ *
516
+ * @param event - The `exit` event name
517
+ * @param listener - The listener registered through `once`
518
+ * @returns Whatever the emitter returns, which the helpers ignore
519
+ */
520
+ off(event: 'exit', listener: () => void): unknown;
360
521
  }
361
522
 
362
523
  /**
@@ -421,8 +582,9 @@ export declare class ProcessManager implements ProcessManagerInterface {
421
582
  * nothing has started and a throw from one strands no process. A getter that begins `destroy`
422
583
  * without throwing is the one remaining race, and it leaves a bounded residual: the child is
423
584
  * already spawned, so the launch is refused with a {@link createProtocolError} and that child is
424
- * torn down asynchronously, bounded by `grace` plus the confirmation window. The `destroy` barrier
425
- * settled before the refusal, so awaiting it does not cover that teardown.
585
+ * torn down asynchronously, bounded by `grace` plus the confirmation window. The `protocol`
586
+ * refusal throws synchronously before the `destroy` barrier settles. The barrier does not cover
587
+ * that child's asynchronous teardown.
426
588
  *
427
589
  * @param id - The registry key, unique among live children
428
590
  * @param options - The child construction options
@@ -469,8 +631,9 @@ export declare class ProcessManager implements ProcessManagerInterface {
469
631
  * @remarks
470
632
  * A token carrying whitespace or a shell metacharacter is wrapped in double quotes so `cmd.exe`
471
633
  * passes it as one literal argument; an embedded double quote is doubled, which is how `cmd.exe`
472
- * reads a literal quote inside a quoted token. Every other token is left exactly as written, so a
473
- * batch script still receives `%1` without added quotes.
634
+ * reads a literal quote inside a quoted token. The quoted set includes `%`, although quoting cannot
635
+ * stop `cmd.exe` expansion; {@link buildSpawn} therefore refuses `%` in an argument to a Windows
636
+ * batch target.
474
637
  *
475
638
  * @param value - The token to quote
476
639
  * @returns The token, quoted only when `cmd.exe` would otherwise split or interpret it
@@ -483,139 +646,127 @@ export declare class ProcessManager implements ProcessManagerInterface {
483
646
  export declare function quoteArgument(value: string): string;
484
647
 
485
648
  /**
486
- * Reads one environment variable the way the host resolves it.
649
+ * Reads one environment variable under an explicit platform's key rules.
487
650
  *
488
651
  * @remarks
489
652
  * Windows environment keys are case-insensitive, so a merged record can hold `Path` where the
490
- * caller asks for `PATH`. The exact key is tried first and a folded scan runs only on Windows.
653
+ * caller asks for `PATH`. The exact key is tried first and a folded scan runs only for `win32`.
491
654
  *
492
655
  * @param environment - The environment record to read
493
656
  * @param name - The variable name
657
+ * @param platform - The platform whose key rules apply
494
658
  * @returns The value, or `undefined` when the environment declares no such variable
495
659
  *
496
660
  * @example
497
661
  * ```ts
498
- * readVariable({ Path: 'C:\\Windows' }, 'PATH') // 'C:\\Windows' on Windows
662
+ * readPlatformVariable({ Path: 'C:\\Windows' }, 'PATH', 'win32') // 'C:\\Windows'
499
663
  * ```
500
664
  */
501
- export declare function readVariable(environment: Readonly<Record<string, string | undefined>>, name: string): string | undefined;
665
+ export declare function readPlatformVariable(environment: Readonly<Record<string, string | undefined>>, name: string, platform: NodeJS.Platform): string | undefined;
502
666
 
503
667
  /**
504
- * Resolves a command file to the executable path the host would launch.
668
+ * Reads one environment variable the way the current host resolves it.
505
669
  *
506
- * @remarks
507
- * Windows alone needs this: the host searches the working directory before `PATH` and applies
508
- * `PATHEXT`, and Node reproduces neither for a direct spawn. Within each searched directory the
509
- * literal name is tried first and each `PATHEXT` candidate after it, whether or not the name already
510
- * carries an extension — so `report.txt` resolves to a `report.txt` file where one exists and to
511
- * `report.txt.cmd` where none does. The lookup reads the child's effective environment, so an
512
- * overridden `PATH` selects the executable the child would have found. A POSIX host resolves the
513
- * file itself, so the answer there is always `undefined`. An appended extension is spelled the way
514
- * `PATHEXT` spells it rather than the way the directory entry does, which the case-insensitive host
515
- * treats as the same file.
516
- *
517
- * @param file - The command executable name or path
518
- * @param options - The directory searched first and the child's effective environment
519
- * @returns The resolved absolute path, or `undefined` when no candidate is a regular file
670
+ * @param environment - The environment record to read
671
+ * @param name - The variable name
672
+ * @returns The value, or `undefined` when the environment declares no such variable
520
673
  *
521
674
  * @example
522
675
  * ```ts
523
- * resolveExecutable('git', {}) ?? 'git' // an absolute path on Windows, 'git' elsewhere
676
+ * readVariable({ PATH: '/usr/bin' }, 'PATH') // '/usr/bin'
524
677
  * ```
525
678
  */
526
- export declare function resolveExecutable(file: string, options?: ExecutableOptions): string | undefined;
679
+ export declare function readVariable(environment: Readonly<Record<string, string | undefined>>, name: string): string | undefined;
527
680
 
528
681
  /**
529
- * Retains one delivered chunk up to a byte limit and records what the stream delivered.
682
+ * Resolves a command file to the executable path the host would launch.
530
683
  *
531
- * @remarks
532
- * The retained head is kept as chunk slices and joined once, so a long stream never repeatedly
533
- * concatenates a growing buffer. `counts` is a two-slot tally: slot `0` counts every delivered byte
534
- * and slot `1` counts the retained bytes, so a caller compares slot `0` against `limit` to learn
535
- * whether the capture was truncated.
536
- *
537
- * @param chunk - The delivered chunk, ignored when it is not a buffer
538
- * @param chunks - The retained head, appended to in place
539
- * @param counts - The `[delivered, retained]` tally, updated in place
540
- * @param limit - The maximum retained byte count
541
- * @returns Nothing
684
+ * @param file - The command executable name or path
685
+ * @param options - The directory searched first and the child's effective environment
686
+ * @returns The resolved absolute path, or `undefined` when no candidate is a regular file
542
687
  *
543
688
  * @example
544
689
  * ```ts
545
- * const chunks: Buffer[] = []
546
- * const counts = [0, 0]
547
- * retainChunk(Buffer.from('hello'), chunks, counts, 3)
548
- * counts[1] // 3
690
+ * resolveExecutable('git', {}) ?? 'git' // an absolute path on Windows, 'git' elsewhere
549
691
  * ```
550
692
  */
551
- export declare function retainChunk(chunk: unknown, chunks: Buffer_2[], counts: number[], limit: number): void;
693
+ export declare function resolveExecutable(file: string, options?: ExecutableOptions): string | undefined;
552
694
 
553
695
  /**
554
- * Runs one command to completion, buffering its output, and settles with the outcome.
555
- *
556
- * @remarks
557
- * The executable is resolved through {@link buildSpawn}, so no run uses a shell. On a POSIX host the
558
- * child is detached, which is what lets its whole process group be terminated. Standard output and
559
- * error are each byte-bounded by `limit` (default {@link PROCESS_OUTPUT}) and `truncated` reports
560
- * whether either stream exceeded it. A positive `timeout` and an aborting `signal` both terminate the
561
- * child through the same bounded stop, and only the first of them is recorded: a timeout reports
562
- * `expired` and an abort reports `aborted`, never both. After termination the outcome is awaited for
563
- * a bounded window, so a descendant holding the child's stdio cannot keep the run pending. That bound
564
- * covers a terminated run alone: a run with no `timeout` and no `signal` settles on stdio completion
565
- * rather than on process exit, so a descendant that inherited the child's stdio holds the run open
566
- * after the child itself has gone. Give such a run a `timeout`. The child's `environment` merges over
567
- * the parent unless the command is `isolated`, then `options.environment` on top, and `options.input`
568
- * overrides `command.input`. Unless `strict` is `false`, a failed run rejects with a
569
- * {@link createRunError} carrying the {@link RunResult}. An invalid option or command string rejects
570
- * before the child is spawned, because an async function cannot throw synchronously.
571
- *
572
- * @param command - The executable, arguments, and optional environment and input
573
- * @param options - Working directory, timeout, grace, signal, capture limit, and failure delivery
574
- * @returns The settled run outcome
575
- * @throws A {@link ProcessError} coded `invalid` for a malformed option, command string, or batch-bound argument, or one carrying the {@link RunResult} when the run failed and `strict` is not `false`
696
+ * Accumulates byte totals while retaining a bounded stream head.
576
697
  *
577
698
  * @example
578
699
  * ```ts
579
- * const result = await run({ file: 'git', arguments: ['status'] }, { workspace: process.cwd() })
700
+ * const retention = new Retention()
701
+ * retention.retain(Buffer.from('hello'), 3)?.toString('utf8') // 'hel'
702
+ * retention.retained // 3
580
703
  * ```
581
704
  */
582
- export declare function run(command: ProcessCommand, options?: RunOptions): Promise<RunResult>;
705
+ export declare class Retention implements RetentionInterface {
706
+ #private;
707
+ /** The bytes delivered by the stream. */
708
+ get delivered(): number;
709
+ /** The bytes retained from the stream head. */
710
+ get retained(): number;
711
+ /**
712
+ * Retains a delivered chunk within a byte limit.
713
+ *
714
+ * @param chunk - The delivered chunk, ignored when it is not a buffer
715
+ * @param limit - The maximum retained byte count
716
+ * @returns The retained slice, or `undefined` when the chunk contributes no retained bytes
717
+ */
718
+ retain(chunk: unknown, limit: number): Buffer_2 | undefined;
719
+ }
720
+
721
+ /** Exposes byte totals and bounded stream-head retention. */
722
+ export declare interface RetentionInterface {
723
+ /** The bytes delivered by the stream. */
724
+ readonly delivered: number;
725
+ /** The bytes retained from the stream head. */
726
+ readonly retained: number;
727
+ /**
728
+ * Retains a delivered chunk within a byte limit.
729
+ *
730
+ * @param chunk - The delivered chunk, ignored when it is not a buffer
731
+ * @param limit - The maximum retained byte count
732
+ * @returns The retained slice, or `undefined` when the chunk contributes no retained bytes
733
+ */
734
+ retain(chunk: unknown, limit: number): Buffer_2 | undefined;
735
+ }
583
736
 
584
737
  /**
585
- * Runs one command to completion synchronously, buffering its output, and returns the outcome.
738
+ * Takes one owned frozen snapshot of a caller's command.
586
739
  *
587
740
  * @remarks
588
- * The synchronous counterpart of {@link run}, spawned through the same resolver and never through a
589
- * shell. The host offers no cooperative termination window and no in-flight cancellation, so this
590
- * contract carries neither. A positive `timeout` and an output overflow both end the child with
591
- * `SIGKILL`: an overflow reports `truncated` and `failed` together and trims the partial output to
592
- * `limit`, where {@link run} keeps reading and reports `truncated` without failing. The environment
593
- * and input follow the same merge as {@link run}. Unless `strict` is `false`, a failed run throws a
594
- * {@link createRunError} carrying the {@link RunResult}.
741
+ * Every public entry point snapshots before it validates, so the object validated is the object
742
+ * spawned. Each property is read exactly once, because reading one runs the caller's own getter: a
743
+ * command whose `file` changes between reads would otherwise validate one executable and spawn
744
+ * another. The argument vector and the environment record are copied and frozen, so a caller
745
+ * mutating either after the call cannot reach the spawn. An absent optional stays absent rather than
746
+ * becoming an explicit `undefined`.
595
747
  *
596
- * @param command - The executable, arguments, and optional environment and input
597
- * @param options - Working directory, timeout, capture limit, and failure delivery
598
- * @returns The run outcome
599
- * @throws A {@link ProcessError} coded `invalid` for a malformed option, command string, or batch-bound argument, or one carrying the {@link RunResult} when the run failed and `strict` is not `false`
748
+ * @param command - The caller's command, whose properties may be getters
749
+ * @returns A frozen command carrying the values read at this instant
600
750
  *
601
751
  * @example
602
752
  * ```ts
603
- * const result = runSync({ file: 'git', arguments: ['--version'] }, { strict: false })
753
+ * snapshotCommand({ file: 'git', arguments: ['status'] }) // { file: 'git', arguments: ['status'] }
604
754
  * ```
605
755
  */
606
- export declare function runSync(command: ProcessCommand, options?: RunSyncOptions): RunResult;
756
+ export declare function snapshotCommand(command: ProcessCommand): ProcessCommand;
607
757
 
608
758
  /**
609
759
  * Terminates one child process tree and reports whether its native exit was observed.
610
760
  *
611
761
  * @remarks
612
- * Windows ends the tree at once through {@link killTree}, falling back to a direct kill when the
613
- * utility fails, because the host has no cooperative termination to offer. A POSIX host signals the
614
- * process group `SIGTERM`, waits `grace`, then signals `SIGKILL`. No signal is initiated once the
615
- * native exit is observed; the window between initiating a signal and the host delivering it belongs
616
- * to the operating system. `confirm` bounds each awaited step rather than the call as a whole: on
617
- * Windows it bounds the `taskkill` call and then the final wait, and on a POSIX host `grace` bounds
618
- * the cooperative wait and `confirm` the final one. Never rejects.
762
+ * Windows ends the whole tree at once through {@link killTree}, falling back to a direct kill after
763
+ * the utility fails, because the host has no cooperative termination to offer. A POSIX host signals
764
+ * the process group `SIGTERM`, falls back to the direct child when no group owns its pid, waits
765
+ * `grace`, then sends `SIGKILL` through the same route. No signal is initiated after the native exit
766
+ * is observed; the window between initiating a signal and the host delivering it belongs to the
767
+ * operating system. `confirm` bounds each awaited step rather than the call as a whole: on Windows
768
+ * it bounds the `taskkill` call and then the final wait, and on a POSIX host `grace` bounds the
769
+ * cooperative wait and `confirm` the final one. Never rejects.
619
770
  *
620
771
  * @param child - The child boundary
621
772
  * @param grace - The cooperative POSIX window in milliseconds between `SIGTERM` and `SIGKILL`
@@ -780,6 +931,6 @@ export declare function validateWorkspace(workspace: string | undefined): void;
780
931
  * await waitForExit(child, 5_000)
781
932
  * ```
782
933
  */
783
- export declare function waitForExit(child: Pick<ProcessChild, 'exitCode' | 'signalCode' | 'once'>, timeout: number): Promise<void>;
934
+ export declare function waitForExit(child: Pick<ProcessChild, 'exitCode' | 'signalCode' | 'once' | 'off'>, timeout: number): Promise<void>;
784
935
 
785
936
  export { }