@orkestrel/process 0.0.1 → 0.0.3

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.
@@ -1,5 +1,7 @@
1
1
  import { Buffer as Buffer_2 } from 'node:buffer';
2
+ import { DetachOptions } from '@orkestrel/process';
2
3
  import { EmitterInterface } from '@orkestrel/emitter';
4
+ import { ExecutableOptions } from '@orkestrel/process';
3
5
  import { ProcessCommand } from '@orkestrel/process';
4
6
  import { ProcessEventMap } from '@orkestrel/process';
5
7
  import { ProcessExit } from '@orkestrel/process';
@@ -8,45 +10,65 @@ import { ProcessManagerEventMap } from '@orkestrel/process';
8
10
  import { ProcessManagerInterface } from '@orkestrel/process';
9
11
  import { ProcessManagerOptions } from '@orkestrel/process';
10
12
  import { ProcessOptions } from '@orkestrel/process';
13
+ import { RunInput } from '@orkestrel/process';
11
14
  import { RunOptions } from '@orkestrel/process';
12
15
  import { RunResult } from '@orkestrel/process';
16
+ import { RunSyncOptions } from '@orkestrel/process';
17
+ import { SpawnInput } from '@orkestrel/process';
13
18
 
14
19
  /**
15
- * Builds one settled {@link RunResult} from a completed one-shot run's captured bytes and exit.
20
+ * Builds one settled {@link RunResult} from a completed run's captured bytes and terminal facts.
16
21
  *
17
22
  * @remarks
18
- * `failed` is derived: a run failed when it timed out, was ended by a signal, or exited with a
19
- * code other than `0` — a `null` code from a spawn fault is therefore a failure. Both output
20
- * buffers are byte-bounded by `limit` before decoding.
21
- *
22
- * @param command - The diagnostic command line
23
- * @param stdout - The captured standard output bytes
24
- * @param stderr - The captured standard error bytes
25
- * @param code - The exit code, or `null` when a signal or spawn fault ended the run
26
- * @param signal - The terminating signal, or `null`
27
- * @param timedOut - If `true`, the run's own timeout elapsed; if `false`, it did not
28
- * @param limit - The maximum captured bytes for each stream
23
+ * `failed` is derived: a run failed when it timed out, was aborted, ended on a host fault, was ended
24
+ * 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
26
+ * place the captured bytes are decoded.
27
+ *
28
+ * @param input - The captured bytes, terminal state, and capture limit
29
29
  * @returns The frozen run outcome
30
30
  *
31
31
  * @example
32
32
  * ```ts
33
- * buildRunResult('git status', Buffer.from('ok'), Buffer.alloc(0), 0, null, false, 1024)
33
+ * buildRunResult({
34
+ * command: 'git status',
35
+ * stdout: Buffer.from('ok'),
36
+ * stderr: Buffer.alloc(0),
37
+ * code: 0,
38
+ * signal: null,
39
+ * expired: false,
40
+ * aborted: false,
41
+ * truncated: false,
42
+ * limit: 1_024,
43
+ * }).failed // false
34
44
  * ```
35
45
  */
36
- export declare function buildRunResult(command: string, stdout: Buffer_2, stderr: Buffer_2, code: number | null, signal: string | null, timedOut: boolean, limit: number): RunResult;
46
+ export declare function buildRunResult(input: RunInput): RunResult;
37
47
 
38
48
  /**
39
- * Renders one command into its diagnostic command line.
49
+ * Builds the resolved spawn form of one command.
50
+ *
51
+ * @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.
40
60
  *
41
61
  * @param command - The executable and its argument vector
42
- * @returns The space-joined command line, for a {@link RunResult} and error messages
62
+ * @param options - The directory searched first and the child's effective environment
63
+ * @returns The file, argument vector, and verbatim flag to spawn with
64
+ * @throws A {@link ProcessError} coded `invalid` when the host is Windows, the resolved target is a batch script, and an argument carries a percent sign
43
65
  *
44
66
  * @example
45
67
  * ```ts
46
- * commandLine({ file: 'git', arguments: ['status'] }) // 'git status'
68
+ * buildSpawn({ file: 'node', arguments: ['--version'] }).verbatim // false
47
69
  * ```
48
70
  */
49
- export declare function commandLine(command: ProcessCommand): string;
71
+ export declare function buildSpawn(command: ProcessCommand, options?: ExecutableOptions): SpawnInput;
50
72
 
