@osdlc/command-seam 0.0.0-stage → 0.0.1-stage

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +328 -0
  2. package/CONTRIBUTING.md +58 -0
  3. package/LICENSE +21 -0
  4. package/README.md +741 -2
  5. package/SECURITY.md +23 -0
  6. package/async.ts +466 -0
  7. package/defaults.ts +83 -0
  8. package/dist/async.d.ts +146 -0
  9. package/dist/async.d.ts.map +1 -0
  10. package/dist/async.js +368 -0
  11. package/dist/defaults.d.ts +52 -0
  12. package/dist/defaults.d.ts.map +1 -0
  13. package/dist/defaults.js +51 -0
  14. package/dist/groups.d.ts +155 -0
  15. package/dist/groups.d.ts.map +1 -0
  16. package/dist/groups.js +331 -0
  17. package/dist/index.d.ts +30 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +29 -0
  20. package/dist/process.d.ts +146 -0
  21. package/dist/process.d.ts.map +1 -0
  22. package/dist/process.js +175 -0
  23. package/dist/process.test.d.ts +1 -0
  24. package/dist/process.test.d.ts.map +1 -0
  25. package/dist/process.test.js +1 -0
  26. package/dist/quote.d.ts +18 -0
  27. package/dist/quote.d.ts.map +1 -0
  28. package/dist/quote.js +28 -0
  29. package/dist/result.d.ts +211 -0
  30. package/dist/result.d.ts.map +1 -0
  31. package/dist/result.js +219 -0
  32. package/dist/shell.d.ts +84 -0
  33. package/dist/shell.d.ts.map +1 -0
  34. package/dist/shell.js +83 -0
  35. package/dist/stream.d.ts +90 -0
  36. package/dist/stream.d.ts.map +1 -0
  37. package/dist/stream.js +73 -0
  38. package/dist/sync.d.ts +311 -0
  39. package/dist/sync.d.ts.map +1 -0
  40. package/dist/sync.js +406 -0
  41. package/dist/testing.d.ts +158 -0
  42. package/dist/testing.d.ts.map +1 -0
  43. package/dist/testing.js +173 -0
  44. package/groups.ts +360 -0
  45. package/index.ts +98 -0
  46. package/package.json +94 -3
  47. package/process.ts +270 -0
  48. package/quote.ts +31 -0
  49. package/result.ts +403 -0
  50. package/shell.ts +131 -0
  51. package/smoke.mjs +61 -0
  52. package/stream.ts +134 -0
  53. package/sync.ts +604 -0
  54. package/testing.ts +263 -0
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The managed-process runner: start a long-running child and get a handle to
3
+ * it, for a process the caller watches and stops rather than waits on.
4
+ *
5
+ * Every other runner in this package waits for the child to exit
6
+ * (synchronously or through a promise) and reports how it went. This one
7
+ * returns as soon as the child exists, so the caller can do other work while
8
+ * it runs: enforce its own timeout, respond to signals, stop it later.
9
+ */
10
+ import { type EnvOverrides, type NeverExitedReason } from './sync.ts';
11
+ /** Per-call options for a {@link ManagedProcessRunner}. */
12
+ export interface ManagedProcessRunOptions {
13
+ /** Working directory for the child. */
14
+ cwd: string;
15
+ /**
16
+ * Variables merged over the parent's environment. An `undefined` value
17
+ * removes that variable from the child's environment.
18
+ */
19
+ env?: EnvOverrides | undefined;
20
+ /**
21
+ * Run the child as its own process-group leader. {@link
22
+ * ManagedProcessHandle.kill} then signals the whole group instead of just
23
+ * the direct child, so processes the child started die with it. The group
24
+ * no longer gets the terminal's Ctrl-C. POSIX only.
25
+ * `installGroupSignalForwarding` kills live groups when the host is
26
+ * signalled, and once it is stopping the host, a new group run is refused
27
+ * (`reason: 'abort'`, pid `-1`).
28
+ */
29
+ processGroup?: boolean | undefined;
30
+ /**
31
+ * Let the parent exit while the child runs: the child's handle stops
32
+ * holding the parent's event loop open. Meant for a `processGroup` child
33
+ * that is supposed to outlive the call that started it (a loop tick that
34
+ * starts a run and returns); {@link ManagedProcessHandle.exited} then only
35
+ * settles while the parent is still alive. An `unref`'d group is not tracked
36
+ * by `installGroupSignalForwarding`: a signal to the parent does not reach it,
37
+ * and the parent's exit does not `SIGKILL` it.
38
+ */
39
+ unref?: boolean | undefined;
40
+ }
41
+ /** How a managed process ended. */
42
+ export interface ManagedProcessExit {
43
+ /** The child's exit code, or `1` when it never reported one (see `neverExited`). */
44
+ exitCode: number;
45
+ /** The signal that ended the child, or `null` when it exited on its own or never started. */
46
+ signal: NodeJS.Signals | null;
47
+ /**
48
+ * No exit code was reported: the spawn failed (e.g. a missing binary) or a
49
+ * signal killed the process (see `signal`). Same meaning as
50
+ * `CommandResult.neverExited`.
51
+ */
52
+ neverExited?: boolean | undefined;
53
+ /**
54
+ * Why no exit code was reported. Set whenever `neverExited` is, absent
55
+ * otherwise. `spawn`: the process never started (see `error`). `abort`: a
56
+ * signal ended it after the caller's own `kill()`, or a `processGroup` run
57
+ * was refused because signal forwarding is stopping the host (it never
58
+ * started; see `error`). `signal`: a signal from
59
+ * elsewhere ended it. There is no `timeout` or `output`: this runner has
60
+ * neither a timeout nor captured output.
61
+ */
62
+ reason?: Exclude<NeverExitedReason, 'timeout' | 'output'> | undefined;
63
+ /**
64
+ * The spawn error's message, such as `spawn node ENOENT`, when `reason` is
65
+ * `spawn`, or the refusal message for a refused `processGroup` run.
66
+ */
67
+ error?: string | undefined;
68
+ }
69
+ /** A running child: its pid, a way to kill it, and a promise settling once it's gone. */
70
+ export interface ManagedProcessHandle {
71
+ /** The child's pid, or `-1` when it could not be spawned at all. */
72
+ readonly pid: number;
73
+ /**
74
+ * Send `signal` (default `SIGTERM`) to the child, or to its whole process
75
+ * group when started with `processGroup: true`. Does nothing when `pid` is
76
+ * `-1`. Without `processGroup`, it also does nothing once the child has
77
+ * exited, so a reused pid is never signalled. With `processGroup`, it still
78
+ * signals any member of the group that is left. If the process dies of a
79
+ * signal sent here, `exited` reports `reason: 'abort'`. A signal the child
80
+ * handles and survives does not count: a later death by another signal is
81
+ * `'signal'`.
82
+ */
83
+ kill(signal?: NodeJS.Signals | number): void;
84
+ /** Settles once the child is gone, including when the spawn failed. Never rejects. */
85
+ readonly exited: Promise<ManagedProcessExit>;
86
+ }
87
+ /** Starts a long-running process and returns a handle instead of waiting for it. */
88
+ export interface ManagedProcessRunner {
89
+ /**
90
+ * Start the binary with `argv` (without the binary name) and return at
91
+ * once. A failed spawn is reported through `exited`, not thrown. A NUL
92
+ * byte in argv or env is a programmer error and throws.
93
+ */
94
+ run(argv: string[], opts: ManagedProcessRunOptions): ManagedProcessHandle;
95
+ }
96
+ /**
97
+ * The default {@link ManagedProcessRunner}: starts `binary` with `spawn`.
98
+ * Stdio is always inherited, so the child's output reaches the terminal. It
99
+ * does not capture output and has no timeout of its own.
100
+ *
101
+ * @param binary - A name looked up on `PATH`, or a path.
102
+ * @returns A runner for that binary.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * const server = managedProcessRunner('node').run(['server.js'], {
107
+ * cwd: '.',
108
+ * processGroup: true,
109
+ * })
110
+ * setTimeout(() => server.kill(), 5_000)
111
+ * const { exitCode, signal } = await server.exited
112
+ * ```
113
+ */
114
+ export declare function managedProcessRunner(binary: string): ManagedProcessRunner;
115
+ /**
116
+ * Stop a managed process: send `SIGTERM`, wait up to `graceMs` for it to
117
+ * exit, then send `SIGKILL`. A plain `kill()` sends one signal, so a process
118
+ * that ignores `SIGTERM` would leave `await handle.exited` waiting forever.
119
+ *
120
+ * Works with any {@link ManagedProcessHandle}, fakes included. It waits for
121
+ * the handle's own process only: with `processGroup`, a group member that
122
+ * outlives the leader is not sent `SIGKILL`.
123
+ *
124
+ * @param handle - The process to stop.
125
+ * @param opts - `graceMs`: how long to wait after `SIGTERM`. Default `5_000`.
126
+ * `0` sends `SIGKILL` right after `SIGTERM`; `Infinity` never sends it. A
127
+ * value above 2^31 - 1, the largest timer delay, is clamped to it. A
128
+ * negative value or `NaN` rejects with `RangeError`, a non-number with
129
+ * `TypeError`.
130
+ * @returns How the process ended. `reason` is `'abort'` when one of the two
131
+ * signals ended it.
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * const server = managedProcessRunner('node').run(['server.js'], { cwd: '.' })
136
+ * const exit = await stopProcess(server, { graceMs: 2_000 })
137
+ * ```
138
+ *
139
+ * @experimental
140
+ */
141
+ export declare function stopProcess(handle: ManagedProcessHandle, opts?: {
142
+ graceMs?: number | undefined;
143
+ }): Promise<ManagedProcessExit>;
144
+ /** The name of signal number `n`, or `undefined` for 0 (a probe) and unknown numbers. */
145
+ export declare function signalName(n: number): NodeJS.Signals | undefined;
146
+ //# sourceMappingURL=process.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../process.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,EAAgC,KAAK,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAEnG,2DAA2D;AAC3D,MAAM,WAAW,wBAAwB;IACvC,uCAAuC;IACvC,GAAG,EAAE,MAAM,CAAA;IACX;;;OAGG;IACH,GAAG,CAAC,EAAE,YAAY,GAAG,SAAS,CAAA;IAC9B;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAClC;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC5B;AAED,mCAAmC;AACnC,MAAM,WAAW,kBAAkB;IACjC,oFAAoF;IACpF,QAAQ,EAAE,MAAM,CAAA;IAChB,6FAA6F;IAC7F,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAA;IAC7B;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IACjC;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC,iBAAiB,EAAE,SAAS,GAAG,QAAQ,CAAC,GAAG,SAAS,CAAA;IACrE;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC3B;AAED,yFAAyF;AACzF,MAAM,WAAW,oBAAoB;IACnC,oEAAoE;IACpE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB;;;;;;;;;OASG;IACH,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,OAAO,GAAG,MAAM,GAAG,IAAI,CAAA;IAC5C,sFAAsF;IACtF,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAA;CAC7C;AAED,oFAAoF;AACpF,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,wBAAwB,GAAG,oBAAoB,CAAA;CAC1E;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,oBAAoB,CAoFzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,oBAAoB,EAC5B,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAO,GAC1C,OAAO,CAAC,kBAAkB,CAAC,CAa7B;AAYD,yFAAyF;AACzF,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,OAAO,GAAG,SAAS,CAKhE"}
@@ -0,0 +1,175 @@
1
+ /**
2
+ * The managed-process runner: start a long-running child and get a handle to
3
+ * it, for a process the caller watches and stops rather than waits on.
4
+ *
5
+ * Every other runner in this package waits for the child to exit
6
+ * (synchronously or through a promise) and reports how it went. This one
7
+ * returns as soon as the child exists, so the caller can do other work while
8
+ * it runs: enforce its own timeout, respond to signals, stop it later.
9
+ */
10
+ import { spawn } from 'node:child_process';
11
+ import { constants } from 'node:os';
12
+ import { groupStartRefusal, releaseWhenEmpty, settleGroup, spawnTrackedGroup } from "./groups.js";
13
+ import { cwdError, mergedEnv, timerMs } from "./sync.js";
14
+ /**
15
+ * The default {@link ManagedProcessRunner}: starts `binary` with `spawn`.
16
+ * Stdio is always inherited, so the child's output reaches the terminal. It
17
+ * does not capture output and has no timeout of its own.
18
+ *
19
+ * @param binary - A name looked up on `PATH`, or a path.
20
+ * @returns A runner for that binary.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const server = managedProcessRunner('node').run(['server.js'], {
25
+ * cwd: '.',
26
+ * processGroup: true,
27
+ * })
28
+ * setTimeout(() => server.kill(), 5_000)
29
+ * const { exitCode, signal } = await server.exited
30
+ * ```
31
+ */
32
+ export function managedProcessRunner(binary) {
33
+ return {
34
+ run(argv, opts) {
35
+ const cwdMessage = cwdError(`spawn ${binary}`, binary, argv, opts);
36
+ if (cwdMessage !== undefined) {
37
+ return { pid: -1, kill: () => { }, exited: Promise.resolve(spawnExit(cwdMessage)) };
38
+ }
39
+ const group = opts.processGroup === true;
40
+ const refusal = group ? groupStartRefusal() : undefined;
41
+ if (refusal !== undefined) {
42
+ return { pid: -1, kill: () => { }, exited: Promise.resolve(refusedExit(refusal)) };
43
+ }
44
+ const start = () => spawn(binary, argv, {
45
+ cwd: opts.cwd,
46
+ detached: group,
47
+ stdio: 'inherit',
48
+ ...mergedEnv(opts),
49
+ });
50
+ // An `unref`'d group is meant to outlive the host, so forwarding must not reap it.
51
+ const { child, untrack: untrackGroup } = group && opts.unref !== true
52
+ ? spawnTrackedGroup(start)
53
+ : { child: start(), untrack: undefined };
54
+ const pid = child.pid ?? -1;
55
+ if (opts.unref === true)
56
+ child.unref();
57
+ // The signals `kill()` has sent. A death by one of them is the caller's
58
+ // own stop; a death by any other signal came from elsewhere.
59
+ const sent = new Set();
60
+ const exited = new Promise((resolve) => {
61
+ let settled = false;
62
+ const finish = (result) => {
63
+ if (settled)
64
+ return;
65
+ settled = true;
66
+ resolve(result);
67
+ };
68
+ // A spawn failure (e.g. ENOENT): no exit was ever reported.
69
+ child.on('error', (err) => {
70
+ untrackGroup?.();
71
+ finish(spawnExit(err.message));
72
+ });
73
+ child.on('exit', (code, signal) => {
74
+ // The leader is gone but the group may not be: stay registered until
75
+ // no member is left, so a signal to the host still reaches the rest.
76
+ // Unless `kill()` is stopping it, the host's exit leaves it running.
77
+ if (untrackGroup !== undefined) {
78
+ if (sent.size === 0)
79
+ settleGroup(pid);
80
+ releaseWhenEmpty(pid, untrackGroup);
81
+ }
82
+ finish({
83
+ exitCode: code ?? 1,
84
+ signal,
85
+ ...(code === null
86
+ ? {
87
+ neverExited: true,
88
+ reason: signal !== null && sent.has(signal) ? 'abort' : 'signal',
89
+ }
90
+ : {}),
91
+ });
92
+ });
93
+ });
94
+ return {
95
+ pid,
96
+ kill(signal = 'SIGTERM') {
97
+ if (pid === -1)
98
+ return;
99
+ try {
100
+ // A negative pid signals the whole process group — see
101
+ // `processGroup`'s doc above. The direct child goes through
102
+ // `child.kill`, which does nothing once the child has been
103
+ // reaped, so a reused pid is never signalled.
104
+ const delivered = group ? process.kill(-pid, signal) : child.kill(signal);
105
+ const name = typeof signal === 'number' ? signalName(signal) : signal;
106
+ if (delivered && name !== undefined)
107
+ sent.add(name);
108
+ }
109
+ catch {
110
+ // Already gone — nothing to reap.
111
+ }
112
+ },
113
+ exited,
114
+ };
115
+ },
116
+ };
117
+ }
118
+ /**
119
+ * Stop a managed process: send `SIGTERM`, wait up to `graceMs` for it to
120
+ * exit, then send `SIGKILL`. A plain `kill()` sends one signal, so a process
121
+ * that ignores `SIGTERM` would leave `await handle.exited` waiting forever.
122
+ *
123
+ * Works with any {@link ManagedProcessHandle}, fakes included. It waits for
124
+ * the handle's own process only: with `processGroup`, a group member that
125
+ * outlives the leader is not sent `SIGKILL`.
126
+ *
127
+ * @param handle - The process to stop.
128
+ * @param opts - `graceMs`: how long to wait after `SIGTERM`. Default `5_000`.
129
+ * `0` sends `SIGKILL` right after `SIGTERM`; `Infinity` never sends it. A
130
+ * value above 2^31 - 1, the largest timer delay, is clamped to it. A
131
+ * negative value or `NaN` rejects with `RangeError`, a non-number with
132
+ * `TypeError`.
133
+ * @returns How the process ended. `reason` is `'abort'` when one of the two
134
+ * signals ended it.
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * const server = managedProcessRunner('node').run(['server.js'], { cwd: '.' })
139
+ * const exit = await stopProcess(server, { graceMs: 2_000 })
140
+ * ```
141
+ *
142
+ * @experimental
143
+ */
144
+ export async function stopProcess(handle, opts = {}) {
145
+ const graceMs = timerMs('graceMs', opts.graceMs ?? 5_000, 'zero');
146
+ handle.kill();
147
+ if (graceMs === undefined)
148
+ return handle.exited;
149
+ let timer;
150
+ const grace = new Promise((resolve) => {
151
+ timer = setTimeout(resolve, graceMs, undefined);
152
+ });
153
+ const exit = await Promise.race([handle.exited, grace]);
154
+ clearTimeout(timer);
155
+ if (exit !== undefined)
156
+ return exit;
157
+ handle.kill('SIGKILL');
158
+ return handle.exited;
159
+ }
160
+ /** The exit of a `processGroup` run refused because signal forwarding is stopping the host. */
161
+ function refusedExit(error) {
162
+ return { exitCode: 1, signal: null, neverExited: true, reason: 'abort', error };
163
+ }
164
+ /** The exit of a process that never started. */
165
+ function spawnExit(error) {
166
+ return { exitCode: 1, signal: null, neverExited: true, reason: 'spawn', error };
167
+ }
168
+ /** The name of signal number `n`, or `undefined` for 0 (a probe) and unknown numbers. */
169
+ export function signalName(n) {
170
+ for (const [name, value] of Object.entries(constants.signals)) {
171
+ if (value === n)
172
+ return name;
173
+ }
174
+ return undefined;
175
+ }
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=process.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process.test.d.ts","sourceRoot":"","sources":["../process.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1,18 @@
1
+ /**
2
+ * POSIX shell quoting, for a command line printed for a person to paste or
3
+ * passed to `sh -c`. The argv runners never need it: they pass argv straight
4
+ * to the binary.
5
+ */
6
+ /**
7
+ * `word` as one shell word: unquoted when it holds only characters no POSIX
8
+ * shell treats specially (letters, digits and `_@%+=:,./-`), single-quoted
9
+ * otherwise.
10
+ */
11
+ export declare function shellQuote(word: string): string;
12
+ /**
13
+ * `argv` as one shell command line. A first word containing `=` is always
14
+ * quoted: bare, the shell would read it as a variable assignment, not a
15
+ * command.
16
+ */
17
+ export declare function shellJoin(argv: readonly string[]): string;
18
+ //# sourceMappingURL=quote.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quote.d.ts","sourceRoot":"","sources":["../quote.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG/C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAEzD"}
package/dist/quote.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * POSIX shell quoting, for a command line printed for a person to paste or
3
+ * passed to `sh -c`. The argv runners never need it: they pass argv straight
4
+ * to the binary.
5
+ */
6
+ /** Characters no POSIX shell treats specially. Same set as Python's `shlex.quote`. */
7
+ const INERT = /^[A-Za-z0-9_@%+=:,./-]+$/;
8
+ /**
9
+ * `word` as one shell word: unquoted when it holds only characters no POSIX
10
+ * shell treats specially (letters, digits and `_@%+=:,./-`), single-quoted
11
+ * otherwise.
12
+ */
13
+ export function shellQuote(word) {
14
+ if (INERT.test(word))
15
+ return word;
16
+ return quoted(word);
17
+ }
18
+ /**
19
+ * `argv` as one shell command line. A first word containing `=` is always
20
+ * quoted: bare, the shell would read it as a variable assignment, not a
21
+ * command.
22
+ */
23
+ export function shellJoin(argv) {
24
+ return argv.map((w, i) => (i === 0 && w.includes('=') ? quoted(w) : shellQuote(w))).join(' ');
25
+ }
26
+ function quoted(word) {
27
+ return `'${word.replaceAll("'", `'\\''`)}'`;
28
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Helpers for reading a {@link CommandResult}: did it run and succeed, what
3
+ * to tell a person, and its stdout as JSON.
4
+ *
5
+ * None of them throw for a failed command: a failure stays a value the
6
+ * caller reads. Use `runChecked` when stopping on failure is the only
7
+ * sensible response.
8
+ */
9
+ import type { AsyncCommandRunner, AsyncCommandRunOptions } from './async.ts';
10
+ import type { CommandResult, CommandRunner, CommandRunOptions } from './sync.ts';
11
+ /**
12
+ * Whether the process started and exited 0. `exitCode` alone is not enough:
13
+ * a spawn failure, timeout or abort reports a stand-in `1`, and a caller that
14
+ * reads exit codes as answers must rule that out first.
15
+ *
16
+ * Takes any runner's result: a `CommandResult`, a `StreamResult` or a
17
+ * `ManagedProcessExit`.
18
+ *
19
+ * @returns `true` when `neverExited` is not set and `exitCode` is 0.
20
+ */
21
+ export declare function ran(result: {
22
+ exitCode: number;
23
+ neverExited?: boolean | undefined;
24
+ }): boolean;
25
+ /**
26
+ * The child's trimmed stderr, else the runner's trimmed `error` (a failed
27
+ * spawn, a timeout, an abort), else `''`. Use it where a failure message reads
28
+ * stderr: since the runner's message is only in `error`, reading `stderr`
29
+ * alone shows nothing when the command never ran.
30
+ *
31
+ * @param result - A run's result.
32
+ * @returns The text, trimmed; `''` when both are empty.
33
+ * @experimental
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const r = spawnRunner('git').run(['fetch'], { cwd: '.' })
38
+ * if (!ran(r)) console.error(`fetch failed: ${stderrOrError(r)}`)
39
+ * ```
40
+ */
41
+ export declare function stderrOrError(result: Pick<CommandResult, 'stderr' | 'error'>): string;
42
+ /** Options for {@link failureText}. */
43
+ export interface FailureTextOptions {
44
+ /**
45
+ * Return both streams instead of one: trimmed stderr (or the runner's
46
+ * `error`, when the child wrote no stderr) and trimmed stdout,
47
+ * newline-joined, empty ones dropped. Empty when stderr, `error` and stdout
48
+ * are all empty; there is no `exit N` fallback. This is what `CommandFailed.detail`
49
+ * returns. Use it when both halves matter, such as a git hook that rejects
50
+ * a commit: git's own error is on stderr, the hook's reason on stdout.
51
+ */
52
+ both?: boolean | undefined;
53
+ }
54
+ /**
55
+ * The failure text to show a person. By default it picks one stream, which
56
+ * suits a one-line message: trimmed stderr, else the runner's trimmed
57
+ * `error` (a failed spawn, a timeout), else trimmed stdout (some commands
58
+ * explain a failure there), else `exit N`. With `both: true` it
59
+ * joins both streams (see {@link FailureTextOptions.both}).
60
+ *
61
+ * @param result - The result of a failed run.
62
+ * @param opts - `both: true` to join both streams.
63
+ * @returns The message. Non-empty unless `both` is set and there is no text
64
+ * at all.
65
+ */
66
+ export declare function failureText(result: Pick<CommandResult, 'exitCode' | 'stdout' | 'stderr' | 'error'>, opts?: FailureTextOptions): string;
67
+ /**
68
+ * The first non-blank line of `text`, trimmed; `fallback` when there is
69
+ * none. For short diagnostics from multi-line tool output.
70
+ *
71
+ * @param text - Usually `stdout` or `stderr`.
72
+ * @param fallback - Returned when every line is blank. Defaults to `''`.
73
+ * @experimental
74
+ */
75
+ export declare function firstLine(text: string, fallback?: string): string;
76
+ /**
77
+ * Outcome of {@link runJson}: a parsed value, or why there is none. Two
78
+ * fields are named `reason`: `j.reason` says why there is no value (`failed`,
79
+ * `unparseable` or `invalid`), and `j.result.reason` says why the command
80
+ * never finished, when it did not (see `NeverExitedReason`).
81
+ *
82
+ * @experimental
83
+ */
84
+ export type JsonRun<T> = {
85
+ ok: true;
86
+ value: T;
87
+ result: CommandResult;
88
+ } | {
89
+ ok: false;
90
+ /**
91
+ * `failed`: did not run, or exited with a code not in `okExitCodes`.
92
+ * `unparseable`: exited with an accepted code, and stdout was not JSON.
93
+ */
94
+ reason: 'failed' | 'unparseable';
95
+ /** A human-readable explanation (the failure text, or the parse error). */
96
+ message: string;
97
+ result: CommandResult;
98
+ } | {
99
+ ok: false;
100
+ /** `invalid`: stdout was JSON, and the `parse` option threw for it. */
101
+ reason: 'invalid';
102
+ /** `stdout JSON was rejected: ` and the thrown error's message. */
103
+ message: string;
104
+ /** What `parse` threw, such as a schema library's error with its issue list. */
105
+ cause: unknown;
106
+ result: CommandResult;
107
+ };
108
+ /**
109
+ * Options for {@link runJson}, apart from `parse`, which is typed by the
110
+ * value it returns. A value typed `RunJsonOptions` leaves `T` to the caller.
111
+ *
112
+ * @experimental
113
+ */
114
+ export interface RunJsonOptions {
115
+ /**
116
+ * Skip leading non-JSON output (log lines, banners). The JSON is the rest of
117
+ * stdout from the first line (after `\n` or `\r`) that starts with `{` or
118
+ * `[` and parses to the end, so a `[INFO] ...` prefix is skipped too.
119
+ * Trailing output is not handled. Off by default: strict `JSON.parse` of
120
+ * the whole stdout.
121
+ */
122
+ tolerateLeadingLog?: boolean | undefined;
123
+ /**
124
+ * Exit codes that count as an answer, for a tool that prints JSON and
125
+ * exits non-zero on purpose (`npm outdated --json` exits 1 when anything is
126
+ * outdated). Default `[0]`. List `0` too if it still counts. A run that
127
+ * never exited is `failed` whatever its stand-in code. Output from an
128
+ * accepted non-zero exit must still parse, so pass a `parse` that rejects
129
+ * the tool's own error output.
130
+ */
131
+ okExitCodes?: readonly number[] | undefined;
132
+ }
133
+ /** The `parse` option of `runJson` and its siblings. `T` is what it returns. */
134
+ interface ParseOption<T> {
135
+ /**
136
+ * Check the parsed value and return it typed, such as a schema's `parse`.
137
+ * A throw makes the outcome `invalid`. It must be sync: a returned promise
138
+ * is also `invalid`. Without it, `T` is the caller's claim and is not
139
+ * checked.
140
+ */
141
+ parse?: ((value: unknown) => T) | undefined;
142
+ }
143
+ /** Options with an optional `parse`. */
144
+ type JsonOptions<T> = RunJsonOptions & ParseOption<T>;
145
+ /**
146
+ * Parse a finished run's stdout as JSON, if it ran and exited with a code in
147
+ * `okExitCodes` (default `[0]`). Without the `parse` option, `T` is the
148
+ * caller's claim about the shape and is not checked; pass `parse` when the
149
+ * producer is not trusted. Never throws.
150
+ *
151
+ * @param result - A finished run.
152
+ * @param opts - Parse options.
153
+ * @returns `{ ok: true, value }`, or `{ ok: false, reason, message }` with
154
+ * `reason` `failed` (did not run, or exited with a code not accepted),
155
+ * `unparseable`, or `invalid` (`parse` threw; `cause` holds what it threw).
156
+ * @experimental
157
+ */
158
+ export declare function parseJsonResult<T = unknown>(result: CommandResult, opts?: JsonOptions<T>): JsonRun<T>;
159
+ /**
160
+ * The runner's options tuple, `[opts]` or `[opts?]`, with the `runJson`
161
+ * options added. A conditional rather than a mapped type, so `T` can be inferred
162
+ * from `parse`; `NoInfer` keeps `P` inferred from the runner alone.
163
+ */
164
+ type WithJsonOptions<P extends unknown[], T> = P extends [opts: infer O] ? [opts: NoInfer<O> & JsonOptions<T>] : P extends [opts?: infer O] ? [opts?: NoInfer<O> & JsonOptions<T>] : never;
165
+ /**
166
+ * Run a command with a sync runner and parse its stdout as JSON
167
+ * ({@link parseJsonResult}). Never throws for a failed command; a programmer
168
+ * error, such as an invalid `timeoutMs`, throws. With the `parse` option,
169
+ * `T` is what `parse` returns.
170
+ *
171
+ * Takes the runner's own options, so a `withDefaults` runner needs only the
172
+ * options it did not bind.
173
+ *
174
+ * @param runner - Runs the command; pass a fake in tests.
175
+ * @param argv - Arguments, without the binary name.
176
+ * @param opts - Per-call options and {@link RunJsonOptions}.
177
+ * @returns A {@link JsonRun}: the parsed value, or why there is none.
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * const gh = spawnRunner('gh')
182
+ * const r = runJson<{ login: string }>(gh, ['api', 'user'], { cwd: '.' })
183
+ * if (r.ok) console.log(r.value.login)
184
+ * else console.error(r.message)
185
+ * ```
186
+ *
187
+ * @experimental
188
+ */
189
+ export declare function runJson<T = unknown>(runner: CommandRunner, argv: string[], opts: CommandRunOptions & JsonOptions<T>): JsonRun<T>;
190
+ export declare function runJson<T = unknown>(runner: {
191
+ run(argv: string[]): CommandResult;
192
+ }, argv: string[], opts?: Partial<CommandRunOptions> & JsonOptions<T>): JsonRun<T>;
193
+ export declare function runJson<T = unknown, P extends unknown[] = [opts: CommandRunOptions]>(runner: {
194
+ run(argv: string[], ...opts: P): CommandResult;
195
+ }, argv: string[], ...opts: WithJsonOptions<P, T>): JsonRun<T>;
196
+ /**
197
+ * The async version of {@link runJson}. Never rejects for a failed command;
198
+ * a programmer error, such as an invalid `timeoutMs`, rejects. `parse` is
199
+ * sync, as in `runJson`.
200
+ *
201
+ * @experimental
202
+ */
203
+ export declare function runJsonAsync<T = unknown>(runner: AsyncCommandRunner, argv: string[], opts: AsyncCommandRunOptions & JsonOptions<T>): Promise<JsonRun<T>>;
204
+ export declare function runJsonAsync<T = unknown>(runner: {
205
+ run(argv: string[]): Promise<CommandResult>;
206
+ }, argv: string[], opts?: Partial<AsyncCommandRunOptions> & JsonOptions<T>): Promise<JsonRun<T>>;
207
+ export declare function runJsonAsync<T = unknown, P extends unknown[] = [opts: AsyncCommandRunOptions]>(runner: {
208
+ run(argv: string[], ...opts: P): Promise<CommandResult>;
209
+ }, argv: string[], ...opts: WithJsonOptions<P, T>): Promise<JsonRun<T>>;
210
+ export {};
211
+ //# sourceMappingURL=result.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../result.ts"],"names":[],"mappings":"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"}