@orkestrel/process 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,351 @@
1
+ import { EmitterErrorHandler } from '@orkestrel/emitter';
2
+ import { EmitterHooks } from '@orkestrel/emitter';
3
+ import { EmitterInterface } from '@orkestrel/emitter';
4
+
5
+ /**
6
+ * Creates the failure raised when a manager launch reuses a live id.
7
+ *
8
+ * @param id - The id already occupied by a live child
9
+ * @returns A typed duplicate-id failure
10
+ */
11
+ export declare function createDuplicateError(id: string): ProcessError;
12
+
13
+ /**
14
+ * Creates the failure raised when a run does not complete successfully and rejection is requested.
15
+ *
16
+ * @param result - The buffered run outcome that failed
17
+ * @returns A typed run failure carrying its {@link RunResult}
18
+ */
19
+ export declare function createRunError(result: RunResult): ProcessError;
20
+
21
+ /**
22
+ * Checks whether an unknown value is a {@link ProcessError}.
23
+ *
24
+ * @param value - The value to inspect
25
+ * @returns True only for a `ProcessError` instance; false otherwise
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * isProcessError(new ProcessError('spawn failed', { code: 'spawn' })) // true
30
+ * isProcessError(new Error('spawn failed')) // false
31
+ * ```
32
+ */
33
+ export declare function isProcessError(value: unknown): value is ProcessError;
34
+
35
+ /** The default maximum retained stderr tail in bytes for a supervised {@link ProcessInterface}. */
36
+ export declare const PROCESS_EVIDENCE = 2048;
37
+
38
+ /** The default cooperative window in milliseconds between `SIGTERM` and `SIGKILL` during termination. */
39
+ export declare const PROCESS_GRACE = 5000;
40
+
41
+ /** The default maximum captured bytes for a one-shot run's stdout and stderr, each. */
42
+ export declare const PROCESS_OUTPUT = 10485760;
43
+
44
+ /**
45
+ * The public contracts for `@orkestrel/process`: a typed child-process toolkit.
46
+ *
47
+ * @remarks
48
+ * Three tiers, smallest to largest:
49
+ *
50
+ * - **{@link ProcessInterface}** — one supervised child with eagerly framed stdout lines, a
51
+ * byte-bounded stderr tail, a live stderr event, a writable stdin channel, a typed lifecycle
52
+ * {@link ProcessEventMap} emitter, and bounded `SIGTERM` → grace → `SIGKILL` termination. The
53
+ * low-level streaming primitive.
54
+ * - **{@link RunResult} / run** — a one-shot runner that buffers a child to completion and settles
55
+ * with its output and exit, the ergonomic layer for fire-and-collect commands.
56
+ * - **{@link ProcessManagerInterface}** — a keyed registry of live {@link ProcessInterface}
57
+ * children, launched and stopped by id, observed through its own {@link ProcessManagerEventMap}.
58
+ *
59
+ * Every contract here is host-independent (`AbortSignal`, `AsyncIterable`, `Promise`, and plain
60
+ * `string` signal names). The Node implementations live in `@orkestrel/process/server`.
61
+ */
62
+ /**
63
+ * One spawnable command: the executable, its argument vector, and optional environment overrides
64
+ * and initial standard input.
65
+ *
66
+ * @remarks
67
+ * `environment` merges over the parent process environment; an `undefined` value unsets a key.
68
+ * `input` is written to the child's stdin immediately after spawn.
69
+ */
70
+ export declare interface ProcessCommand {
71
+ readonly file: string;
72
+ readonly arguments: readonly string[];
73
+ readonly environment?: Readonly<Record<string, string | undefined>>;
74
+ readonly input?: string;
75
+ }
76
+
77
+ /** A child-process failure with a stable machine-readable category. */
78
+ export declare class ProcessError extends Error {
79
+ readonly name = "ProcessError";
80
+ readonly code: ProcessErrorCode;
81
+ readonly context?: ProcessErrorContext;
82
+ /** The buffered run outcome, present when a one-shot run produced the failure. */
83
+ readonly result?: RunResult;
84
+ /**
85
+ * Create a process error.
86
+ *
87
+ * @param message - Human-readable failure description
88
+ * @param options - Machine-readable category, optional context, optional cause, and optional run result
89
+ */
90
+ constructor(message: string, options: ProcessErrorOptions);
91
+ }
92
+
93
+ /** The machine-readable {@link ProcessError} categories. */
94
+ export declare type ProcessErrorCode = 'spawn' | 'timeout' | 'duplicate' | 'protocol';
95
+
96
+ /** Structured context carried by a {@link ProcessError}. */
97
+ export declare interface ProcessErrorContext {
98
+ /** The registry id involved, for a manager failure. */
99
+ readonly id?: string;
100
+ /** The command line involved, for a run or spawn failure. */
101
+ readonly command?: string;
102
+ /** The child's exit code, when one was observed. */
103
+ readonly code?: number | null;
104
+ /** The terminating signal, when one ended the child. */
105
+ readonly signal?: string | null;
106
+ /** The rejected public input, for an input-validation failure. */
107
+ readonly value?: unknown;
108
+ }
109
+
110
+ /** Construction options for a {@link ProcessError}. */
111
+ export declare interface ProcessErrorOptions {
112
+ /** The stable machine-readable category. */
113
+ readonly code: ProcessErrorCode;
114
+ /** Structured detail about the failure. */
115
+ readonly context?: ProcessErrorContext;
116
+ /** The underlying cause, when one exists. */
117
+ readonly cause?: unknown;
118
+ /** The buffered run outcome, present when a {@link RunResult} produced the failure. */
119
+ readonly result?: RunResult;
120
+ }
121
+
122
+ /**
123
+ * The push observation surface of a {@link ProcessInterface} — the moments a fire-and-forget
124
+ * observer subscribes to, alongside the `lines` stream and the `exit` promise.
125
+ *
126
+ * @remarks
127
+ * Declared as a `type` alias so it satisfies the emitter's `EventMap` constraint structurally. A
128
+ * listener throw is isolated by the emitter and routed to its `error` handler, never onto this map.
129
+ */
130
+ export declare type ProcessEventMap = {
131
+ /** A decoded standard-error chunk arrived. */
132
+ readonly stderr: readonly [chunk: string];
133
+ /** The child settled — its terminal state, delivered once. */
134
+ readonly exit: readonly [exit: ProcessExit];
135
+ };
136
+
137
+ /** The observed terminal state of a child process: its exit code, or the signal that ended it. */
138
+ export declare interface ProcessExit {
139
+ /** The exit code, or `null` when a signal ended the process. */
140
+ readonly code: number | null;
141
+ /** The terminating signal name, or `null` when the process exited on its own. */
142
+ readonly signal: string | null;
143
+ }
144
+
145
+ /**
146
+ * One supervised child process with eagerly framed output and bounded termination.
147
+ *
148
+ * @remarks
149
+ * `lines` is pumped eagerly: stdout is drained whether or not a consumer iterates, so `exit`
150
+ * resolves and a late consumer still receives every line. `evidence` is the decoded, byte-bounded
151
+ * stderr tail — the diagnostic to attach to a failed exit. The typed `emitter` carries the live
152
+ * `stderr` chunks and the terminal `exit`, alongside the `exit` promise. `send` never throws; it
153
+ * returns whether the line reached an open channel. `stop` is idempotent.
154
+ */
155
+ export declare interface ProcessInterface {
156
+ /** The typed lifecycle observation surface. */
157
+ readonly emitter: EmitterInterface<ProcessEventMap>;
158
+ /** The eagerly captured stdout lines, in arrival order, ending when the child's stdout closes. */
159
+ readonly lines: AsyncIterable<string>;
160
+ /** The decoded byte-bounded stderr tail. */
161
+ readonly evidence: string;
162
+ /** The terminal child state, observed once from the close event. */
163
+ readonly exit: Promise<ProcessExit>;
164
+ /**
165
+ * Write one line to the open standard-input channel.
166
+ *
167
+ * @param text - The line text without its trailing newline
168
+ * @returns True when the channel accepted the line without backpressure; false otherwise
169
+ */
170
+ send(text: string): boolean;
171
+ /**
172
+ * Terminate the child process tree and await its observed exit.
173
+ *
174
+ * @returns A promise that resolves after bounded termination
175
+ */
176
+ stop(): Promise<void>;
177
+ /**
178
+ * Stop the child and destroy the observation emitter.
179
+ *
180
+ * @returns The stable barrier shared by every call
181
+ */
182
+ destroy(): Promise<void>;
183
+ }
184
+
185
+ /**
186
+ * The push observation surface of a {@link ProcessManagerInterface} — the fleet-level moments a
187
+ * fire-and-forget observer subscribes to.
188
+ *
189
+ * @remarks
190
+ * Declared as a `type` alias so it satisfies the emitter's `EventMap` constraint structurally.
191
+ */
192
+ export declare type ProcessManagerEventMap = {
193
+ /** A child was launched under its id. */
194
+ readonly launch: readonly [id: string];
195
+ /** A child settled and left the registry — its id and terminal state. */
196
+ readonly exit: readonly [id: string, exit: ProcessExit];
197
+ };
198
+
199
+ /**
200
+ * A keyed registry of live supervised child processes.
201
+ *
202
+ * @remarks
203
+ * A child that settles removes itself from the registry, so `count` and `processes` reflect only
204
+ * live children, and its departure emits `exit`. `launch` throws a {@link ProcessError} coded
205
+ * `duplicate` when the id is already live — spawn faults surface through the returned child's
206
+ * `exit`, not from `launch`. The typed `emitter` carries the `launch` and `exit` moments across
207
+ * every child.
208
+ */
209
+ export declare interface ProcessManagerInterface {
210
+ /** The typed fleet-level observation surface. */
211
+ readonly emitter: EmitterInterface<ProcessManagerEventMap>;
212
+ /** The number of live children. */
213
+ readonly count: number;
214
+ /**
215
+ * The live child under `id`, or `undefined` when none is.
216
+ *
217
+ * @param id - The registry key
218
+ * @returns The child, or `undefined`
219
+ */
220
+ process(id: string): ProcessInterface | undefined;
221
+ /**
222
+ * A snapshot of every live child.
223
+ *
224
+ * @returns The live children in launch order
225
+ */
226
+ processes(): readonly ProcessInterface[];
227
+ /**
228
+ * Spawn and register one child under `id`.
229
+ *
230
+ * @param id - The registry key, unique among live children
231
+ * @param options - The child construction options
232
+ * @returns The launched child
233
+ * @throws A {@link ProcessError} coded `duplicate` when `id` is already live
234
+ */
235
+ launch(id: string, options: ProcessOptions): ProcessInterface;
236
+ /**
237
+ * Terminate the named children and await their exit.
238
+ *
239
+ * @param ids - The registry keys to stop
240
+ * @returns True when every named child stopped, false when any id was not live
241
+ */
242
+ stop(ids: readonly string[]): Promise<boolean>;
243
+ /**
244
+ * Terminate one child and await its exit.
245
+ *
246
+ * @param id - The registry key to stop
247
+ * @returns True when the child was live and stopped, false when the id was not live
248
+ */
249
+ stop(id: string): Promise<boolean>;
250
+ /**
251
+ * Terminate every live child and await their exit.
252
+ *
253
+ * @returns A promise that resolves after all children stop
254
+ */
255
+ stop(): Promise<void>;
256
+ /**
257
+ * Stop every child, then destroy the registry emitter last.
258
+ *
259
+ * @returns The stable barrier shared by every call
260
+ */
261
+ destroy(): Promise<void>;
262
+ }
263
+
264
+ /**
265
+ * Construction options for a {@link ProcessManagerInterface}.
266
+ *
267
+ * @remarks
268
+ * `on` installs initial {@link ProcessManagerEventMap} listeners and `error` receives isolated
269
+ * listener failures.
270
+ */
271
+ export declare interface ProcessManagerOptions {
272
+ readonly on?: EmitterHooks<ProcessManagerEventMap>;
273
+ readonly error?: EmitterErrorHandler;
274
+ }
275
+
276
+ /**
277
+ * Construction options for one supervised child process.
278
+ *
279
+ * @remarks
280
+ * `grace` is the sole timing knob: after `stop`, the child is sent `SIGTERM`, then `SIGKILL` if it
281
+ * has not exited within `grace` milliseconds. There is no completion deadline — a caller that wants
282
+ * one arms its own timer and calls `stop`. `on` installs initial {@link ProcessEventMap} listeners
283
+ * and `error` receives isolated listener failures.
284
+ */
285
+ export declare interface ProcessOptions {
286
+ readonly on?: EmitterHooks<ProcessEventMap>;
287
+ readonly error?: EmitterErrorHandler;
288
+ readonly command: ProcessCommand;
289
+ /** The working directory the child runs in. */
290
+ readonly workspace: string;
291
+ /** Cooperative termination window in milliseconds between `SIGTERM` and `SIGKILL`. */
292
+ readonly grace: number;
293
+ /** Maximum retained stderr tail in bytes. Default: {@link PROCESS_EVIDENCE}. */
294
+ readonly evidence?: number;
295
+ /** If `true`, stdin stays open for {@link ProcessInterface.send}; if `false` or omitted, stdin closes after any initial `input`. */
296
+ readonly writable?: boolean;
297
+ /** Aborting this signal terminates the child through the same bounded `stop`. */
298
+ readonly signal?: AbortSignal;
299
+ }
300
+
301
+ /**
302
+ * Options for a one-shot run.
303
+ *
304
+ * @remarks
305
+ * A run is a fire-and-collect function, not a lifecycle entity, so it carries no emitter. `reject`
306
+ * follows the command-runner convention: by default a non-zero exit rejects with a
307
+ * {@link ProcessError} carrying the {@link RunResult}; `reject: false` resolves with the result
308
+ * instead, so the caller inspects `failed`.
309
+ */
310
+ export declare interface RunOptions {
311
+ /** The working directory. Default: the current working directory. */
312
+ readonly workspace?: string;
313
+ /** Environment overrides merged over the parent environment; an `undefined` value unsets a key. */
314
+ readonly environment?: Readonly<Record<string, string | undefined>>;
315
+ /** Standard input written to the child. */
316
+ readonly input?: string;
317
+ /** Milliseconds before the child is terminated. `0` or omitted disables the timeout. */
318
+ readonly timeout?: number;
319
+ /** Cooperative window between `SIGTERM` and `SIGKILL` when terminating. Default: {@link PROCESS_GRACE}. */
320
+ readonly grace?: number;
321
+ /** Aborting this signal terminates the run. */
322
+ readonly signal?: AbortSignal;
323
+ /** If `false`, resolve with the result on failure instead of rejecting. Default: `true`. */
324
+ readonly reject?: boolean;
325
+ /** Maximum captured bytes for stdout and for stderr, each. Default: {@link PROCESS_OUTPUT}. */
326
+ readonly limit?: number;
327
+ }
328
+
329
+ /**
330
+ * The settled outcome of a one-shot run: the buffered output and the terminal state.
331
+ *
332
+ * @remarks
333
+ * `failed` is `true` when the child exited non-zero, was killed by a signal, timed out, or failed to
334
+ * spawn. `timedOut` is `true` only when the run's own `timeout` elapsed.
335
+ */
336
+ export declare interface RunResult {
337
+ /** The command line that was run, for diagnostics. */
338
+ readonly command: string;
339
+ /** The complete captured standard output, byte-bounded by `limit`. */
340
+ readonly stdout: string;
341
+ /** The complete captured standard error, byte-bounded by `limit`. */
342
+ readonly stderr: string;
343
+ readonly code: number | null;
344
+ readonly signal: string | null;
345
+ /** True if the run did not complete successfully. */
346
+ readonly failed: boolean;
347
+ /** True if the run's `timeout` elapsed before completion. */
348
+ readonly timedOut: boolean;
349
+ }
350
+
351
+ export { }
@@ -0,0 +1,80 @@
1
+ import { holds } from "@orkestrel/contract";
2
+ //#region src/core/constants.ts
3
+ /** The default cooperative window in milliseconds between `SIGTERM` and `SIGKILL` during termination. */
4
+ var PROCESS_GRACE = 5e3;
5
+ /** The default maximum retained stderr tail in bytes for a supervised {@link ProcessInterface}. */
6
+ var PROCESS_EVIDENCE = 2048;
7
+ /** The default maximum captured bytes for a one-shot run's stdout and stderr, each. */
8
+ var PROCESS_OUTPUT = 10485760;
9
+ //#endregion
10
+ //#region src/core/errors.ts
11
+ /** A child-process failure with a stable machine-readable category. */
12
+ var ProcessError = class extends Error {
13
+ name = "ProcessError";
14
+ code;
15
+ context;
16
+ /** The buffered run outcome, present when a one-shot run produced the failure. */
17
+ result;
18
+ /**
19
+ * Create a process error.
20
+ *
21
+ * @param message - Human-readable failure description
22
+ * @param options - Machine-readable category, optional context, optional cause, and optional run result
23
+ */
24
+ constructor(message, options) {
25
+ const cause = options.cause;
26
+ super(message, cause === void 0 ? void 0 : { cause });
27
+ this.code = options.code;
28
+ if (options.context !== void 0) this.context = options.context;
29
+ if (options.result !== void 0) this.result = options.result;
30
+ }
31
+ };
32
+ /**
33
+ * Checks whether an unknown value is a {@link ProcessError}.
34
+ *
35
+ * @param value - The value to inspect
36
+ * @returns True only for a `ProcessError` instance; false otherwise
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * isProcessError(new ProcessError('spawn failed', { code: 'spawn' })) // true
41
+ * isProcessError(new Error('spawn failed')) // false
42
+ * ```
43
+ */
44
+ function isProcessError(value) {
45
+ return holds(() => value instanceof ProcessError);
46
+ }
47
+ /**
48
+ * Creates the failure raised when a manager launch reuses a live id.
49
+ *
50
+ * @param id - The id already occupied by a live child
51
+ * @returns A typed duplicate-id failure
52
+ */
53
+ function createDuplicateError(id) {
54
+ return new ProcessError(`Process '${id}' is already live`, {
55
+ code: "duplicate",
56
+ context: { id }
57
+ });
58
+ }
59
+ /**
60
+ * Creates the failure raised when a run does not complete successfully and rejection is requested.
61
+ *
62
+ * @param result - The buffered run outcome that failed
63
+ * @returns A typed run failure carrying its {@link RunResult}
64
+ */
65
+ function createRunError(result) {
66
+ const reason = result.timedOut ? "timed out" : result.signal !== null ? `was killed by ${result.signal}` : `exited with code ${String(result.code)}`;
67
+ return new ProcessError(`Command '${result.command}' ${reason}`, {
68
+ code: result.timedOut ? "timeout" : "spawn",
69
+ context: {
70
+ command: result.command,
71
+ code: result.code,
72
+ signal: result.signal
73
+ },
74
+ result
75
+ });
76
+ }
77
+ //#endregion
78
+ export { PROCESS_EVIDENCE, PROCESS_GRACE, PROCESS_OUTPUT, ProcessError, createDuplicateError, createRunError, isProcessError };
79
+
80
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/core/constants.ts","../../../src/core/errors.ts"],"sourcesContent":["/** The default cooperative window in milliseconds between `SIGTERM` and `SIGKILL` during termination. */\nexport const PROCESS_GRACE = 5_000\n\n/** The default maximum retained stderr tail in bytes for a supervised {@link ProcessInterface}. */\nexport const PROCESS_EVIDENCE = 2_048\n\n/** The default maximum captured bytes for a one-shot run's stdout and stderr, each. */\nexport const PROCESS_OUTPUT = 10_485_760\n","import { holds } from '@orkestrel/contract'\nimport type {\n\tProcessErrorCode,\n\tProcessErrorContext,\n\tProcessErrorOptions,\n\tRunResult,\n} from './types.js'\n\n/** A child-process failure with a stable machine-readable category. */\nexport class ProcessError extends Error {\n\toverride readonly name = 'ProcessError'\n\treadonly code: ProcessErrorCode\n\treadonly context?: ProcessErrorContext\n\t/** The buffered run outcome, present when a one-shot run produced the failure. */\n\treadonly result?: RunResult\n\n\t/**\n\t * Create a process error.\n\t *\n\t * @param message - Human-readable failure description\n\t * @param options - Machine-readable category, optional context, optional cause, and optional run result\n\t */\n\tconstructor(message: string, options: ProcessErrorOptions) {\n\t\tconst cause = options.cause\n\t\tsuper(message, cause === undefined ? undefined : { cause })\n\t\tthis.code = options.code\n\t\tif (options.context !== undefined) this.context = options.context\n\t\tif (options.result !== undefined) this.result = options.result\n\t}\n}\n\n/**\n * Checks whether an unknown value is a {@link ProcessError}.\n *\n * @param value - The value to inspect\n * @returns True only for a `ProcessError` instance; false otherwise\n *\n * @example\n * ```ts\n * isProcessError(new ProcessError('spawn failed', { code: 'spawn' })) // true\n * isProcessError(new Error('spawn failed')) // false\n * ```\n */\nexport function isProcessError(value: unknown): value is ProcessError {\n\treturn holds(() => value instanceof ProcessError)\n}\n\n/**\n * Creates the failure raised when a manager launch reuses a live id.\n *\n * @param id - The id already occupied by a live child\n * @returns A typed duplicate-id failure\n */\nexport function createDuplicateError(id: string): ProcessError {\n\treturn new ProcessError(`Process '${id}' is already live`, {\n\t\tcode: 'duplicate',\n\t\tcontext: { id },\n\t})\n}\n\n/**\n * Creates the failure raised when a run does not complete successfully and rejection is requested.\n *\n * @param result - The buffered run outcome that failed\n * @returns A typed run failure carrying its {@link RunResult}\n */\nexport function createRunError(result: RunResult): ProcessError {\n\tconst reason = result.timedOut\n\t\t? 'timed out'\n\t\t: result.signal !== null\n\t\t\t? `was killed by ${result.signal}`\n\t\t\t: `exited with code ${String(result.code)}`\n\treturn new ProcessError(`Command '${result.command}' ${reason}`, {\n\t\tcode: result.timedOut ? 'timeout' : 'spawn',\n\t\tcontext: { command: result.command, code: result.code, signal: result.signal },\n\t\tresult,\n\t})\n}\n"],"mappings":";;;AACA,IAAa,gBAAgB;;AAG7B,IAAa,mBAAmB;;AAGhC,IAAa,iBAAiB;;;;ACE9B,IAAa,eAAb,cAAkC,MAAM;CACvC,OAAyB;CACzB;CACA;;CAEA;;;;;;;CAQA,YAAY,SAAiB,SAA8B;EAC1D,MAAM,QAAQ,QAAQ;EACtB,MAAM,SAAS,UAAU,KAAA,IAAY,KAAA,IAAY,EAAE,MAAM,CAAC;EAC1D,KAAK,OAAO,QAAQ;EACpB,IAAI,QAAQ,YAAY,KAAA,GAAW,KAAK,UAAU,QAAQ;EAC1D,IAAI,QAAQ,WAAW,KAAA,GAAW,KAAK,SAAS,QAAQ;CACzD;AACD;;;;;;;;;;;;;AAcA,SAAgB,eAAe,OAAuC;CACrE,OAAO,YAAY,iBAAiB,YAAY;AACjD;;;;;;;AAQA,SAAgB,qBAAqB,IAA0B;CAC9D,OAAO,IAAI,aAAa,YAAY,GAAG,oBAAoB;EAC1D,MAAM;EACN,SAAS,EAAE,GAAG;CACf,CAAC;AACF;;;;;;;AAQA,SAAgB,eAAe,QAAiC;CAC/D,MAAM,SAAS,OAAO,WACnB,cACA,OAAO,WAAW,OACjB,iBAAiB,OAAO,WACxB,oBAAoB,OAAO,OAAO,IAAI;CAC1C,OAAO,IAAI,aAAa,YAAY,OAAO,QAAQ,IAAI,UAAU;EAChE,MAAM,OAAO,WAAW,YAAY;EACpC,SAAS;GAAE,SAAS,OAAO;GAAS,MAAM,OAAO;GAAM,QAAQ,OAAO;EAAO;EAC7E;CACD,CAAC;AACF"}