51
73
  /**
52
74
  * Creates one supervised child process.
@@ -82,16 +104,86 @@ export declare function createProcess(options: ProcessOptions): ProcessInterface
82
104
  */
83
105
  export declare function createProcessManager(options?: ProcessManagerOptions): ProcessManagerInterface;
84
106
 
107
+ /**
108
+ * Spawns one command as a detached process and returns without waiting for it.
109
+ *
110
+ * @remarks
111
+ * The child owns no stdio and is unreferenced, so it outlives this process and nothing here observes
112
+ * its outcome. The error listener is attached before the child is unreferenced, so a post-spawn host
113
+ * fault is swallowed rather than crashing the caller; an invalid command is refused before anything
114
+ * is spawned.
115
+ *
116
+ * @param command - The executable, arguments, and optional environment
117
+ * @param options - The working directory the detached child starts in
118
+ * @returns Nothing
119
+ * @throws A {@link ProcessError} coded `invalid` when the working directory or a command string is malformed
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * detach({ file: 'node', arguments: ['daemon.js'] }, { workspace: process.cwd() })
124
+ * ```
125
+ */
126
+ export declare function detach(command: ProcessCommand, options?: DetachOptions): void;
127
+
128
+ /**
129
+ * Renders one command into its diagnostic command line.
130
+ *
131
+ * @param command - The executable and its argument vector
132
+ * @returns The space-joined command line, for a {@link RunResult} and error messages
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * formatCommand({ file: 'git', arguments: ['status'] }) // 'git status'
137
+ * ```
138
+ */
139
+ export declare function formatCommand(command: ProcessCommand): string;
140
+
141
+ /**
142
+ * Checks whether a child process has reached its native exit.
143
+ *
144
+ * @remarks
145
+ * The host sets exactly one of these fields when the process ends, so together they are the
146
+ * authoritative liveness answer. Reading them before each signal is what keeps a signal from being
147
+ * initiated against a pid the host may have reused.
148
+ *
149
+ * @param child - The child boundary
150
+ * @returns True when the process has exited; false while it is live
151
+ *
152
+ * @example
153
+ * ```ts
154
+ * isExited({ exitCode: 0, signalCode: null }) // true
155
+ * ```
156
+ */
157
+ export declare function isExited(child: Pick<ProcessChild, 'exitCode' | 'signalCode'>): boolean;
158
+
159
+ /**
160
+ * Checks whether a path names a regular file.
161
+ *
162
+ * @remarks
163
+ * Existence alone does not make a lookup hit: a directory named `git` on `PATH` is not the `git`
164
+ * command. An unreadable or malformed path reports `false` rather than throwing.
165
+ *
166
+ * @param target - The absolute or relative path to inspect
167
+ * @returns True when the path resolves to a regular file; false otherwise
168
+ *
169
+ * @example
170
+ * ```ts
171
+ * isFile(process.execPath) // true
172
+ * ```
173
+ */
174
+ export declare function isFile(target: string): boolean;
175
+
85
176
  /**
86
177
  * Signals one owned child process, or its detached process group on a POSIX host.
87
178
  *
88
179
  * @remarks
89
180
  * On a POSIX host a child spawned `detached` leads its own process group, so signalling the
90
181
  * negated pid reaches the whole tree. On Windows, or when no pid is available, the signal goes to
91
- * the child directly. A throw during signalling is swallowed: the process can exit and win the
92
- * signal race, and the child's close event remains the authoritative terminal state.
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.
93
185
  *
94
- * @param child - The child process signal boundary
186
+ * @param child - The child boundary
95
187
  * @param signal - The signal to deliver
96
188
  * @returns Nothing
97
189
  *
@@ -100,39 +192,68 @@ export declare function createProcessManager(options?: ProcessManagerOptions): P
100
192
  * killProcess(child, 'SIGTERM')
101
193
  * ```
102
194
  */
