@osdlc/command-seam 0.0.0-stage → 0.4.0

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 +315 -0
  2. package/CONTRIBUTING.md +61 -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
package/dist/result.js ADDED
@@ -0,0 +1,219 @@
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
+ /**
10
+ * Whether the process started and exited 0. `exitCode` alone is not enough:
11
+ * a spawn failure, timeout or abort reports a stand-in `1`, and a caller that
12
+ * reads exit codes as answers must rule that out first.
13
+ *
14
+ * Takes any runner's result: a `CommandResult`, a `StreamResult` or a
15
+ * `ManagedProcessExit`.
16
+ *
17
+ * @returns `true` when `neverExited` is not set and `exitCode` is 0.
18
+ */
19
+ export function ran(result) {
20
+ return result.neverExited !== true && result.exitCode === 0;
21
+ }
22
+ /**
23
+ * The child's trimmed stderr, else the runner's trimmed `error` (a failed
24
+ * spawn, a timeout, an abort), else `''`. Use it where a failure message reads
25
+ * stderr: since the runner's message is only in `error`, reading `stderr`
26
+ * alone shows nothing when the command never ran.
27
+ *
28
+ * @param result - A run's result.
29
+ * @returns The text, trimmed; `''` when both are empty.
30
+ * @experimental
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * const r = spawnRunner('git').run(['fetch'], { cwd: '.' })
35
+ * if (!ran(r)) console.error(`fetch failed: ${stderrOrError(r)}`)
36
+ * ```
37
+ */
38
+ export function stderrOrError(result) {
39
+ return result.stderr.trim() || (result.error?.trim() ?? '');
40
+ }
41
+ /**
42
+ * The failure text to show a person. By default it picks one stream, which
43
+ * suits a one-line message: trimmed stderr, else the runner's trimmed
44
+ * `error` (a failed spawn, a timeout), else trimmed stdout (some commands
45
+ * explain a failure there), else `exit N`. With `both: true` it
46
+ * joins both streams (see {@link FailureTextOptions.both}).
47
+ *
48
+ * @param result - The result of a failed run.
49
+ * @param opts - `both: true` to join both streams.
50
+ * @returns The message. Non-empty unless `both` is set and there is no text
51
+ * at all.
52
+ */
53
+ export function failureText(result, opts = {}) {
54
+ const stderr = stderrOrError(result);
55
+ const stdout = result.stdout.trim();
56
+ if (opts.both === true)
57
+ return [stderr, stdout].filter(Boolean).join('\n');
58
+ return stderr || stdout || `exit ${result.exitCode}`;
59
+ }
60
+ /**
61
+ * The first non-blank line of `text`, trimmed; `fallback` when there is
62
+ * none. For short diagnostics from multi-line tool output.
63
+ *
64
+ * @param text - Usually `stdout` or `stderr`.
65
+ * @param fallback - Returned when every line is blank. Defaults to `''`.
66
+ * @experimental
67
+ */
68
+ export function firstLine(text, fallback = '') {
69
+ return (text
70
+ .split('\n')
71
+ .map((l) => l.trim())
72
+ .find((l) => l !== '') ?? fallback);
73
+ }
74
+ /**
75
+ * Parse a finished run's stdout as JSON, if it ran and exited with a code in
76
+ * `okExitCodes` (default `[0]`). Without the `parse` option, `T` is the
77
+ * caller's claim about the shape and is not checked; pass `parse` when the
78
+ * producer is not trusted. Never throws.
79
+ *
80
+ * @param result - A finished run.
81
+ * @param opts - Parse options.
82
+ * @returns `{ ok: true, value }`, or `{ ok: false, reason, message }` with
83
+ * `reason` `failed` (did not run, or exited with a code not accepted),
84
+ * `unparseable`, or `invalid` (`parse` threw; `cause` holds what it threw).
85
+ * @experimental
86
+ */
87
+ export function parseJsonResult(result, opts = {}) {
88
+ const okExitCodes = opts.okExitCodes ?? [0];
89
+ if (result.neverExited === true || !okExitCodes.includes(result.exitCode)) {
90
+ return { ok: false, reason: 'failed', message: failureText(result), result };
91
+ }
92
+ const parsed = tryParse(result.stdout, opts.tolerateLeadingLog === true);
93
+ if (!parsed.ok) {
94
+ return {
95
+ ok: false,
96
+ reason: 'unparseable',
97
+ message: `stdout is not JSON: ${parsed.error}`,
98
+ result,
99
+ };
100
+ }
101
+ if (opts.parse === undefined)
102
+ return { ok: true, value: parsed.value, result };
103
+ let value;
104
+ try {
105
+ value = opts.parse(parsed.value);
106
+ }
107
+ catch (e) {
108
+ const why = e instanceof Error ? e.message : String(e);
109
+ return {
110
+ ok: false,
111
+ reason: 'invalid',
112
+ message: `stdout JSON was rejected: ${why}`,
113
+ cause: e,
114
+ result,
115
+ };
116
+ }
117
+ if (isThenable(value)) {
118
+ // An async `parse` (such as `parseAsync`): its verdict would arrive too
119
+ // late, so say so, and keep its rejection from going unhandled.
120
+ value.then(undefined, () => { });
121
+ return {
122
+ ok: false,
123
+ reason: 'invalid',
124
+ message: 'stdout JSON was rejected: parse returned a promise; it must be sync',
125
+ cause: value,
126
+ result,
127
+ };
128
+ }
129
+ return { ok: true, value, result };
130
+ }
131
+ function isThenable(v) {
132
+ return ((typeof v === 'object' || typeof v === 'function') &&
133
+ v !== null &&
134
+ typeof v.then === 'function');
135
+ }
136
+ function tryParse(stdout, tolerateLeadingLog) {
137
+ try {
138
+ return { ok: true, value: JSON.parse(stdout) };
139
+ }
140
+ catch (e) {
141
+ const start = tolerateLeadingLog ? jsonStart(stdout) : -1;
142
+ if (start > 0) {
143
+ try {
144
+ return { ok: true, value: JSON.parse(stdout.slice(start)) };
145
+ }
146
+ catch {
147
+ // Report the whole-stdout error below.
148
+ }
149
+ }
150
+ return { ok: false, error: e.message };
151
+ }
152
+ }
153
+ /**
154
+ * Where the JSON starts for `tolerateLeadingLog`: the line starting with `{` or
155
+ * `[` whose brackets stay open until the last bracket in stdout closes them.
156
+ * Only that line's suffix can parse, so stdout is parsed once more at most.
157
+ * A JSON string holds no raw line break, so string state resets at each one.
158
+ * `-1` when no line qualifies.
159
+ */
160
+ function jsonStart(stdout) {
161
+ // The bracket depth after each bracket outside a string, in order.
162
+ const depths = [];
163
+ // Each candidate line: where it starts, its first bracket's index in
164
+ // `depths`, and the depth before that bracket.
165
+ const lines = [];
166
+ let depth = 0;
167
+ let inString = false;
168
+ for (let i = 0; i < stdout.length; i++) {
169
+ const c = stdout[i];
170
+ if (c === '\n' || c === '\r') {
171
+ inString = false;
172
+ }
173
+ else if (inString) {
174
+ if (c === '"')
175
+ inString = false;
176
+ else if (c === '\\' && stdout[i + 1] !== '\n' && stdout[i + 1] !== '\r')
177
+ i++;
178
+ }
179
+ else if (c === '"') {
180
+ inString = true;
181
+ }
182
+ else if (c === '{' || c === '[') {
183
+ const prev = stdout[i - 1];
184
+ if (prev === '\n' || prev === '\r')
185
+ lines.push({ at: i, first: depths.length, before: depth });
186
+ depths.push(++depth);
187
+ }
188
+ else if (c === '}' || c === ']') {
189
+ depths.push(--depth);
190
+ }
191
+ }
192
+ const end = depths.at(-1);
193
+ // The lowest depth from each bracket on, leaving out the last bracket.
194
+ let low = Infinity;
195
+ let next = depths.length - 2;
196
+ for (let k = lines.length - 1; k >= 0; k--) {
197
+ const { at, first, before } = lines[k];
198
+ for (; next >= first; next--)
199
+ low = Math.min(low, depths[next]);
200
+ if (before === end && low > before)
201
+ return at;
202
+ }
203
+ return -1;
204
+ }
205
+ export function runJson(runner, argv, ...opts) {
206
+ const { run, json } = splitJsonOptions(opts);
207
+ return parseJsonResult(runner.run(argv, ...run), json);
208
+ }
209
+ export async function runJsonAsync(runner, argv, ...opts) {
210
+ const { run, json } = splitJsonOptions(opts);
211
+ return parseJsonResult(await runner.run(argv, ...run), json);
212
+ }
213
+ /** Split `runJson`'s options into the runner's and the parse step's. */
214
+ function splitJsonOptions(opts) {
215
+ if (opts[0] === undefined)
216
+ return { run: [], json: {} };
217
+ const { tolerateLeadingLog, parse, okExitCodes, ...run } = opts[0];
218
+ return { run: [run], json: { tolerateLeadingLog, parse, okExitCodes } };
219
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The shell runner: the one place in this package that runs a shell string.
3
+ *
4
+ * Every other runner passes argv straight to a binary, so nothing is parsed
5
+ * as shell. A configured command such as `bun install --filter '@x/y'` or
6
+ * `a && b` is a shell string by nature, and it runs as `sh -c -- <verb>`.
7
+ * Keeping that call in one place makes shell use easy to find and audit, and
8
+ * keeps the invocation the same everywhere.
9
+ *
10
+ * Built on the capture runners, so it uses their types
11
+ * ({@link CommandResult}, {@link CommandRunOptions}, `CommandFailed`) and
12
+ * their timeout, env and stdin handling.
13
+ */
14
+ import { type AsyncCommandRunner, type AsyncCommandRunOptions } from './async.ts';
15
+ import { type CommandResult, type CommandRunner, type CommandRunOptions } from './sync.ts';
16
+ /**
17
+ * Positional parameters for the verb. They ride as separate argv entries,
18
+ * never spliced into the string, so they are inert data.
19
+ */
20
+ export interface ShellPositionals {
21
+ /** Sets `$0`, the name the script sees for itself, used in its error messages. Default `sh`. */
22
+ scriptName?: string | undefined;
23
+ /** Visible to the script as `$1`, `$2`, … */
24
+ args?: readonly string[] | undefined;
25
+ }
26
+ /** {@link CommandRunOptions} plus {@link ShellPositionals}. */
27
+ export interface ShellRunOptions extends CommandRunOptions, ShellPositionals {
28
+ }
29
+ /** {@link ShellPositionals} plus the async runner's options. */
30
+ export interface AsyncShellRunOptions extends AsyncCommandRunOptions, ShellPositionals {
31
+ }
32
+ /** Runs a shell string. `verb` is interpreted by `sh`, so quote data with `shellQuote`. */
33
+ export interface ShellRunner {
34
+ run(verb: string, opts: ShellRunOptions): CommandResult;
35
+ }
36
+ /**
37
+ * The async version of {@link ShellRunner}.
38
+ *
39
+ * @experimental
40
+ */
41
+ export interface AsyncShellRunner {
42
+ run(verb: string, opts: AsyncShellRunOptions): Promise<CommandResult>;
43
+ }
44
+ /**
45
+ * A {@link ShellRunner} over `sh`. Pass `sh` to run through a different
46
+ * {@link CommandRunner}, such as a test fake. An invalid `timeoutMs` throws
47
+ * before `sh` is called, as it does on the capture runners.
48
+ *
49
+ * @param sh - The runner that receives `['-c', '--', verb]`, then
50
+ * `scriptName` and `args` when given. Defaults to `spawnRunner('sh')`.
51
+ */
52
+ export declare function shellRunner(sh?: CommandRunner): ShellRunner;
53
+ /**
54
+ * The async version of {@link shellRunner}. Wrap a sync fake with `asAsync`
55
+ * to pass it in. An invalid `timeoutMs` rejects before `sh` is called.
56
+ *
57
+ * @param sh - The runner that receives `['-c', '--', verb]`, then
58
+ * `scriptName` and `args` when given. Defaults to `spawnRunnerAsync('sh')`.
59
+ * @experimental
60
+ */
61
+ export declare function shellRunnerAsync(sh?: AsyncCommandRunner): AsyncShellRunner;
62
+ /**
63
+ * Run `verb` once through `sh -c --` and capture the result. Needs `sh` on
64
+ * `PATH`. Never throws for a failed command, like `spawnRunner`; programmer
65
+ * errors such as an invalid `timeoutMs` throw.
66
+ *
67
+ * @param verb - A shell string. Quote any data in it with `shellQuote`, or
68
+ * pass it through `args`.
69
+ * @param opts - The capture options plus `scriptName` (`$0`) and `args` (`$1`…).
70
+ * @returns The captured result.
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * const r = runShell('ls "$1" | wc -l', { cwd: '.', args: ['my dir'] })
75
+ * console.log(r.stdout.trim())
76
+ * ```
77
+ */
78
+ export declare function runShell(verb: string, opts: ShellRunOptions): CommandResult;
79
+ /**
80
+ * The async version of {@link runShell}. Never rejects for a failed command;
81
+ * rejects for programmer errors such as an invalid `timeoutMs`.
82
+ */
83
+ export declare function runShellAsync(verb: string, opts: AsyncShellRunOptions): Promise<CommandResult>;
84
+ //# sourceMappingURL=shell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../shell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAoB,KAAK,kBAAkB,EAAE,KAAK,sBAAsB,EAAE,MAAM,YAAY,CAAA;AACnG,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACvB,MAAM,WAAW,CAAA;AAElB;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,gGAAgG;IAChG,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC/B,6CAA6C;IAC7C,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAA;CACrC;AAED,+DAA+D;AAC/D,MAAM,WAAW,eAAgB,SAAQ,iBAAiB,EAAE,gBAAgB;CAAG;AAE/E,gEAAgE;AAChE,MAAM,WAAW,oBAAqB,SAAQ,sBAAsB,EAAE,gBAAgB;CAAG;AAEzF,2FAA2F;AAC3F,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,aAAa,CAAA;CACxD;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;CACtE;AAgBD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,EAAE,GAAE,aAAiC,GAAG,WAAW,CAO9E;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,EAAE,GAAE,kBAA2C,GAC9C,gBAAgB,CAOlB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,aAAa,CAE3E;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC,CAE9F"}
package/dist/shell.js ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The shell runner: the one place in this package that runs a shell string.
3
+ *
4
+ * Every other runner passes argv straight to a binary, so nothing is parsed
5
+ * as shell. A configured command such as `bun install --filter '@x/y'` or
6
+ * `a && b` is a shell string by nature, and it runs as `sh -c -- <verb>`.
7
+ * Keeping that call in one place makes shell use easy to find and audit, and
8
+ * keeps the invocation the same everywhere.
9
+ *
10
+ * Built on the capture runners, so it uses their types
11
+ * ({@link CommandResult}, {@link CommandRunOptions}, `CommandFailed`) and
12
+ * their timeout, env and stdin handling.
13
+ */
14
+ import { spawnRunnerAsync } from "./async.js";
15
+ import { normalizeTimeout, spawnRunner, } from "./sync.js";
16
+ /**
17
+ * The `sh -c -- <verb>` argv; `$0`/`$1…` are appended only when given. The
18
+ * `--` keeps a verb that starts with `-` or `+` from being read as an option.
19
+ */
20
+ function shellArgv(verb, scriptName, args) {
21
+ const rest = args ?? [];
22
+ if (scriptName === undefined && rest.length === 0)
23
+ return ['-c', '--', verb];
24
+ return ['-c', '--', verb, scriptName ?? 'sh', ...rest];
25
+ }
26
+ /**
27
+ * A {@link ShellRunner} over `sh`. Pass `sh` to run through a different
28
+ * {@link CommandRunner}, such as a test fake. An invalid `timeoutMs` throws
29
+ * before `sh` is called, as it does on the capture runners.
30
+ *
31
+ * @param sh - The runner that receives `['-c', '--', verb]`, then
32
+ * `scriptName` and `args` when given. Defaults to `spawnRunner('sh')`.
33
+ */
34
+ export function shellRunner(sh = spawnRunner('sh')) {
35
+ return {
36
+ run: (verb, { scriptName, args, ...opts }) => {
37
+ normalizeTimeout(opts.timeoutMs);
38
+ return sh.run(shellArgv(verb, scriptName, args), opts);
39
+ },
40
+ };
41
+ }
42
+ /**
43
+ * The async version of {@link shellRunner}. Wrap a sync fake with `asAsync`
44
+ * to pass it in. An invalid `timeoutMs` rejects before `sh` is called.
45
+ *
46
+ * @param sh - The runner that receives `['-c', '--', verb]`, then
47
+ * `scriptName` and `args` when given. Defaults to `spawnRunnerAsync('sh')`.
48
+ * @experimental
49
+ */
50
+ export function shellRunnerAsync(sh = spawnRunnerAsync('sh')) {
51
+ return {
52
+ run: async (verb, { scriptName, args, ...opts }) => {
53
+ normalizeTimeout(opts.timeoutMs);
54
+ return sh.run(shellArgv(verb, scriptName, args), opts);
55
+ },
56
+ };
57
+ }
58
+ /**
59
+ * Run `verb` once through `sh -c --` and capture the result. Needs `sh` on
60
+ * `PATH`. Never throws for a failed command, like `spawnRunner`; programmer
61
+ * errors such as an invalid `timeoutMs` throw.
62
+ *
63
+ * @param verb - A shell string. Quote any data in it with `shellQuote`, or
64
+ * pass it through `args`.
65
+ * @param opts - The capture options plus `scriptName` (`$0`) and `args` (`$1`…).
66
+ * @returns The captured result.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * const r = runShell('ls "$1" | wc -l', { cwd: '.', args: ['my dir'] })
71
+ * console.log(r.stdout.trim())
72
+ * ```
73
+ */
74
+ export function runShell(verb, opts) {
75
+ return shellRunner().run(verb, opts);
76
+ }
77
+ /**
78
+ * The async version of {@link runShell}. Never rejects for a failed command;
79
+ * rejects for programmer errors such as an invalid `timeoutMs`.
80
+ */
81
+ export function runShellAsync(verb, opts) {
82
+ return shellRunnerAsync().run(verb, opts);
83
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The streaming runner: the {@link StreamRunner} interface and its
3
+ * `spawnSync`-backed default, {@link streamRunner}.
4
+ *
5
+ * Use it for a command whose output a person must see live: a build, a test
6
+ * run, or an interactive program. The capture runners would hold all output
7
+ * until the child exited. Options are {@link CommandRunOptions} without
8
+ * `input`, since stdin is inherited, not piped. Code takes a `StreamRunner`
9
+ * the same way it takes a `CommandRunner`, and tests pass a fake. Mixed stdio
10
+ * (say, a child's stdout sent to the parent's stderr) is out of scope; call
11
+ * `child_process` directly for that.
12
+ *
13
+ * There is no async version: `run` blocks until the child exits, as
14
+ * `spawnRunner` does.
15
+ */
16
+ import { type CommandRunOptions, type NeverExitedReason } from './sync.ts';
17
+ /**
18
+ * Per-call options for a {@link StreamRunner}. Same as {@link
19
+ * CommandRunOptions} minus `input`: stdin is inherited from the parent
20
+ * process, not fed programmatically, so there is nothing to write to.
21
+ */
22
+ export type StreamRunOptions = Omit<CommandRunOptions, 'input'>;
23
+ /**
24
+ * Result of a streamed run. There is no output to report, since it went
25
+ * straight to the inherited stdio; only how the child ended.
26
+ */
27
+ export interface StreamResult {
28
+ /** The child's exit code, or `1` when it never reported one (see `neverExited`). */
29
+ exitCode: number;
30
+ /**
31
+ * The child never reported an exit status: the spawn failed, the timeout
32
+ * killed it, or a signal took it down. Same meaning as
33
+ * `CommandResult.neverExited`. A message is also written to the parent's
34
+ * stderr.
35
+ */
36
+ neverExited?: boolean | undefined;
37
+ /**
38
+ * Why the child never reported an exit status: `spawn`, `timeout` or
39
+ * `signal` (a streamed run cannot be aborted, and its output is not
40
+ * captured, so there is no cap). Set whenever `neverExited`
41
+ * is, absent otherwise.
42
+ */
43
+ reason?: Exclude<NeverExitedReason, 'abort' | 'output'> | undefined;
44
+ /**
45
+ * The signal that ended the child, for `reason: 'signal'` and
46
+ * `reason: 'timeout'` (`SIGKILL`).
47
+ */
48
+ signal?: NodeJS.Signals | undefined;
49
+ /**
50
+ * The runner's error message when the spawn failed or the timeout fired,
51
+ * such as `spawnSync npm ENOENT`. The same text is written to the parent's
52
+ * stderr.
53
+ */
54
+ error?: string | undefined;
55
+ }
56
+ /** Runs a binary with inherited stdio instead of capturing it. */
57
+ export interface StreamRunner {
58
+ /**
59
+ * Run the binary with `argv` (without the binary name) and wait for it to
60
+ * exit. Never throws for a failed command. Programmer errors throw: an
61
+ * invalid `timeoutMs` (see `CommandRunOptions.timeoutMs`) or a NUL byte in
62
+ * argv or env.
63
+ */
64
+ run(argv: string[], opts: StreamRunOptions): StreamResult;
65
+ }
66
+ /**
67
+ * The default {@link StreamRunner}: runs `binary` with `spawnSync` and
68
+ * `stdio: 'inherit'`, so the child's prompts, progress and output reach the
69
+ * terminal directly. It behaves like a direct `spawnSync` call, but code
70
+ * takes it as a parameter, so tests can pass a fake. On timeout the direct
71
+ * child gets `SIGKILL`. Processes it started keep running: this runner has
72
+ * no `processGroup` option.
73
+ *
74
+ * When the spawn fails, the timeout fires or a signal ends the child, a
75
+ * message is written to the parent's stderr, since there is no captured
76
+ * stderr to carry it. The result says why in `reason`, and carries the spawn
77
+ * or timeout message in `error`.
78
+ *
79
+ * @param binary - A name looked up on `PATH`, or a path.
80
+ * @returns A runner for that binary.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * const npm = streamRunner('npm')
85
+ * const { exitCode } = npm.run(['run', 'build'], { cwd: '.' })
86
+ * process.exitCode = exitCode
87
+ * ```
88
+ */
89
+ export declare function streamRunner(binary: string): StreamRunner;
90
+ //# sourceMappingURL=stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,EAKL,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACvB,MAAM,WAAW,CAAA;AAElB;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAA;AAE/D;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,oFAAoF;IACpF,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IACjC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC,iBAAiB,EAAE,OAAO,GAAG,QAAQ,CAAC,GAAG,SAAS,CAAA;IACnE;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,OAAO,GAAG,SAAS,CAAA;IACnC;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC3B;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,gBAAgB,GAAG,YAAY,CAAA;CAC1D;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CA+BzD"}
package/dist/stream.js ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The streaming runner: the {@link StreamRunner} interface and its
3
+ * `spawnSync`-backed default, {@link streamRunner}.
4
+ *
5
+ * Use it for a command whose output a person must see live: a build, a test
6
+ * run, or an interactive program. The capture runners would hold all output
7
+ * until the child exited. Options are {@link CommandRunOptions} without
8
+ * `input`, since stdin is inherited, not piped. Code takes a `StreamRunner`
9
+ * the same way it takes a `CommandRunner`, and tests pass a fake. Mixed stdio
10
+ * (say, a child's stdout sent to the parent's stderr) is out of scope; call
11
+ * `child_process` directly for that.
12
+ *
13
+ * There is no async version: `run` blocks until the child exits, as
14
+ * `spawnRunner` does.
15
+ */
16
+ import { spawnSync } from 'node:child_process';
17
+ import { cwdError, mergedEnv, normalizeTimeout, syncStop, } from "./sync.js";
18
+ /**
19
+ * The default {@link StreamRunner}: runs `binary` with `spawnSync` and
20
+ * `stdio: 'inherit'`, so the child's prompts, progress and output reach the
21
+ * terminal directly. It behaves like a direct `spawnSync` call, but code
22
+ * takes it as a parameter, so tests can pass a fake. On timeout the direct
23
+ * child gets `SIGKILL`. Processes it started keep running: this runner has
24
+ * no `processGroup` option.
25
+ *
26
+ * When the spawn fails, the timeout fires or a signal ends the child, a
27
+ * message is written to the parent's stderr, since there is no captured
28
+ * stderr to carry it. The result says why in `reason`, and carries the spawn
29
+ * or timeout message in `error`.
30
+ *
31
+ * @param binary - A name looked up on `PATH`, or a path.
32
+ * @returns A runner for that binary.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * const npm = streamRunner('npm')
37
+ * const { exitCode } = npm.run(['run', 'build'], { cwd: '.' })
38
+ * process.exitCode = exitCode
39
+ * ```
40
+ */
41
+ export function streamRunner(binary) {
42
+ return {
43
+ run(argv, opts) {
44
+ const timeout = normalizeTimeout(opts.timeoutMs);
45
+ const cwdMessage = cwdError(`spawnSync ${binary}`, binary, argv, opts);
46
+ if (cwdMessage !== undefined) {
47
+ process.stderr.write(`${cwdMessage}\n`);
48
+ return { exitCode: 1, neverExited: true, reason: 'spawn', error: cwdMessage };
49
+ }
50
+ const proc = spawnSync(binary, argv, {
51
+ cwd: opts.cwd,
52
+ stdio: 'inherit',
53
+ timeout,
54
+ // See `sync.ts`'s `runViaChildRedirect`: the default SIGTERM is not a
55
+ // hard cap.
56
+ killSignal: 'SIGKILL',
57
+ ...mergedEnv(opts),
58
+ });
59
+ // `spawnSync` reports a stop as a missing `status` (null, or undefined
60
+ // under Bun for a failed spawn), with `error` and `signal` saying why.
61
+ if (proc.status != null)
62
+ return { exitCode: proc.status };
63
+ // Output is inherited, not buffered, so the output cap cannot apply.
64
+ const stop = syncStop(proc);
65
+ if (proc.error !== undefined) {
66
+ process.stderr.write(`${proc.error.message}\n`);
67
+ return { exitCode: 1, neverExited: true, ...stop, error: proc.error.message };
68
+ }
69
+ process.stderr.write(`${binary} was killed by ${proc.signal}\n`);
70
+ return { exitCode: 1, neverExited: true, ...stop };
71
+ },
72
+ };
73
+ }