103
- export declare function killProcess(child: {
104
- readonly pid?: number | undefined;
105
- kill(signal: NodeJS.Signals): boolean;
106
- }, signal: NodeJS.Signals): void;
195
+ export declare function killProcess(child: Pick<ProcessChild, 'pid' | 'kill'>, signal: NodeJS.Signals): void;
196
+
197
+ /**
198
+ * Kills one Windows process tree through `taskkill`.
199
+ *
200
+ * @remarks
201
+ * Windows has no process group a signal can reach, so the whole tree is ended by the host utility,
202
+ * addressed by its absolute `System32` path so a `PATH` override cannot substitute another program.
203
+ * The call is bounded: it is killed itself when `timeout` elapses, which is what keeps a termination
204
+ * from waiting on the host indefinitely. A tree is discoverable only while its root lives, so a
205
+ * descendant that outlives the root is beyond this mechanism.
206
+ *
207
+ * @param pid - The root process id of the tree to end
208
+ * @param timeout - The milliseconds the utility is given before it is killed
209
+ * @returns True when the utility reported success; false when it failed, was unavailable, or was cut off
210
+ *
211
+ * @example
212
+ * ```ts
213
+ * await killTree(child.pid ?? 0, 5_000)
214
+ * ```
215
+ */
216
+ export declare function killTree(pid: number, timeout: number): Promise<boolean>;
107
217
 
108
218
  /**
109
- * Merges environment overrides over the current process environment.
219
+ * Merges environment overrides into the environment one child receives.
110
220
  *
111
221
  * @remarks
112
- * Later maps override earlier ones and an `undefined` value unsets a key. The base is the parent
113
- * process environment, so a child inherits it unless a key is overridden or unset.
222
+ * Later maps override earlier ones and an `undefined` value unsets a key. On Windows the keys fold
223
+ * case-insensitively and the last writer wins, so `PATH` followed by `Path` yields one variable
224
+ * rather than two the host would resolve unpredictably.
114
225
  *
226
+ * @param isolated - If `true`, the parent environment is excluded; if `false`, the overrides layer over it
115
227
  * @param base - The command's own environment overrides
116
228
  * @param override - Per-invocation overrides applied last
117
- * @returns The merged environment for a spawn
229
+ * @returns The environment for a spawn, carrying no unset key
118
230
  *
119
231
  * @example
120
232
  * ```ts
121
- * mergeEnvironment({ TOKEN: 'a' }, { TOKEN: undefined }) // TOKEN unset
233
+ * mergeEnvironment(false, { TOKEN: 'a' }, { TOKEN: undefined }) // TOKEN unset
122
234
  * ```
123
235
  */
124
- export declare function mergeEnvironment(base?: Readonly<Record<string, string | undefined>>, override?: Readonly<Record<string, string | undefined>>): NodeJS.ProcessEnv;
236
+ export declare function mergeEnvironment(isolated: boolean, base?: Readonly<Record<string, string | undefined>>, override?: Readonly<Record<string, string | undefined>>): NodeJS.ProcessEnv;
125
237
 
126
238
  /**
127
- * One supervised child process with eagerly framed output and bounded termination.
239
+ * One supervised child process with framed output, a bounded backlog, and bounded termination.
128
240
  *
129
241
  * @remarks
130
- * Standard output is drained eagerly through `readline`, so `exit` resolves and a late consumer of
131
- * `lines` still receives every framed line, including a final line written without a trailing
132
- * newline. Standard error is decoded and forwarded live as the `stderr` event while a byte-bounded
133
- * raw tail is retained as `evidence`. The typed `emitter` also carries the terminal `exit`,
134
- * alongside the `exit` promise. `stop` terminates the child through `SIGTERM`, then `SIGKILL` after
135
- * `grace`, and is idempotent; `destroy` stops the child and destroys the emitter last.
242
+ * 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
247
+ * 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.
136
257
  *
137
258
  * @example
138
259
  * ```ts
@@ -141,7 +262,6 @@ export declare function mergeEnvironment(base?: Readonly<Record<string, string |
141
262
  * const child = new Process({
142
263
  * command: { file: 'node', arguments: ['--version'] },
143
264
  * workspace: process.cwd(),
144
- * grace: 5000,
145
265
  * })
146
266
  * const exit = await child.exit
147
267
  * await child.destroy()
@@ -150,14 +270,15 @@ export declare function mergeEnvironment(base?: Readonly<Record<string, string |
150
270
  export declare class Process implements ProcessInterface {
151
271
  #private;
152
272
  /**
153
- * Spawn one child process and begin eager stream capture.
273
+ * Spawn one child process and begin stream capture.
154
274
  *
155
- * @param options - Command, workspace, termination, evidence, stdin, and observation settings
275
+ * @param options - Command, workspace, termination, capture, stdin, and observation settings
276
+ * @throws A {@link ProcessError} coded `invalid` when an option or command string is malformed
156
277
  */
157
278
  constructor(options: ProcessOptions);
158
279
  /** The typed lifecycle observation surface. */
159
280
  get emitter(): EmitterInterface<ProcessEventMap>;
160
- /** The eagerly captured stdout lines, in arrival order, ending when the child's stdout closes. */
281
+ /** The captured stdout lines, in arrival order, ending when the child's stdout closes. */
161
282
  get lines(): AsyncIterable<string>;
162
283
  /** The decoded byte-bounded stderr tail. */
163
284
  get evidence(): string;
@@ -166,32 +287,92 @@ export declare class Process implements ProcessInterface {
166
287
  /**
167
288
  * Write one line to the open standard-input channel.
168
289
  *
290
+ * @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`.
295
+ *
169
296
  * @param text - The line text without its trailing newline
170
- * @returns True when the channel accepted the line without backpressure; false otherwise
297
+ * @returns True when the line reached the host without error; false when the channel was closed, destroyed, ended, or the write failed
171
298
  */
172
- send(text: string): boolean;
299
+ send(text: string): Promise<boolean>;
173
300
  /**
174
301
  * Terminate the child process tree and await its observed exit.
175
302
  *
176
- * @returns A promise that resolves after bounded termination
303
+ * @remarks
304
+ * Never rejects, and every call shares one termination.
305
+ *
306
+ * @returns True when the child's native exit was observed; false when the confirmation deadline elapsed without it
177
307
  */
178
- stop(): Promise<void>;
308
+ stop(): Promise<boolean>;
179
309
  /**
180
- * Stop the child and destroy the observation emitter.
310
+ * Stop the child, close its standard-input channel, and destroy the observation emitter.
311
+ *
312
+ * @remarks
313
+ * Always resolves, including when termination was never confirmed.
181
314
  *
182
315
  * @returns The stable barrier shared by every call
183
316
  */
184
317
  destroy(): Promise<void>;
185
318
  }
186
319
 
320
+ /**
321
+ * The Node-side contracts of `@orkestrel/process/server`.
322
+ *
323
+ * @remarks
324
+ * The host-independent contracts live in `@orkestrel/process`. This module declares only what a
325
+ * Node child process boundary requires, which the published contracts cannot express without
326
+ * naming `node:child_process` types.
327
+ */
328
+ /**
329
+ * The child boundary the termination helpers drive.
330
+ *
331
+ * @remarks
332
+ * A `ChildProcess` satisfies this structurally, and so does any object carrying the same four
333
+ * members, which is what lets a caller drive `stopChild` over a child it spawned itself. Each helper
334
+ * takes the slice of this contract it reads: `exitCode` and `signalCode` are the host's
335
+ * authoritative liveness answer, `pid` addresses a POSIX process group, `kill` delivers one signal,
336
+ * and `once` reports the native exit.
337
+ */
338
+ export declare interface ProcessChild {
339
+ /** The process id the host assigned, or `undefined` when the spawn never produced one. */
340
+ readonly pid?: number | undefined;
341
+ /** The exit code, or `null` while the process is live or a signal ended it. */
342
+ readonly exitCode: number | null;
343
+ /** The terminating signal name, or `null` while the process is live or it exited on its own. */
344
+ readonly signalCode: string | null;
345
+ /**
346
+ * Deliver one signal to the process.
347
+ *
348
+ * @param signal - The signal to deliver
349
+ * @returns True when the host accepted the signal; false otherwise
350
+ */
351
+ kill(signal: NodeJS.Signals): boolean;
352
+ /**
353
+ * Register a one-shot listener for the native exit.
354
+ *
355
+ * @param event - The `exit` event name
356
+ * @param listener - The listener invoked once the process exits
357
+ * @returns Whatever the emitter returns, which the helpers ignore
358
+ */
359
+ once(event: 'exit', listener: () => void): unknown;
360
+ }
361
+
187
362
  /**
188
363
  * A keyed registry of live supervised child processes.
189
364
  *
190
365
  * @remarks
191
366
  * A child launched under an id joins the registry and emits `launch`; when it settles it removes
192
- * itself and emits `exit`, so `count` and `processes` reflect only live children. `launch` throws
193
- * a {@link createDuplicateError} when the id is already live — a spawn fault surfaces through the
194
- * returned child's `exit`, never from `launch`. `destroy` stops every child, then destroys the
367
+ * itself and emits `exit`, so `count` and `processes` reflect only live children. The id is reserved
368
+ * before the child is constructed and released when construction throws, so a refused launch never
369
+ * strands its key. Eviction follows the child's own `exit` promise, which no listener can forge, so
370
+ * it lands one microtask after the child's public `exit` event: a listener on that event still sees
371
+ * the child registered. `launch` throws a {@link createDuplicateError} when the id is already live
372
+ * and a {@link createProtocolError} after `destroy` has begun, including when the caller's own option
373
+ * getter begins that teardown mid-construction — a spawn fault surfaces through the returned child's
374
+ * `exit`, never from `launch`. `destroy` awaits every child's own teardown, which destroys each
375
+ * child's observation emitter and leaves every subscription on it silently inert, then destroys the
195
376
  * registry emitter last.
196
377
  *
197
378
  * @example
@@ -202,7 +383,6 @@ export declare class Process implements ProcessInterface {
202
383
  * const child = manager.launch('build', {
203
384
  * command: { file: 'node', arguments: ['build.js'] },
204
385
  * workspace: process.cwd(),
205
- * grace: 5000,
206
386
  * })
207
387
  * await child.exit
208
388
  * await manager.destroy()
@@ -236,77 +416,163 @@ export declare class ProcessManager implements ProcessManagerInterface {
236
416
  /**
237
417
  * Spawn and register one child under `id`.
238
418
  *
419
+ * @remarks
420
+ * {@link Process} reads every option before it spawns, so a caller's own option getter runs while
421
+ * nothing has started and a throw from one strands no process. A getter that begins `destroy`
422
+ * without throwing is the one remaining race, and it leaves a bounded residual: the child is
423
+ * 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.
426
+ *
239
427
  * @param id - The registry key, unique among live children
240
428
  * @param options - The child construction options
241
429
  * @returns The launched child
242
- * @throws A {@link ProcessError} coded `duplicate` when `id` is already live
430
+ * @throws A {@link ProcessError} coded `duplicate` when `id` is already live, `protocol` when the registry is being destroyed, or `invalid` when an option or command string is malformed
243
431
  */
244
432
  launch(id: string, options: ProcessOptions): ProcessInterface;
245
433
  /**
246
434
  * Terminate the named children and await their exit.
247
435
  *
248
436
  * @param ids - The registry keys to stop
249
- * @returns True when every named child stopped, false when any id was not live
437
+ * @returns True when every named child was live and its exit was confirmed; false otherwise
250
438
  */
251
439
  stop(ids: readonly string[]): Promise<boolean>;
252
440
  /**
253
441
  * Terminate one child and await its exit.
254
442
  *
255
443
  * @param id - The registry key to stop
256
- * @returns True when the child was live and stopped, false when the id was not live
444
+ * @returns True when the child was live and its exit was confirmed; false when the id was not live or the confirmation deadline elapsed
257
445
  */
258
446
  stop(id: string): Promise<boolean>;
259
447
  /**
260
448
  * Terminate every live child and await their exit.
261
449
  *
262
- * @returns A promise that resolves after all children stop
450
+ * @returns A promise that resolves after every child stops
263
451
  */
264
452
  stop(): Promise<void>;
265
453
  /**
266
454
  * Stop every child, then destroy the registry emitter last.
267
455
  *
456
+ * @remarks
457
+ * Always resolves, and refuses a later `launch` with a {@link createProtocolError}. Each child is
458
+ * destroyed rather than merely stopped, so its own observation emitter is destroyed too and every
459
+ * subscription on it goes silently inert.
460
+ *
268
461
  * @returns The stable barrier shared by every call
269
462
  */
270
463
  destroy(): Promise<void>;
271
464
  }
272
465
 
273
466
  /**
274
- * Checks whether a command file must be spawned through a shell on the current host.
467
+ * Quotes one command-line token for a `cmd.exe` command line.
468
+ *
469
+ * @remarks
470
+ * A token carrying whitespace or a shell metacharacter is wrapped in double quotes so `cmd.exe`
471
+ * 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.
474
+ *
475
+ * @param value - The token to quote
476
+ * @returns The token, quoted only when `cmd.exe` would otherwise split or interpret it
477
+ *
478
+ * @example
479
+ * ```ts
480
+ * quoteArgument('a&b') // '"a&b"'
481
+ * ```
482
+ */
483
+ export declare function quoteArgument(value: string): string;
484
+
485
+ /**
486
+ * Reads one environment variable the way the host resolves it.
275
487
  *
276
488
  * @remarks
277
- * Node v22+ refuses to spawn a Windows `.cmd` or `.bat` batch file directly and returns `EINVAL`,
278
- * and a bare command name such as `git` resolves through `PATHEXT` to a batch shim rather than an
279
- * executable. Both cases run through the shell. A POSIX host, and a Windows path carrying a real
280
- * executable extension such as `.exe`, spawn directly. When the result is `true`, treat the
281
- * command file and arguments as trusted: shell invocation exposes shell metacharacters.
489
+ * 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.
491
+ *
492
+ * @param environment - The environment record to read
493
+ * @param name - The variable name
494
+ * @returns The value, or `undefined` when the environment declares no such variable
495
+ *
496
+ * @example
497
+ * ```ts
498
+ * readVariable({ Path: 'C:\\Windows' }, 'PATH') // 'C:\\Windows' on Windows
499
+ * ```
500
+ */
501
+ export declare function readVariable(environment: Readonly<Record<string, string | undefined>>, name: string): string | undefined;
502
+
503
+ /**
504
+ * Resolves a command file to the executable path the host would launch.
505
+ *
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.
282
516
  *
283
517
  * @param file - The command executable name or path
284
- * @returns True when the file must run through a shell; false when it spawns directly
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
520
+ *
521
+ * @example
522
+ * ```ts
523
+ * resolveExecutable('git', {}) ?? 'git' // an absolute path on Windows, 'git' elsewhere
524
+ * ```
525
+ */
526
+ export declare function resolveExecutable(file: string, options?: ExecutableOptions): string | undefined;
527
+
528
+ /**
529
+ * Retains one delivered chunk up to a byte limit and records what the stream delivered.
530
+ *
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
285
542
  *
286
543
  * @example
287
544
  * ```ts
288
- * requiresShell('git') // true on Windows, false on POSIX
545
+ * const chunks: Buffer[] = []
546
+ * const counts = [0, 0]
547
+ * retainChunk(Buffer.from('hello'), chunks, counts, 3)
548
+ * counts[1] // 3
289
549
  * ```
290
550
  */
291
- export declare function requiresShell(file: string): boolean;
551
+ export declare function retainChunk(chunk: unknown, chunks: Buffer_2[], counts: number[], limit: number): void;
292
552
 
293
553
  /**
294
554
  * Runs one command to completion, buffering its output, and settles with the outcome.
295
555
  *
296
556
  * @remarks
297
- * The child is spawned through the cross-platform command resolver ({@link requiresShell}) and, on
298
- * a POSIX host, detached so its whole process group can be terminated. Standard output and error
299
- * are each byte-bounded by `limit` (default {@link PROCESS_OUTPUT}). A positive `timeout` arms a
300
- * timer that terminates the child through `SIGTERM`, then `SIGKILL` after `grace`
301
- * (default {@link PROCESS_GRACE}); aborting `signal` terminates the same way. The child's
302
- * `environment` merges over the parent, then `options.environment` on top, and `options.input`
303
- * overrides `command.input`. Unless `reject` is `false`, a failed run rejects with a
304
- * {@link createRunError} carrying the {@link RunResult}.
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.
305
571
  *
306
572
  * @param command - The executable, arguments, and optional environment and input
307
- * @param options - Working directory, timeout, grace, signal, capture limit, and reject behavior
573
+ * @param options - Working directory, timeout, grace, signal, capture limit, and failure delivery
308
574
  * @returns The settled run outcome
309
- * @throws A {@link ProcessError} carrying the {@link RunResult} when the run failed and `reject` is not `false`
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`
310
576
  *
311
577
  * @example
312
578
  * ```ts
@@ -319,24 +585,49 @@ export declare function run(command: ProcessCommand, options?: RunOptions): Prom
319
585
  * Runs one command to completion synchronously, buffering its output, and returns the outcome.
320
586
  *
321
587
  * @remarks
322
- * The synchronous counterpart of {@link run}. The child is spawned through the same cross-platform
323
- * command resolver ({@link requiresShell}). Standard output and error are each byte-bounded by
324
- * `limit` (default {@link PROCESS_OUTPUT}). A positive `timeout` is enforced by the host, which
325
- * ends the child with `SIGKILL` when it elapses. The environment and input follow the same merge
326
- * as {@link run}. Unless `reject` is `false`, a failed run throws a {@link createRunError} carrying
327
- * the {@link RunResult}.
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}.
328
595
  *
329
596
  * @param command - The executable, arguments, and optional environment and input
330
- * @param options - Working directory, timeout, signal, capture limit, and reject behavior
597
+ * @param options - Working directory, timeout, capture limit, and failure delivery
331
598
  * @returns The run outcome
332
- * @throws A {@link ProcessError} carrying the {@link RunResult} when the run failed and `reject` is not `false`
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`
333
600
  *
334
601
  * @example
335
602
  * ```ts
336
- * const result = runSync({ file: 'git', arguments: ['--version'] }, { reject: false })
603
+ * const result = runSync({ file: 'git', arguments: ['--version'] }, { strict: false })
337
604
  * ```
338
605
  */
339
- export declare function runSync(command: ProcessCommand, options?: RunOptions): RunResult;
606
+ export declare function runSync(command: ProcessCommand, options?: RunSyncOptions): RunResult;
607
+
608
+ /**
609
+ * Terminates one child process tree and reports whether its native exit was observed.
610
+ *
611
+ * @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.
619
+ *
620
+ * @param child - The child boundary
621
+ * @param grace - The cooperative POSIX window in milliseconds between `SIGTERM` and `SIGKILL`
622
+ * @param confirm - The milliseconds the native exit is awaited after the final kill
623
+ * @returns True when the native exit was observed; false when `confirm` elapsed without it
624
+ *
625
+ * @example
626
+ * ```ts
627
+ * const confirmed = await stopChild(child, 5_000, 5_000)
628
+ * ```
629
+ */
630
+ export declare function stopChild(child: ProcessChild, grace: number, confirm: number): Promise<boolean>;
340
631
 
341
632
  /**
342
633
  * Trims a buffer to at most `limit` leading bytes without splitting a UTF-8 sequence.
@@ -346,7 +637,7 @@ export declare function runSync(command: ProcessCommand, options?: RunOptions):
346
637
  * a multibyte sequence, the end retreats to the sequence's start so the retained bytes always end
347
638
  * on a code-point boundary and decode without a replacement character.
348
639
  *
349
- * @param buffer - The accumulated bytes
640
+ * @param bytes - The accumulated bytes
350
641
  * @param limit - The maximum retained byte count
351
642
  * @returns The leading bytes, at most `limit` and never ending mid-sequence
352
643
  *
@@ -355,7 +646,7 @@ export declare function runSync(command: ProcessCommand, options?: RunOptions):
355
646
  * trimHead(Buffer.from('hello'), 3) // <Buffer 68 65 6c>
356
647
  * ```
357
648
  */
358
- export declare function trimHead(buffer: Buffer_2, limit: number): Buffer_2;
649
+ export declare function trimHead(bytes: Uint8Array, limit: number): Buffer_2;
359
650
 
360
651
  /**
361
652
  * Trims a buffer to at most `limit` trailing bytes without splitting a UTF-8 sequence.
@@ -365,7 +656,7 @@ export declare function trimHead(buffer: Buffer_2, limit: number): Buffer_2;
365
656
  * multibyte sequence, the start advances past the leading continuation bytes so the retained
366
657
  * bytes always begin on a code-point boundary.
367
658
  *
368
- * @param buffer - The accumulated bytes
659
+ * @param bytes - The accumulated bytes
369
660
  * @param limit - The maximum retained byte count
370
661
  * @returns The trailing bytes, at most `limit` and never starting mid-sequence
371
662
  *
@@ -374,6 +665,121 @@ export declare function trimHead(buffer: Buffer_2, limit: number): Buffer_2;
374
665
  * trimTail(Buffer.from('hello'), 3) // <Buffer 6c 6c 6f>
375
666
  * ```
376
667
  */
377
- export declare function trimTail(buffer: Buffer_2, limit: number): Buffer_2;
668
+ export declare function trimTail(bytes: Uint8Array, limit: number): Buffer_2;
669
+
670
+ /**
671
+ * Validates one byte-valued option.
672
+ *
673
+ * @param value - The value to inspect, or `undefined` when the option was omitted
674
+ * @param subject - The rejected input named as the caller wrote it, such as `option 'limit'`
675
+ * @param minimum - The smallest accepted value
676
+ * @returns Nothing; a successful return means the value bounds a real byte count
677
+ * @throws A {@link ProcessError} coded `invalid` when the value is not a safe integer at or above `minimum`
678
+ *
679
+ * @example
680
+ * ```ts
681
+ * validateBytes(1_024, "option 'limit'", 0) // returns
682
+ * ```
683
+ */
684
+ export declare function validateBytes(value: number | undefined, subject: string, minimum: number): void;
685
+
686
+ /**
687
+ * Validates every spawn-bound string of one command.
688
+ *
689
+ * @param command - The executable, arguments, and optional environment overrides
690
+ * @returns Nothing; a successful return means the command reaches the host intact
691
+ * @throws A {@link ProcessError} coded `invalid` when the file is empty or any string carries a NUL
692
+ *
693
+ * @example
694
+ * ```ts
695
+ * validateCommand({ file: 'git', arguments: ['status'] }) // returns
696
+ * ```
697
+ */
698
+ export declare function validateCommand(command: ProcessCommand): void;
699
+
700
+ /**
701
+ * Validates every spawn-bound string of one environment override map.
702
+ *
703
+ * @remarks
704
+ * The host refuses a NUL in a variable name or value with a raw `ERR_INVALID_ARG_VALUE`, so the same
705
+ * refusal runs here for a command's own overrides and for a per-invocation override map alike.
706
+ *
707
+ * @param environment - The override map, or `undefined` when none was supplied
708
+ * @returns Nothing; a successful return means every name and value reaches the host intact
709
+ * @throws A {@link ProcessError} coded `invalid` when a name is empty or a name or value carries a NUL
710
+ *
711
+ * @example
712
+ * ```ts
713
+ * validateEnvironment({ TOKEN: 'a' }) // returns
714
+ * ```
715
+ */
716
+ export declare function validateEnvironment(environment: Readonly<Record<string, string | undefined>> | undefined): void;
717
+
718
+ /**
719
+ * Validates one spawn-bound string.
720
+ *
721
+ * @remarks
722
+ * A NUL character terminates a string for the host, so it is refused everywhere rather than
723
+ * reaching the spawn as a raw `ERR_INVALID_ARG_VALUE`.
724
+ *
725
+ * @param value - The value to inspect
726
+ * @param subject - The rejected input named as the caller wrote it, such as `command file`
727
+ * @param required - If `true`, an empty string is refused; if `false`, it is accepted
728
+ * @returns Nothing; a successful return means the string is spawn-safe
729
+ * @throws A {@link ProcessError} coded `invalid` when the value is not a string, carries a NUL, or is required and empty
730
+ *
731
+ * @example
732
+ * ```ts
733
+ * validateText('status', 'command argument', false) // returns
734
+ * ```
735
+ */
736
+ export declare function validateText(value: unknown, subject: string, required: boolean): void;
737
+
738
+ /**
739
+ * Validates one timer-valued option in milliseconds.
740
+ *
741
+ * @remarks
742
+ * The host truncates a fractional delay and converts anything above {@link PROCESS_TIMER} to one
743
+ * millisecond, so a requested 25-day timeout would otherwise fire immediately.
744
+ *
745
+ * @param value - The value to inspect, or `undefined` when the option was omitted
746
+ * @param subject - The rejected input named as the caller wrote it, such as `option 'grace'`
747
+ * @returns Nothing; a successful return means the value schedules as written
748
+ * @throws A {@link ProcessError} coded `invalid` when the value is not a non-negative integer at or below `PROCESS_TIMER`
749
+ *
750
+ * @example
751
+ * ```ts
752
+ * validateTimer(5_000, "option 'grace'") // returns
753
+ * ```
754
+ */
755
+ export declare function validateTimer(value: number | undefined, subject: string): void;
756
+
757
+ /**
758
+ * Validates the working directory one child starts in.
759
+ *
760
+ * @param workspace - The directory, or `undefined` when the option was omitted
761
+ * @returns Nothing; a successful return means the directory reaches the host intact
762
+ * @throws A {@link ProcessError} coded `invalid` when the directory is empty or carries a NUL
763
+ *
764
+ * @example
765
+ * ```ts
766
+ * validateWorkspace(process.cwd()) // returns
767
+ * ```
768
+ */
769
+ export declare function validateWorkspace(workspace: string | undefined): void;
770
+
771
+ /**
772
+ * Waits for one child process's native exit, bounded by a deadline.
773
+ *
774
+ * @param child - The child boundary
775
+ * @param timeout - The milliseconds to wait before giving up
776
+ * @returns A promise that resolves on the native exit or when the deadline elapses, whichever comes first
777
+ *
778
+ * @example
779
+ * ```ts
780
+ * await waitForExit(child, 5_000)
781
+ * ```
782
+ */
783
+ export declare function waitForExit(child: Pick<ProcessChild, 'exitCode' | 'signalCode' | 'once'>, timeout: number): Promise<void>;
378
784
 
379
785
  export { }