@osdlc/command-seam 0.4.0 → 0.5.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.
- package/CHANGELOG.md +18 -0
- package/CONTRIBUTING.md +3 -2
- package/README.md +63 -11
- package/async.ts +13 -3
- package/defaults.ts +7 -1
- package/dist/async.d.ts +3 -0
- package/dist/async.d.ts.map +1 -1
- package/dist/async.js +3 -1
- package/dist/defaults.d.ts +2 -0
- package/dist/defaults.d.ts.map +1 -1
- package/dist/defaults.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/sync.d.ts +47 -8
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +30 -35
- package/dist/which.d.ts +47 -0
- package/dist/which.d.ts.map +1 -0
- package/dist/which.js +53 -0
- package/index.ts +3 -0
- package/package.json +3 -3
- package/sync.ts +63 -46
- package/which.ts +72 -0
- package/smoke.mjs +0 -61
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2f3fc7c: `CommandFailed` now names the program that failed, and the sync runner flags output it stopped reading early.
|
|
8
|
+
|
|
9
|
+
- `CommandFailed.message` includes the binary: `command failed (exit 1): git nope` instead of `command failed (exit 1): nope`. The new `binary` field holds it, and the constructor takes it as an optional third argument. `runChecked` and `runCheckedAsync` read it from the runner's new optional `binary` property. `spawnRunner`, `spawnRunnerAsync`, `asAsync` and `withDefaults` runners have it. A fake runner without `binary` keeps the old argv-only message. ([#231](https://github.com/osdlc/osdlc/issues/231))
|
|
10
|
+
- `CommandResult` has a new `outputIncomplete` flag. The sync runner sets it when the child exited but a process it started still held stdout or stderr open at `timeoutMs`. That run used to look like a success with a stray `ETIMEDOUT` in `error`. A child that passes the output cap but exits before the runner's kill lands gets the flag too, on Node or Bun. The `bun test` capture path never sets it. `exitCode` stays the child's own and `reason` stays absent, so `ran` and `runChecked` still count the run as a success. Check `outputIncomplete` when the output must be whole. ([#232](https://github.com/osdlc/osdlc/issues/232))
|
|
11
|
+
- **Breaking:** `CommandFailed.message` now starts with the binary, so code that matches on the message text sees a different string. `CommandRunner` and `AsyncCommandRunner` gain an optional public `readonly binary?: string` member, so a class that implements one of them with a private or non-string `binary` field no longer type-checks. Make the field public and a string, or rename it.
|
|
12
|
+
- 2f3fc7c: Add `which(binary, opts?)`, experimental: the absolute path a spawn of `binary` would run, or `undefined`, without running it. It uses the runners' lookup rules: a name with a `/` resolves against `cwd`, a bare name is searched on `PATH`, and a match must be an executable file. `opts` takes `cwd` and `env` (merged over the parent's, as the runners take it), typed `WhichOptions`. ([#236](https://github.com/osdlc/osdlc/issues/236))
|
|
13
|
+
|
|
14
|
+
## 0.4.1
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 4fa373f: The npm package no longer contains `smoke.mjs`. It was a check for this
|
|
19
|
+
repository's CI, not part of the API.
|
|
20
|
+
|
|
3
21
|
## 0.4.0
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
package/CONTRIBUTING.md
CHANGED
|
@@ -30,8 +30,9 @@ pnpm test
|
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
Build before testing: the examples import the package by name, and under Node
|
|
33
|
-
that name resolves to `dist/`. After a build, `pnpm run smoke` runs
|
|
34
|
-
check of the built package under Node.
|
|
33
|
+
that name resolves to `dist/`. After a build, `pnpm run smoke` runs
|
|
34
|
+
`tests/smoke.mjs`, a short check of the built package under Node. It is not
|
|
35
|
+
published.
|
|
35
36
|
|
|
36
37
|
Other checks:
|
|
37
38
|
|
package/README.md
CHANGED
|
@@ -93,7 +93,7 @@ try {
|
|
|
93
93
|
of arguments, without the binary name. The binary is fixed when you create
|
|
94
94
|
the runner.
|
|
95
95
|
- **Result**: what a capture runner returns: `exitCode`, `stdout`, `stderr`,
|
|
96
|
-
`neverExited`, `reason`, `signal` and `error`.
|
|
96
|
+
`neverExited`, `reason`, `signal`, `outputIncomplete` and `error`.
|
|
97
97
|
- **`neverExited`**: `true` when the command never reported an exit status.
|
|
98
98
|
The spawn failed, a timeout or abort killed it, a signal ended it, or the
|
|
99
99
|
sync runner killed it at the output cap.
|
|
@@ -106,6 +106,10 @@ try {
|
|
|
106
106
|
runner's 256 MB output cap). `signal` names the signal when the runner
|
|
107
107
|
knows it. A timeout may pass on a retry; a spawn failure or the output cap
|
|
108
108
|
will not.
|
|
109
|
+
- **`outputIncomplete`**: `true` when the child exited but the runner stopped
|
|
110
|
+
reading its output early, so `stdout` and `stderr` may be cut short. The
|
|
111
|
+
exit code is real and `reason` is absent. See "`error` can come with a real
|
|
112
|
+
exit code" under Behaviour.
|
|
109
113
|
- **`error`**: the runner's own message, such as a spawn error or
|
|
110
114
|
`timed out after 500ms`. It is never copied into `stderr`, which holds only
|
|
111
115
|
what the child wrote. `failureText` and `CommandFailed` read `error` when
|
|
@@ -345,6 +349,27 @@ const user = runJson<{ login: string }>(gh, ['api', 'user'], { env: { GH_HOST: '
|
|
|
345
349
|
console.log(head, user.ok && user.value.login)
|
|
346
350
|
```
|
|
347
351
|
|
|
352
|
+
## Finding a binary
|
|
353
|
+
|
|
354
|
+
`which(binary, opts?)` returns the absolute path a spawn of `binary` would
|
|
355
|
+
run, or `undefined`, without running anything. Use it to pick a tool by what
|
|
356
|
+
is installed, where probing with `--version` is slow or has side effects.
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
import { which } from '@osdlc/command-seam'
|
|
360
|
+
|
|
361
|
+
const harnesses = ['codex', 'claude', 'cursor-agent'].filter((b) => which(b) !== undefined)
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
It looks a binary up the way the runners' spawn does: a name with a `/` is
|
|
365
|
+
resolved against `cwd`, a bare name is searched on `PATH`, and a match must
|
|
366
|
+
be an executable file. `opts` takes `cwd` (default `process.cwd()`) and
|
|
367
|
+
`env`, merged over the parent's as the runners take it, so
|
|
368
|
+
`which('tool', { env: { PATH: binDir } })` checks a test's own `PATH`.
|
|
369
|
+
An empty `PATH` entry means `cwd`, as it does for Node's spawn. Bun's spawn
|
|
370
|
+
skips empty entries, so under Bun `which` can find a binary there that a
|
|
371
|
+
spawn would not.
|
|
372
|
+
|
|
348
373
|
## Reading results
|
|
349
374
|
|
|
350
375
|
Small helpers that never throw for a failed command (a programmer error,
|
|
@@ -411,14 +436,19 @@ else if (checked.reason === 'invalid') console.error(checked.message, checked.ca
|
|
|
411
436
|
|
|
412
437
|
## Reporting failures
|
|
413
438
|
|
|
414
|
-
`CommandFailed` carries `argv` and the full `result`. Its `message`
|
|
415
|
-
`command failed (exit N): <argv>`,
|
|
439
|
+
`CommandFailed` carries `binary`, `argv` and the full `result`. Its `message`
|
|
440
|
+
is `command failed (exit N): <binary> <argv>`, quoted by `shellJoin`, then the
|
|
416
441
|
stderr, or the runner's `error` when the child wrote no stderr. It never
|
|
417
442
|
includes stdout. Some commands explain a failure on stdout instead, such as a
|
|
418
443
|
git hook that rejects a commit. `e.detail` joins both
|
|
419
444
|
streams, trimmed, and drops empty ones: it is
|
|
420
445
|
`failureText(e.result, { both: true })`. Show `detail` to a person.
|
|
421
446
|
|
|
447
|
+
`runChecked` and `runCheckedAsync` take the binary from the runner's
|
|
448
|
+
`binary` property, which `spawnRunner`, `spawnRunnerAsync`, `asAsync` and
|
|
449
|
+
`withDefaults` runners have. A fake runner may leave it out; the message then
|
|
450
|
+
shows the argv alone and `e.binary` is `undefined`.
|
|
451
|
+
|
|
422
452
|
The message carries the full argv, so it reaches error logs and crash
|
|
423
453
|
reports. Do not pass a secret, such as a token in a URL, as an argument:
|
|
424
454
|
pass it through `env` or `input`.
|
|
@@ -572,6 +602,17 @@ console.log(await server.exited) // { exitCode: 0, signal: null }
|
|
|
572
602
|
the output captured so far. The sync runner also sets `signal: 'SIGKILL'`.
|
|
573
603
|
Both runners set `error`: the async runner's is `timed out after <N>ms`,
|
|
574
604
|
the sync runner's is spawnSync's `ETIMEDOUT` message.
|
|
605
|
+
- The sync capture runners wait for the output pipes to close, not only for
|
|
606
|
+
the child to exit. If the child exits but a process it started still holds
|
|
607
|
+
stdout or stderr, the run waits until `timeoutMs` (or, without one, until
|
|
608
|
+
that process exits). The result then has the child's own `exitCode`,
|
|
609
|
+
`outputIncomplete: true`, no `reason`, and spawnSync's `ETIMEDOUT` message
|
|
610
|
+
in `error`. Output written after the deadline is lost, and the process
|
|
611
|
+
keeps running. A run that may start background work belongs on an async
|
|
612
|
+
runner with `processGroup: true`. Under `bun test` the sync runner captures
|
|
613
|
+
through temp files instead of pipes (see Known issue: `bun test`), so it
|
|
614
|
+
returns as soon as the child exits: later output is lost without
|
|
615
|
+
`outputIncomplete` or `error`, and a test there never sees the flag.
|
|
575
616
|
- The sync runners (`spawnRunner`, `runShell`, `shellRunner`) and
|
|
576
617
|
`streamRunner` kill only the direct child on timeout. Processes it started
|
|
577
618
|
keep running, and these runners have no option to change that. The async
|
|
@@ -614,13 +655,21 @@ console.log(await server.exited) // { exitCode: 0, signal: null }
|
|
|
614
655
|
written, the result keeps the child's exit code. The async runner and the
|
|
615
656
|
sync runner on Node put the write error in `error`. The sync runner under
|
|
616
657
|
Bun does not report it.
|
|
617
|
-
- **`error` can come with a real exit code.**
|
|
618
|
-
|
|
619
|
-
|
|
658
|
+
- **`error` can come with a real exit code.** `exitCode` is then the child's
|
|
659
|
+
own and `reason` is absent. Three cases:
|
|
660
|
+
- a failed stdin write: only `error` says what went wrong;
|
|
661
|
+
- a sync run whose child exited while a process it started held the output
|
|
662
|
+
pipes past `timeoutMs` (see Timeouts and cancellation): `outputIncomplete: true`;
|
|
663
|
+
- a child that passes the output cap but exits before the runner's kill
|
|
664
|
+
lands, on Node or Bun: `outputIncomplete: true`.
|
|
665
|
+
|
|
666
|
+
`ran` and `runChecked` go by the exit code, so they treat the last two as
|
|
667
|
+
success. Check `outputIncomplete` when the output must be whole.
|
|
620
668
|
- **Output is text.** Captured output is decoded as UTF-8. The sync pipe
|
|
621
669
|
path caps each stream at 256 MB and usually kills a child that passes it,
|
|
622
|
-
with `reason: 'output'`.
|
|
623
|
-
keeps its exit code and gets
|
|
670
|
+
with `reason: 'output'`. A child that passes the cap but exits before the
|
|
671
|
+
kill lands keeps its exit code and gets `outputIncomplete: true` and
|
|
672
|
+
`error`. The `bun test` capture path and
|
|
624
673
|
the async runner have no cap.
|
|
625
674
|
|
|
626
675
|
## Known issue: `bun test`
|
|
@@ -657,6 +706,7 @@ The experimental exports:
|
|
|
657
706
|
- `firstLine`, `stderrOrError`
|
|
658
707
|
- `runJson`, `runJsonAsync`, `parseJsonResult`, `JsonRun`, `RunJsonOptions`
|
|
659
708
|
- `withDefaults`, `DefaultedRunner`
|
|
709
|
+
- `which`, `WhichOptions`
|
|
660
710
|
- `stopProcess`
|
|
661
711
|
- `installGroupSignalForwarding`, `GroupSignalForwardingOptions`
|
|
662
712
|
- `liveProcessGroups`, `forwardedSignal`, `forwardedExitCode`
|
|
@@ -678,9 +728,9 @@ are settled, and Windows support has landed, if that is feasible.
|
|
|
678
728
|
| `mapFailed`, `mapFailedAsync` | function | Rethrow a `CommandFailed` as another error |
|
|
679
729
|
| `catchFailed`, `catchFailedAsync` | function | Turn a `CommandFailed` into a value |
|
|
680
730
|
| `asAsync` | function | Wrap a sync runner as an async one |
|
|
681
|
-
| `CommandFailed` | class | Error with `argv`, `result` and `detail`; `message` shows
|
|
682
|
-
| `CommandRunner`, `AsyncCommandRunner` | interface | Capture runner contracts
|
|
683
|
-
| `CommandResult` | interface | `exitCode`, `stdout`, `stderr`, `neverExited`, `reason`, `signal`, `error` |
|
|
731
|
+
| `CommandFailed` | class | Error with `binary`, `argv`, `result` and `detail`; `message` shows the command |
|
|
732
|
+
| `CommandRunner`, `AsyncCommandRunner` | interface | Capture runner contracts; `binary` names the program |
|
|
733
|
+
| `CommandResult` | interface | `exitCode`, `stdout`, `stderr`, `neverExited`, `reason`, `signal`, `outputIncomplete`, `error` |
|
|
684
734
|
| `NeverExitedReason` | type | `'spawn' \| 'timeout' \| 'abort' \| 'signal' \| 'output'` |
|
|
685
735
|
| `CommandRunOptions`, `AsyncCommandRunOptions` | interface | `cwd`, `timeoutMs`, `input`, `env`; async adds `signal`, `onSpawn`, `processGroup` |
|
|
686
736
|
| `EnvOverrides` | type | `Record<string, string \| undefined>` |
|
|
@@ -703,6 +753,8 @@ are settled, and Windows support has landed, if that is feasible.
|
|
|
703
753
|
| `GroupSignalForwardingOptions` | interface | `signals`, `graceMs`, `exit` |
|
|
704
754
|
| `withDefaults` | function | Bind default options to any runner |
|
|
705
755
|
| `DefaultedRunner` | interface | The runner `withDefaults` returns |
|
|
756
|
+
| `which` | function | The path a spawn of a binary would run, without running it |
|
|
757
|
+
| `WhichOptions` | interface | `cwd`, `env` |
|
|
706
758
|
| `fakeRunner` | function | `/testing`: a `CommandRunner` that answers from a table |
|
|
707
759
|
| `fakeNeverExited` | function | `/testing`: a fake answer for a run that never exited |
|
|
708
760
|
| `fakeProcess` | function | `/testing`: a `ManagedProcessHandle` the test ends by hand |
|
package/async.ts
CHANGED
|
@@ -67,6 +67,8 @@ export interface AsyncCommandRunOptions extends CommandRunOptions {
|
|
|
67
67
|
|
|
68
68
|
/** The async form of {@link CommandRunner}: `run` returns a promise. */
|
|
69
69
|
export interface AsyncCommandRunner {
|
|
70
|
+
/** The binary this runner runs, when it has one. See `CommandRunner.binary`. */
|
|
71
|
+
readonly binary?: string | undefined
|
|
70
72
|
/**
|
|
71
73
|
* Run the binary with `argv` (without the binary name). The promise never
|
|
72
74
|
* rejects for a failed command: a non-zero exit, a missing binary, a
|
|
@@ -141,6 +143,7 @@ const ABORT_KILL_GRACE_MS = 1000
|
|
|
141
143
|
*/
|
|
142
144
|
export function spawnRunnerAsync(binary: string): AsyncCommandRunner {
|
|
143
145
|
return {
|
|
146
|
+
binary,
|
|
144
147
|
run(argv: string[], opts: AsyncCommandRunOptions): Promise<CommandResult> {
|
|
145
148
|
return new Promise((resolve) => {
|
|
146
149
|
// Inside the executor, so an invalid timeoutMs (or a throw from spawn)
|
|
@@ -391,17 +394,23 @@ export async function runCheckedAsync(
|
|
|
391
394
|
opts: AsyncCommandRunOptions,
|
|
392
395
|
): Promise<CommandResult>
|
|
393
396
|
export async function runCheckedAsync<P extends unknown[]>(
|
|
394
|
-
runner: {
|
|
397
|
+
runner: {
|
|
398
|
+
readonly binary?: string | undefined
|
|
399
|
+
run(argv: string[], ...opts: P): Promise<CommandResult>
|
|
400
|
+
},
|
|
395
401
|
argv: string[],
|
|
396
402
|
...opts: NoInfer<P>
|
|
397
403
|
): Promise<CommandResult>
|
|
398
404
|
export async function runCheckedAsync(
|
|
399
|
-
runner: {
|
|
405
|
+
runner: {
|
|
406
|
+
readonly binary?: string | undefined
|
|
407
|
+
run(argv: string[], ...opts: unknown[]): Promise<CommandResult>
|
|
408
|
+
},
|
|
400
409
|
argv: string[],
|
|
401
410
|
...opts: unknown[]
|
|
402
411
|
): Promise<CommandResult> {
|
|
403
412
|
const result = await runner.run(argv, ...opts)
|
|
404
|
-
if (result.exitCode !== 0) throw new CommandFailed(argv, result)
|
|
413
|
+
if (result.exitCode !== 0) throw new CommandFailed(argv, result, runner.binary)
|
|
405
414
|
return result
|
|
406
415
|
}
|
|
407
416
|
|
|
@@ -416,6 +425,7 @@ export async function runCheckedAsync(
|
|
|
416
425
|
*/
|
|
417
426
|
export function asAsync(runner: CommandRunner): AsyncCommandRunner {
|
|
418
427
|
return {
|
|
428
|
+
...(runner.binary === undefined ? {} : { binary: runner.binary }),
|
|
419
429
|
run: async (argv, opts) => runner.run(argv, opts),
|
|
420
430
|
}
|
|
421
431
|
}
|
package/defaults.ts
CHANGED
|
@@ -24,6 +24,8 @@ export type DefaultedOptions<O, K extends keyof O> = Omit<O, K> & Partial<Pick<O
|
|
|
24
24
|
* @experimental
|
|
25
25
|
*/
|
|
26
26
|
export interface DefaultedRunner<Rn extends AnyRunner, K extends keyof RunOptions<Rn> = never> {
|
|
27
|
+
/** The wrapped runner's `binary`, when it has one. */
|
|
28
|
+
readonly binary?: string | undefined
|
|
27
29
|
run(
|
|
28
30
|
arg: Parameters<Rn['run']>[0],
|
|
29
31
|
...opts: {} extends DefaultedOptions<RunOptions<Rn>, K>
|
|
@@ -62,8 +64,12 @@ export function withDefaults<Rn extends AnyRunner, K extends keyof RunOptions<Rn
|
|
|
62
64
|
defaults: Pick<RunOptions<Rn>, K> & Partial<RunOptions<Rn>>,
|
|
63
65
|
): DefaultedRunner<Rn, K> {
|
|
64
66
|
const bound: Record<string, unknown> = { ...defaults }
|
|
65
|
-
const target = runner as unknown as {
|
|
67
|
+
const target = runner as unknown as {
|
|
68
|
+
binary?: unknown
|
|
69
|
+
run(arg: unknown, opts: unknown): ReturnType<Rn['run']>
|
|
70
|
+
}
|
|
66
71
|
return {
|
|
72
|
+
...(typeof target.binary === 'string' ? { binary: target.binary } : {}),
|
|
67
73
|
run(arg: unknown, opts?: object) {
|
|
68
74
|
const merged = { ...bound }
|
|
69
75
|
for (const [key, value] of Object.entries(opts ?? {})) {
|
package/dist/async.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface AsyncCommandRunOptions extends CommandRunOptions {
|
|
|
45
45
|
}
|
|
46
46
|
/** The async form of {@link CommandRunner}: `run` returns a promise. */
|
|
47
47
|
export interface AsyncCommandRunner {
|
|
48
|
+
/** The binary this runner runs, when it has one. See `CommandRunner.binary`. */
|
|
49
|
+
readonly binary?: string | undefined;
|
|
48
50
|
/**
|
|
49
51
|
* Run the binary with `argv` (without the binary name). The promise never
|
|
50
52
|
* rejects for a failed command: a non-zero exit, a missing binary, a
|
|
@@ -108,6 +110,7 @@ export declare function spawnRunnerAsync(binary: string): AsyncCommandRunner;
|
|
|
108
110
|
*/
|
|
109
111
|
export declare function runCheckedAsync(runner: AsyncCommandRunner, argv: string[], opts: AsyncCommandRunOptions): Promise<CommandResult>;
|
|
110
112
|
export declare function runCheckedAsync<P extends unknown[]>(runner: {
|
|
113
|
+
readonly binary?: string | undefined;
|
|
111
114
|
run(argv: string[], ...opts: P): Promise<CommandResult>;
|
|
112
115
|
}, argv: string[], ...opts: NoInfer<P>): Promise<CommandResult>;
|
|
113
116
|
/**
|
package/dist/async.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"async.d.ts","sourceRoot":"","sources":["../async.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAYH,OAAO,EACL,aAAa,EAKb,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACvB,MAAM,WAAW,CAAA;AAElB,wFAAwF;AACxF,MAAM,WAAW,sBAAuB,SAAQ,iBAAiB;IAC/D;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;IAChC;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;IAC7C;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CACnC;AAED,wEAAwE;AACxE,MAAM,WAAW,kBAAkB;IACjC;;;;;;OAMG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;CAC1E;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,kBAAkB,GAAG,SAAS,GACxC,kBAAkB,CAEpB;AAgBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,
|
|
1
|
+
{"version":3,"file":"async.d.ts","sourceRoot":"","sources":["../async.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAYH,OAAO,EACL,aAAa,EAKb,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACvB,MAAM,WAAW,CAAA;AAElB,wFAAwF;AACxF,MAAM,WAAW,sBAAuB,SAAQ,iBAAiB;IAC/D;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;IAChC;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;IAC7C;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CACnC;AAED,wEAAwE;AACxE,MAAM,WAAW,kBAAkB;IACjC,gFAAgF;IAChF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC;;;;;;OAMG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;CAC1E;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,kBAAkB,GAAG,SAAS,GACxC,kBAAkB,CAEpB;AAgBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,CA4OnE;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,aAAa,CAAC,CAAA;AACzB,wBAAsB,eAAe,CAAC,CAAC,SAAS,OAAO,EAAE,EACvD,MAAM,EAAE;IACN,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,IAAI,EAAE,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;CACxD,EACD,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,GAClB,OAAO,CAAC,aAAa,CAAC,CAAA;AAczB;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,aAAa,GAAG,kBAAkB,CAKjE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAC,CAAC,EACpC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,MAAM,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GACxD,OAAO,CAAC,CAAC,CAAC,CAOZ;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,CAAC,EACzC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,QAAQ,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAClD,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAOhB"}
|
package/dist/async.js
CHANGED
|
@@ -69,6 +69,7 @@ const ABORT_KILL_GRACE_MS = 1000;
|
|
|
69
69
|
*/
|
|
70
70
|
export function spawnRunnerAsync(binary) {
|
|
71
71
|
return {
|
|
72
|
+
binary,
|
|
72
73
|
run(argv, opts) {
|
|
73
74
|
return new Promise((resolve) => {
|
|
74
75
|
// Inside the executor, so an invalid timeoutMs (or a throw from spawn)
|
|
@@ -308,7 +309,7 @@ export function spawnRunnerAsync(binary) {
|
|
|
308
309
|
export async function runCheckedAsync(runner, argv, ...opts) {
|
|
309
310
|
const result = await runner.run(argv, ...opts);
|
|
310
311
|
if (result.exitCode !== 0)
|
|
311
|
-
throw new CommandFailed(argv, result);
|
|
312
|
+
throw new CommandFailed(argv, result, runner.binary);
|
|
312
313
|
return result;
|
|
313
314
|
}
|
|
314
315
|
/**
|
|
@@ -322,6 +323,7 @@ export async function runCheckedAsync(runner, argv, ...opts) {
|
|
|
322
323
|
*/
|
|
323
324
|
export function asAsync(runner) {
|
|
324
325
|
return {
|
|
326
|
+
...(runner.binary === undefined ? {} : { binary: runner.binary }),
|
|
325
327
|
run: async (argv, opts) => runner.run(argv, opts),
|
|
326
328
|
};
|
|
327
329
|
}
|
package/dist/defaults.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export type DefaultedOptions<O, K extends keyof O> = Omit<O, K> & Partial<Pick<O
|
|
|
20
20
|
* @experimental
|
|
21
21
|
*/
|
|
22
22
|
export interface DefaultedRunner<Rn extends AnyRunner, K extends keyof RunOptions<Rn> = never> {
|
|
23
|
+
/** The wrapped runner's `binary`, when it has one. */
|
|
24
|
+
readonly binary?: string | undefined;
|
|
23
25
|
run(arg: Parameters<Rn['run']>[0], ...opts: {} extends DefaultedOptions<RunOptions<Rn>, K> ? [opts?: DefaultedOptions<RunOptions<Rn>, K>] : [opts: DefaultedOptions<RunOptions<Rn>, K>]): ReturnType<Rn['run']>;
|
|
24
26
|
}
|
|
25
27
|
/**
|
package/dist/defaults.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../defaults.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,kFAAkF;AAClF,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,GAAG,OAAO,CAAA;CACtC;AAED,uCAAuC;AACvC,KAAK,UAAU,CAAC,EAAE,SAAS,SAAS,IAAI,UAAU,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAEhE,oFAAoF;AACpF,MAAM,MAAM,gBAAgB,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;AAErF;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC,SAAS,MAAM,UAAU,CAAC,EAAE,CAAC,GAAG,KAAK;IAC3F,GAAG,CACD,GAAG,EAAE,UAAU,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAC7B,GAAG,IAAI,EAAE,EAAE,SAAS,gBAAgB,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACnD,CAAC,IAAI,CAAC,EAAE,gBAAgB,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAC5C,CAAC,IAAI,EAAE,gBAAgB,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAC9C,UAAU,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,YAAY,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC,SAAS,MAAM,UAAU,CAAC,EAAE,CAAC,GAAG,KAAK,EACvF,MAAM,EAAE,EAAE,EACV,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,GAC1D,eAAe,CAAC,EAAE,EAAE,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../defaults.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,kFAAkF;AAClF,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,GAAG,OAAO,CAAA;CACtC;AAED,uCAAuC;AACvC,KAAK,UAAU,CAAC,EAAE,SAAS,SAAS,IAAI,UAAU,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAEhE,oFAAoF;AACpF,MAAM,MAAM,gBAAgB,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;AAErF;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC,SAAS,MAAM,UAAU,CAAC,EAAE,CAAC,GAAG,KAAK;IAC3F,sDAAsD;IACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,GAAG,CACD,GAAG,EAAE,UAAU,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAC7B,GAAG,IAAI,EAAE,EAAE,SAAS,gBAAgB,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACnD,CAAC,IAAI,CAAC,EAAE,gBAAgB,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAC5C,CAAC,IAAI,EAAE,gBAAgB,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAC9C,UAAU,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,YAAY,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC,SAAS,MAAM,UAAU,CAAC,EAAE,CAAC,GAAG,KAAK,EACvF,MAAM,EAAE,EAAE,EACV,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,GAC1D,eAAe,CAAC,EAAE,EAAE,CAAC,CAAC,CAoBxB"}
|
package/dist/defaults.js
CHANGED
|
@@ -32,6 +32,7 @@ export function withDefaults(runner, defaults) {
|
|
|
32
32
|
const bound = { ...defaults };
|
|
33
33
|
const target = runner;
|
|
34
34
|
return {
|
|
35
|
+
...(typeof target.binary === 'string' ? { binary: target.binary } : {}),
|
|
35
36
|
run(arg, opts) {
|
|
36
37
|
const merged = { ...bound };
|
|
37
38
|
for (const [key, value] of Object.entries(opts ?? {})) {
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* without waiting for it to finish. `stopProcess` stops it with a grace
|
|
14
14
|
* period.
|
|
15
15
|
* - `withDefaults` binds default options to any of them.
|
|
16
|
+
* - `which` finds a binary on `PATH` without running it.
|
|
16
17
|
*
|
|
17
18
|
* The capture runners share {@link CommandResult}, {@link CommandRunOptions}
|
|
18
19
|
* and {@link CommandFailed}, so one `instanceof CommandFailed` check covers
|
|
@@ -27,4 +28,5 @@ export { streamRunner, type StreamRunner, type StreamRunOptions, type StreamResu
|
|
|
27
28
|
export { managedProcessRunner, stopProcess, type ManagedProcessRunner, type ManagedProcessHandle, type ManagedProcessRunOptions, type ManagedProcessExit, } from './process.ts';
|
|
28
29
|
export { forwardedExitCode, forwardedSignal, installGroupSignalForwarding, liveProcessGroups, type GroupSignalForwardingOptions, } from './groups.ts';
|
|
29
30
|
export { withDefaults, type DefaultedRunner } from './defaults.ts';
|
|
31
|
+
export { which, type WhichOptions } from './which.ts';
|
|
30
32
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EACL,WAAW,EACX,aAAa,EACb,SAAS,EACT,aAAa,EACb,UAAU,EACV,WAAW,EACX,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,iBAAiB,GACvB,MAAM,WAAW,CAAA;AAElB,OAAO,EACL,OAAO,EACP,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,GAC5B,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,WAAW,EACX,SAAS,EACT,eAAe,EACf,GAAG,EACH,OAAO,EACP,YAAY,EACZ,aAAa,EACb,KAAK,kBAAkB,EACvB,KAAK,OAAO,EACZ,KAAK,cAAc,GACpB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAElD,OAAO,EACL,QAAQ,EACR,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,YAAY,EACZ,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,YAAY,GAClB,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,oBAAoB,EACpB,WAAW,EACX,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,GACxB,MAAM,cAAc,CAAA;AAErB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,4BAA4B,EAC5B,iBAAiB,EACjB,KAAK,4BAA4B,GAClC,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,YAAY,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAA;AAElE,OAAO,EAAE,KAAK,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* without waiting for it to finish. `stopProcess` stops it with a grace
|
|
14
14
|
* period.
|
|
15
15
|
* - `withDefaults` binds default options to any of them.
|
|
16
|
+
* - `which` finds a binary on `PATH` without running it.
|
|
16
17
|
*
|
|
17
18
|
* The capture runners share {@link CommandResult}, {@link CommandRunOptions}
|
|
18
19
|
* and {@link CommandFailed}, so one `instanceof CommandFailed` check covers
|
|
@@ -27,3 +28,4 @@ export { streamRunner, } from "./stream.js";
|
|
|
27
28
|
export { managedProcessRunner, stopProcess, } from "./process.js";
|
|
28
29
|
export { forwardedExitCode, forwardedSignal, installGroupSignalForwarding, liveProcessGroups, } from "./groups.js";
|
|
29
30
|
export { withDefaults } from "./defaults.js";
|
|
31
|
+
export { which } from "./which.js";
|
package/dist/sync.d.ts
CHANGED
|
@@ -46,6 +46,26 @@ export interface CommandResult {
|
|
|
46
46
|
* it leaves this unset there.
|
|
47
47
|
*/
|
|
48
48
|
signal?: NodeJS.Signals | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* `true` when the child exited but the runner stopped reading its output
|
|
51
|
+
* early, so `stdout` and `stderr` may be cut short. `exitCode` is the
|
|
52
|
+
* child's own, `neverExited` and `reason` are absent, and `error` says why.
|
|
53
|
+
* Absent otherwise. Two cases, both on the sync capture runner:
|
|
54
|
+
*
|
|
55
|
+
* - The child exited, but a process it started (`cmd &`, a daemon, a
|
|
56
|
+
* postinstall script) still held stdout or stderr open when `timeoutMs`
|
|
57
|
+
* ran out. Anything that process wrote after the deadline is lost, and it
|
|
58
|
+
* keeps running. Without `timeoutMs` the run waits for it to exit.
|
|
59
|
+
* - The child passed the 256 MB output cap but exited before the runner's
|
|
60
|
+
* kill landed, on Node or Bun.
|
|
61
|
+
*
|
|
62
|
+
* The `bun test` capture path writes output to files and never waits on a
|
|
63
|
+
* held pipe, so it never sets this flag.
|
|
64
|
+
*
|
|
65
|
+
* `ran` and `runChecked` go by the exit code alone, so they still count
|
|
66
|
+
* such a run as a success. Check this flag when the output must be whole.
|
|
67
|
+
*/
|
|
68
|
+
outputIncomplete?: boolean | undefined;
|
|
49
69
|
/**
|
|
50
70
|
* The runner's own message, when it has one: a failed spawn, a timeout, an
|
|
51
71
|
* abort, an `onSpawn` that threw, a refused `processGroup` run, the output
|
|
@@ -53,9 +73,8 @@ export interface CommandResult {
|
|
|
53
73
|
* `stderr`. Absent otherwise.
|
|
54
74
|
*
|
|
55
75
|
* It can also be set when the child did exit: a failed stdin write (Bun's
|
|
56
|
-
* `spawnSync` does not report one), or
|
|
57
|
-
*
|
|
58
|
-
* absent.
|
|
76
|
+
* `spawnSync` does not report one), or a run with `outputIncomplete` set.
|
|
77
|
+
* `exitCode` is then the child's own and `reason` is absent.
|
|
59
78
|
*/
|
|
60
79
|
error?: string | undefined;
|
|
61
80
|
}
|
|
@@ -121,6 +140,12 @@ export type EnvOverrides = Record<string, string | undefined>;
|
|
|
121
140
|
* ```
|
|
122
141
|
*/
|
|
123
142
|
export interface CommandRunner {
|
|
143
|
+
/**
|
|
144
|
+
* The binary this runner runs, when it has one. `runChecked` puts it in
|
|
145
|
+
* `CommandFailed`, so the message names the program. `spawnRunner` sets
|
|
146
|
+
* it; a fake may leave it out.
|
|
147
|
+
*/
|
|
148
|
+
readonly binary?: string | undefined;
|
|
124
149
|
/**
|
|
125
150
|
* Run the binary with `argv` (the arguments, without the binary name) and
|
|
126
151
|
* wait for it to exit. Never throws for a failed command: a non-zero exit,
|
|
@@ -196,17 +221,20 @@ export declare function syncStop(proc: {
|
|
|
196
221
|
};
|
|
197
222
|
/**
|
|
198
223
|
* Thrown by {@link runChecked} and `runCheckedAsync` on a non-zero exit.
|
|
199
|
-
* Carries the argv and the full {@link CommandResult}, so a
|
|
200
|
-
* the output without running the command again.
|
|
224
|
+
* Carries the binary, the argv and the full {@link CommandResult}, so a
|
|
225
|
+
* caller can report the output without running the command again.
|
|
201
226
|
*
|
|
202
|
-
* `message` is `command failed (exit N): <argv>`,
|
|
227
|
+
* `message` is `command failed (exit N): <binary> <argv>`, quoted by
|
|
203
228
|
* `shellJoin`, followed on a new line by the trimmed stderr, or the trimmed
|
|
204
229
|
* `error` when stderr is empty. Nothing follows when both are empty. The argv
|
|
205
230
|
* appears in full, so error logs and reports carry it: pass secrets through
|
|
206
231
|
* `env` or `input`, not argv. Use `detail` to show a person
|
|
207
|
-
* why it failed.
|
|
232
|
+
* why it failed. The binary is left out when it is not known, as for a fake
|
|
233
|
+
* runner without `binary`.
|
|
208
234
|
*/
|
|
209
235
|
export declare class CommandFailed extends Error {
|
|
236
|
+
/** The binary that ran, when the runner reports one (see `CommandRunner.binary`). */
|
|
237
|
+
readonly binary: string | undefined;
|
|
210
238
|
/** The arguments the command ran with, without the binary name. */
|
|
211
239
|
readonly argv: string[];
|
|
212
240
|
/** The full result of the failed run. */
|
|
@@ -231,8 +259,9 @@ export declare class CommandFailed extends Error {
|
|
|
231
259
|
/**
|
|
232
260
|
* @param argv - The arguments the command ran with.
|
|
233
261
|
* @param result - The result of the failed run.
|
|
262
|
+
* @param binary - The binary that ran, when known.
|
|
234
263
|
*/
|
|
235
|
-
constructor(argv: string[], result: CommandResult);
|
|
264
|
+
constructor(argv: string[], result: CommandResult, binary?: string | undefined);
|
|
236
265
|
}
|
|
237
266
|
/**
|
|
238
267
|
* Run a command and throw if it fails. Use it where the only sensible
|
|
@@ -258,6 +287,7 @@ export declare class CommandFailed extends Error {
|
|
|
258
287
|
*/
|
|
259
288
|
export declare function runChecked(runner: CommandRunner, argv: string[], opts: CommandRunOptions): CommandResult;
|
|
260
289
|
export declare function runChecked<P extends unknown[]>(runner: {
|
|
290
|
+
readonly binary?: string | undefined;
|
|
261
291
|
run(argv: string[], ...opts: P): CommandResult;
|
|
262
292
|
}, argv: string[], ...opts: NoInfer<P>): CommandResult;
|
|
263
293
|
/**
|
|
@@ -283,6 +313,15 @@ export declare function mapFailed<T>(fn: () => T, mapper: (failed: CommandFailed
|
|
|
283
313
|
* @returns What `fn` returns, or what `onFailed` returns.
|
|
284
314
|
*/
|
|
285
315
|
export declare function catchFailed<T, R>(fn: () => T, onFailed: (failed: CommandFailed) => R): T | R;
|
|
316
|
+
/**
|
|
317
|
+
* Folds a finished `spawnSync` into a {@link CommandResult}. Exported for
|
|
318
|
+
* tests.
|
|
319
|
+
*/
|
|
320
|
+
export declare function fromSpawnSync(proc: {
|
|
321
|
+
status: number | null;
|
|
322
|
+
signal: NodeJS.Signals | null;
|
|
323
|
+
error?: Error | undefined;
|
|
324
|
+
}, stdout: string, stderr: string): CommandResult;
|
|
286
325
|
/**
|
|
287
326
|
* The injected runner when there is one, else `spawnRunner(binary)`.
|
|
288
327
|
*
|
package/dist/sync.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../sync.ts"],"names":[],"mappings":"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"}
|
package/dist/sync.js
CHANGED
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
* exit, for callers whose only sensible response to failure is to stop.
|
|
12
12
|
*/
|
|
13
13
|
import { spawnSync } from 'node:child_process';
|
|
14
|
-
import {
|
|
14
|
+
import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
15
15
|
import { tmpdir } from 'node:os';
|
|
16
|
-
import {
|
|
16
|
+
import { join } from 'node:path';
|
|
17
17
|
import { shellJoin } from "./quote.js";
|
|
18
18
|
import { failureText, stderrOrError } from "./result.js";
|
|
19
|
+
import { which } from "./which.js";
|
|
19
20
|
/**
|
|
20
21
|
* `{ env }`: `process.env` merged with `opts.env`, minus any key whose
|
|
21
22
|
* override is `undefined`. Always an explicit object built from the current
|
|
@@ -147,17 +148,20 @@ export function syncStop(proc) {
|
|
|
147
148
|
}
|
|
148
149
|
/**
|
|
149
150
|
* Thrown by {@link runChecked} and `runCheckedAsync` on a non-zero exit.
|
|
150
|
-
* Carries the argv and the full {@link CommandResult}, so a
|
|
151
|
-
* the output without running the command again.
|
|
151
|
+
* Carries the binary, the argv and the full {@link CommandResult}, so a
|
|
152
|
+
* caller can report the output without running the command again.
|
|
152
153
|
*
|
|
153
|
-
* `message` is `command failed (exit N): <argv>`,
|
|
154
|
+
* `message` is `command failed (exit N): <binary> <argv>`, quoted by
|
|
154
155
|
* `shellJoin`, followed on a new line by the trimmed stderr, or the trimmed
|
|
155
156
|
* `error` when stderr is empty. Nothing follows when both are empty. The argv
|
|
156
157
|
* appears in full, so error logs and reports carry it: pass secrets through
|
|
157
158
|
* `env` or `input`, not argv. Use `detail` to show a person
|
|
158
|
-
* why it failed.
|
|
159
|
+
* why it failed. The binary is left out when it is not known, as for a fake
|
|
160
|
+
* runner without `binary`.
|
|
159
161
|
*/
|
|
160
162
|
export class CommandFailed extends Error {
|
|
163
|
+
/** The binary that ran, when the runner reports one (see `CommandRunner.binary`). */
|
|
164
|
+
binary;
|
|
161
165
|
/** The arguments the command ran with, without the binary name. */
|
|
162
166
|
argv;
|
|
163
167
|
/** The full result of the failed run. */
|
|
@@ -184,12 +188,15 @@ export class CommandFailed extends Error {
|
|
|
184
188
|
/**
|
|
185
189
|
* @param argv - The arguments the command ran with.
|
|
186
190
|
* @param result - The result of the failed run.
|
|
191
|
+
* @param binary - The binary that ran, when known.
|
|
187
192
|
*/
|
|
188
|
-
constructor(argv, result) {
|
|
193
|
+
constructor(argv, result, binary) {
|
|
189
194
|
const stderr = stderrOrError(result);
|
|
190
|
-
|
|
195
|
+
const command = binary === undefined ? argv : [binary, ...argv];
|
|
196
|
+
super(`command failed (exit ${result.exitCode}): ${shellJoin(command)}` +
|
|
191
197
|
(stderr ? `\n${stderr}` : ''));
|
|
192
198
|
this.name = 'CommandFailed';
|
|
199
|
+
this.binary = binary;
|
|
193
200
|
this.argv = argv;
|
|
194
201
|
this.result = result;
|
|
195
202
|
}
|
|
@@ -197,7 +204,7 @@ export class CommandFailed extends Error {
|
|
|
197
204
|
export function runChecked(runner, argv, ...opts) {
|
|
198
205
|
const result = runner.run(argv, ...opts);
|
|
199
206
|
if (result.exitCode !== 0)
|
|
200
|
-
throw new CommandFailed(argv, result);
|
|
207
|
+
throw new CommandFailed(argv, result, runner.binary);
|
|
201
208
|
return result;
|
|
202
209
|
}
|
|
203
210
|
/**
|
|
@@ -253,39 +260,26 @@ export function catchFailed(fn, onFailed) {
|
|
|
253
260
|
// set `NODE_ENV=test`, but they run on Node and keep the pipe path below.
|
|
254
261
|
const CHILD_STDIO_CORRUPTED = process.versions['bun'] !== undefined && process.env['NODE_ENV'] === 'test';
|
|
255
262
|
/**
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
* on the child's `PATH`.
|
|
263
|
+
* Folds a finished `spawnSync` into a {@link CommandResult}. Exported for
|
|
264
|
+
* tests.
|
|
259
265
|
*/
|
|
260
|
-
function
|
|
261
|
-
const
|
|
262
|
-
try {
|
|
263
|
-
accessSync(p, constants.X_OK);
|
|
264
|
-
return statSync(p).isFile();
|
|
265
|
-
}
|
|
266
|
-
catch {
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
if (binary === '')
|
|
271
|
-
return false;
|
|
272
|
-
if (binary.includes('/'))
|
|
273
|
-
return isExecutable(resolve(cwd, binary));
|
|
274
|
-
return (path ?? '/usr/bin:/bin')
|
|
275
|
-
.split(delimiter)
|
|
276
|
-
.some((dir) => isExecutable(resolve(cwd, dir, binary)));
|
|
277
|
-
}
|
|
278
|
-
/** Folds a finished `spawnSync` into a {@link CommandResult}. */
|
|
279
|
-
function fromSpawnSync(proc, stdout, stderr) {
|
|
266
|
+
export function fromSpawnSync(proc, stdout, stderr) {
|
|
267
|
+
const code = proc.error?.code;
|
|
280
268
|
return {
|
|
281
269
|
exitCode: proc.status ?? 1,
|
|
282
270
|
// Bun reports a failed spawn with `status` undefined, not null.
|
|
283
271
|
...(proc.status == null ? { neverExited: true, ...syncStop(proc) } : {}),
|
|
272
|
+
// A real status with a timeout: the child exited, but a process it started
|
|
273
|
+
// held the pipes until the deadline. With the cap: the child passed it and
|
|
274
|
+
// exited before the kill landed.
|
|
275
|
+
...(proc.status != null && (code === 'ETIMEDOUT' || code === 'ENOBUFS')
|
|
276
|
+
? { outputIncomplete: true }
|
|
277
|
+
: {}),
|
|
284
278
|
stdout,
|
|
285
279
|
stderr,
|
|
286
280
|
// `proc.error` is set for a failed spawn, a timeout, the output cap, or a
|
|
287
|
-
// failed stdin write
|
|
288
|
-
//
|
|
281
|
+
// failed stdin write. The first three usually leave `status` null; when
|
|
282
|
+
// they do not, `outputIncomplete` is set above. A stdin write error can
|
|
289
283
|
// come with a child that did exit, so it is kept either way.
|
|
290
284
|
...(proc.error ? { error: proc.error.message } : {}),
|
|
291
285
|
};
|
|
@@ -305,7 +299,7 @@ function runViaChildRedirect(binary, argv, opts, timeoutMs) {
|
|
|
305
299
|
// the same stand-in the pipe path reports for a failed spawn. This also
|
|
306
300
|
// keeps a binary named like an option (`-a`) from reaching `exec` as one,
|
|
307
301
|
// unless an executable named `-a` is on PATH.
|
|
308
|
-
if (
|
|
302
|
+
if (which(binary, { cwd: opts.cwd, env: { PATH: env['PATH'] } }) === undefined) {
|
|
309
303
|
return spawnFailure(`spawnSync ${binary} ENOENT`);
|
|
310
304
|
}
|
|
311
305
|
const dir = mkdtempSync(join(tmpdir(), 'command-seam-'));
|
|
@@ -384,6 +378,7 @@ export function resolveRunner(binary, injected) {
|
|
|
384
378
|
*/
|
|
385
379
|
export function spawnRunner(binary) {
|
|
386
380
|
return {
|
|
381
|
+
binary,
|
|
387
382
|
run(argv, opts) {
|
|
388
383
|
const timeoutMs = normalizeTimeout(opts.timeoutMs);
|
|
389
384
|
const cwdMessage = cwdError(`spawnSync ${binary}`, binary, argv, opts);
|
package/dist/which.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `which`: find a binary on `PATH` without running it, the way the runners'
|
|
3
|
+
* spawn would find it. For code that picks a tool by what is installed;
|
|
4
|
+
* running `--version` to probe is slow for some CLIs and has side effects
|
|
5
|
+
* (update checks, telemetry) for others.
|
|
6
|
+
*/
|
|
7
|
+
import type { EnvOverrides } from './sync.ts';
|
|
8
|
+
/**
|
|
9
|
+
* Options for {@link which}.
|
|
10
|
+
*
|
|
11
|
+
* @experimental
|
|
12
|
+
*/
|
|
13
|
+
export interface WhichOptions {
|
|
14
|
+
/**
|
|
15
|
+
* The folder a relative path, or a relative `PATH` entry, is resolved
|
|
16
|
+
* against, as a spawn with this `cwd` would. Default `process.cwd()`.
|
|
17
|
+
*/
|
|
18
|
+
cwd?: string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Variables merged over the parent's environment, as the runners take
|
|
21
|
+
* them. Only `PATH` is read; an `undefined` value unsets it.
|
|
22
|
+
*/
|
|
23
|
+
env?: EnvOverrides | undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The absolute path of the file a spawn of `binary` would run, or
|
|
27
|
+
* `undefined` when there is none. A name with a `/` is taken relative to
|
|
28
|
+
* `cwd`; a bare name is looked up on `PATH`, first match wins. A match must be
|
|
29
|
+
* an executable file. With `PATH` unset, it searches `/usr/bin:/bin`. An
|
|
30
|
+
* empty `PATH` entry (`PATH=''`, `a::b`, a trailing `:`) means `cwd`, as it
|
|
31
|
+
* does for Node's spawn; Bun's spawn skips empty entries, so under Bun such
|
|
32
|
+
* a match may not start. POSIX only: `PATHEXT` is not read.
|
|
33
|
+
*
|
|
34
|
+
* Never throws for a missing binary, an unreadable folder or a bad `cwd`.
|
|
35
|
+
*
|
|
36
|
+
* @param binary - A name looked up on `PATH`, or a path.
|
|
37
|
+
* @param opts - `cwd` and `env`, as the runners take them.
|
|
38
|
+
* @returns The absolute path, or `undefined`.
|
|
39
|
+
* @experimental
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* const harnesses = ['codex', 'claude', 'cursor-agent'].filter((b) => which(b) !== undefined)
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function which(binary: string, opts?: WhichOptions): string | undefined;
|
|
47
|
+
//# sourceMappingURL=which.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"which.d.ts","sourceRoot":"","sources":["../which.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAE7C;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACxB;;;OAGG;IACH,GAAG,CAAC,EAAE,YAAY,GAAG,SAAS,CAAA;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,YAAiB,GAAG,MAAM,GAAG,SAAS,CAUjF"}
|
package/dist/which.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `which`: find a binary on `PATH` without running it, the way the runners'
|
|
3
|
+
* spawn would find it. For code that picks a tool by what is installed;
|
|
4
|
+
* running `--version` to probe is slow for some CLIs and has side effects
|
|
5
|
+
* (update checks, telemetry) for others.
|
|
6
|
+
*/
|
|
7
|
+
import { accessSync, constants, statSync } from 'node:fs';
|
|
8
|
+
import { delimiter, resolve } from 'node:path';
|
|
9
|
+
/**
|
|
10
|
+
* The absolute path of the file a spawn of `binary` would run, or
|
|
11
|
+
* `undefined` when there is none. A name with a `/` is taken relative to
|
|
12
|
+
* `cwd`; a bare name is looked up on `PATH`, first match wins. A match must be
|
|
13
|
+
* an executable file. With `PATH` unset, it searches `/usr/bin:/bin`. An
|
|
14
|
+
* empty `PATH` entry (`PATH=''`, `a::b`, a trailing `:`) means `cwd`, as it
|
|
15
|
+
* does for Node's spawn; Bun's spawn skips empty entries, so under Bun such
|
|
16
|
+
* a match may not start. POSIX only: `PATHEXT` is not read.
|
|
17
|
+
*
|
|
18
|
+
* Never throws for a missing binary, an unreadable folder or a bad `cwd`.
|
|
19
|
+
*
|
|
20
|
+
* @param binary - A name looked up on `PATH`, or a path.
|
|
21
|
+
* @param opts - `cwd` and `env`, as the runners take them.
|
|
22
|
+
* @returns The absolute path, or `undefined`.
|
|
23
|
+
* @experimental
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const harnesses = ['codex', 'claude', 'cursor-agent'].filter((b) => which(b) !== undefined)
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function which(binary, opts = {}) {
|
|
31
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
32
|
+
if (binary === '')
|
|
33
|
+
return undefined;
|
|
34
|
+
if (binary.includes('/'))
|
|
35
|
+
return executable(resolve(cwd, binary));
|
|
36
|
+
const path = opts.env !== undefined && 'PATH' in opts.env ? opts.env['PATH'] : process.env['PATH'];
|
|
37
|
+
for (const dir of (path ?? '/usr/bin:/bin').split(delimiter)) {
|
|
38
|
+
const found = executable(resolve(cwd, dir, binary));
|
|
39
|
+
if (found !== undefined)
|
|
40
|
+
return found;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
/** `p` when it is an executable file, else `undefined`. */
|
|
45
|
+
function executable(p) {
|
|
46
|
+
try {
|
|
47
|
+
accessSync(p, constants.X_OK);
|
|
48
|
+
return statSync(p).isFile() ? p : undefined;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
package/index.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* without waiting for it to finish. `stopProcess` stops it with a grace
|
|
14
14
|
* period.
|
|
15
15
|
* - `withDefaults` binds default options to any of them.
|
|
16
|
+
* - `which` finds a binary on `PATH` without running it.
|
|
16
17
|
*
|
|
17
18
|
* The capture runners share {@link CommandResult}, {@link CommandRunOptions}
|
|
18
19
|
* and {@link CommandFailed}, so one `instanceof CommandFailed` check covers
|
|
@@ -96,3 +97,5 @@ export {
|
|
|
96
97
|
} from './groups.ts'
|
|
97
98
|
|
|
98
99
|
export { withDefaults, type DefaultedRunner } from './defaults.ts'
|
|
100
|
+
|
|
101
|
+
export { which, type WhichOptions } from './which.ts'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@osdlc/command-seam",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Injectable subprocess runner for Node: capture, stream and managed-process runners, hard timeouts, failures as results.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "SK Sizer",
|
|
@@ -49,7 +49,6 @@
|
|
|
49
49
|
},
|
|
50
50
|
"files": [
|
|
51
51
|
"dist/",
|
|
52
|
-
"smoke.mjs",
|
|
53
52
|
"index.ts",
|
|
54
53
|
"sync.ts",
|
|
55
54
|
"async.ts",
|
|
@@ -60,6 +59,7 @@
|
|
|
60
59
|
"quote.ts",
|
|
61
60
|
"shell.ts",
|
|
62
61
|
"defaults.ts",
|
|
62
|
+
"which.ts",
|
|
63
63
|
"testing.ts",
|
|
64
64
|
"README.md",
|
|
65
65
|
"CHANGELOG.md",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"build": "tsc -p tsconfig.build.json",
|
|
89
89
|
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
90
90
|
"test": "bun test",
|
|
91
|
-
"smoke": "node smoke.mjs",
|
|
91
|
+
"smoke": "node tests/smoke.mjs",
|
|
92
92
|
"format": "prettier --write .",
|
|
93
93
|
"format:check": "prettier --check .",
|
|
94
94
|
"lint": "oxlint .",
|
package/sync.ts
CHANGED
|
@@ -12,20 +12,13 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { spawnSync } from 'node:child_process'
|
|
15
|
-
import {
|
|
16
|
-
accessSync,
|
|
17
|
-
constants,
|
|
18
|
-
mkdtempSync,
|
|
19
|
-
readFileSync,
|
|
20
|
-
rmSync,
|
|
21
|
-
statSync,
|
|
22
|
-
writeFileSync,
|
|
23
|
-
} from 'node:fs'
|
|
15
|
+
import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
24
16
|
import { tmpdir } from 'node:os'
|
|
25
|
-
import {
|
|
17
|
+
import { join } from 'node:path'
|
|
26
18
|
|
|
27
19
|
import { shellJoin } from './quote.ts'
|
|
28
20
|
import { failureText, stderrOrError } from './result.ts'
|
|
21
|
+
import { which } from './which.ts'
|
|
29
22
|
|
|
30
23
|
/** Result of running an external command through a `CommandRunner`. */
|
|
31
24
|
export interface CommandResult {
|
|
@@ -63,6 +56,26 @@ export interface CommandResult {
|
|
|
63
56
|
* it leaves this unset there.
|
|
64
57
|
*/
|
|
65
58
|
signal?: NodeJS.Signals | undefined
|
|
59
|
+
/**
|
|
60
|
+
* `true` when the child exited but the runner stopped reading its output
|
|
61
|
+
* early, so `stdout` and `stderr` may be cut short. `exitCode` is the
|
|
62
|
+
* child's own, `neverExited` and `reason` are absent, and `error` says why.
|
|
63
|
+
* Absent otherwise. Two cases, both on the sync capture runner:
|
|
64
|
+
*
|
|
65
|
+
* - The child exited, but a process it started (`cmd &`, a daemon, a
|
|
66
|
+
* postinstall script) still held stdout or stderr open when `timeoutMs`
|
|
67
|
+
* ran out. Anything that process wrote after the deadline is lost, and it
|
|
68
|
+
* keeps running. Without `timeoutMs` the run waits for it to exit.
|
|
69
|
+
* - The child passed the 256 MB output cap but exited before the runner's
|
|
70
|
+
* kill landed, on Node or Bun.
|
|
71
|
+
*
|
|
72
|
+
* The `bun test` capture path writes output to files and never waits on a
|
|
73
|
+
* held pipe, so it never sets this flag.
|
|
74
|
+
*
|
|
75
|
+
* `ran` and `runChecked` go by the exit code alone, so they still count
|
|
76
|
+
* such a run as a success. Check this flag when the output must be whole.
|
|
77
|
+
*/
|
|
78
|
+
outputIncomplete?: boolean | undefined
|
|
66
79
|
/**
|
|
67
80
|
* The runner's own message, when it has one: a failed spawn, a timeout, an
|
|
68
81
|
* abort, an `onSpawn` that threw, a refused `processGroup` run, the output
|
|
@@ -70,9 +83,8 @@ export interface CommandResult {
|
|
|
70
83
|
* `stderr`. Absent otherwise.
|
|
71
84
|
*
|
|
72
85
|
* It can also be set when the child did exit: a failed stdin write (Bun's
|
|
73
|
-
* `spawnSync` does not report one), or
|
|
74
|
-
*
|
|
75
|
-
* absent.
|
|
86
|
+
* `spawnSync` does not report one), or a run with `outputIncomplete` set.
|
|
87
|
+
* `exitCode` is then the child's own and `reason` is absent.
|
|
76
88
|
*/
|
|
77
89
|
error?: string | undefined
|
|
78
90
|
}
|
|
@@ -146,6 +158,12 @@ export type EnvOverrides = Record<string, string | undefined>
|
|
|
146
158
|
* ```
|
|
147
159
|
*/
|
|
148
160
|
export interface CommandRunner {
|
|
161
|
+
/**
|
|
162
|
+
* The binary this runner runs, when it has one. `runChecked` puts it in
|
|
163
|
+
* `CommandFailed`, so the message names the program. `spawnRunner` sets
|
|
164
|
+
* it; a fake may leave it out.
|
|
165
|
+
*/
|
|
166
|
+
readonly binary?: string | undefined
|
|
149
167
|
/**
|
|
150
168
|
* Run the binary with `argv` (the arguments, without the binary name) and
|
|
151
169
|
* wait for it to exit. Never throws for a failed command: a non-zero exit,
|
|
@@ -295,17 +313,20 @@ export function syncStop(proc: { error?: Error | undefined; signal: NodeJS.Signa
|
|
|
295
313
|
|
|
296
314
|
/**
|
|
297
315
|
* Thrown by {@link runChecked} and `runCheckedAsync` on a non-zero exit.
|
|
298
|
-
* Carries the argv and the full {@link CommandResult}, so a
|
|
299
|
-
* the output without running the command again.
|
|
316
|
+
* Carries the binary, the argv and the full {@link CommandResult}, so a
|
|
317
|
+
* caller can report the output without running the command again.
|
|
300
318
|
*
|
|
301
|
-
* `message` is `command failed (exit N): <argv>`,
|
|
319
|
+
* `message` is `command failed (exit N): <binary> <argv>`, quoted by
|
|
302
320
|
* `shellJoin`, followed on a new line by the trimmed stderr, or the trimmed
|
|
303
321
|
* `error` when stderr is empty. Nothing follows when both are empty. The argv
|
|
304
322
|
* appears in full, so error logs and reports carry it: pass secrets through
|
|
305
323
|
* `env` or `input`, not argv. Use `detail` to show a person
|
|
306
|
-
* why it failed.
|
|
324
|
+
* why it failed. The binary is left out when it is not known, as for a fake
|
|
325
|
+
* runner without `binary`.
|
|
307
326
|
*/
|
|
308
327
|
export class CommandFailed extends Error {
|
|
328
|
+
/** The binary that ran, when the runner reports one (see `CommandRunner.binary`). */
|
|
329
|
+
readonly binary: string | undefined
|
|
309
330
|
/** The arguments the command ran with, without the binary name. */
|
|
310
331
|
readonly argv: string[]
|
|
311
332
|
/** The full result of the failed run. */
|
|
@@ -334,14 +355,17 @@ export class CommandFailed extends Error {
|
|
|
334
355
|
/**
|
|
335
356
|
* @param argv - The arguments the command ran with.
|
|
336
357
|
* @param result - The result of the failed run.
|
|
358
|
+
* @param binary - The binary that ran, when known.
|
|
337
359
|
*/
|
|
338
|
-
constructor(argv: string[], result: CommandResult) {
|
|
360
|
+
constructor(argv: string[], result: CommandResult, binary?: string | undefined) {
|
|
339
361
|
const stderr = stderrOrError(result)
|
|
362
|
+
const command = binary === undefined ? argv : [binary, ...argv]
|
|
340
363
|
super(
|
|
341
|
-
`command failed (exit ${result.exitCode}): ${shellJoin(
|
|
364
|
+
`command failed (exit ${result.exitCode}): ${shellJoin(command)}` +
|
|
342
365
|
(stderr ? `\n${stderr}` : ''),
|
|
343
366
|
)
|
|
344
367
|
this.name = 'CommandFailed'
|
|
368
|
+
this.binary = binary
|
|
345
369
|
this.argv = argv
|
|
346
370
|
this.result = result
|
|
347
371
|
}
|
|
@@ -375,17 +399,20 @@ export function runChecked(
|
|
|
375
399
|
opts: CommandRunOptions,
|
|
376
400
|
): CommandResult
|
|
377
401
|
export function runChecked<P extends unknown[]>(
|
|
378
|
-
runner: { run(argv: string[], ...opts: P): CommandResult },
|
|
402
|
+
runner: { readonly binary?: string | undefined; run(argv: string[], ...opts: P): CommandResult },
|
|
379
403
|
argv: string[],
|
|
380
404
|
...opts: NoInfer<P>
|
|
381
405
|
): CommandResult
|
|
382
406
|
export function runChecked(
|
|
383
|
-
runner: {
|
|
407
|
+
runner: {
|
|
408
|
+
readonly binary?: string | undefined
|
|
409
|
+
run(argv: string[], ...opts: unknown[]): CommandResult
|
|
410
|
+
},
|
|
384
411
|
argv: string[],
|
|
385
412
|
...opts: unknown[]
|
|
386
413
|
): CommandResult {
|
|
387
414
|
const result = runner.run(argv, ...opts)
|
|
388
|
-
if (result.exitCode !== 0) throw new CommandFailed(argv, result)
|
|
415
|
+
if (result.exitCode !== 0) throw new CommandFailed(argv, result, runner.binary)
|
|
389
416
|
return result
|
|
390
417
|
}
|
|
391
418
|
|
|
@@ -442,41 +469,30 @@ const CHILD_STDIO_CORRUPTED =
|
|
|
442
469
|
process.versions['bun'] !== undefined && process.env['NODE_ENV'] === 'test'
|
|
443
470
|
|
|
444
471
|
/**
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
* on the child's `PATH`.
|
|
472
|
+
* Folds a finished `spawnSync` into a {@link CommandResult}. Exported for
|
|
473
|
+
* tests.
|
|
448
474
|
*/
|
|
449
|
-
function
|
|
450
|
-
const isExecutable = (p: string): boolean => {
|
|
451
|
-
try {
|
|
452
|
-
accessSync(p, constants.X_OK)
|
|
453
|
-
return statSync(p).isFile()
|
|
454
|
-
} catch {
|
|
455
|
-
return false
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
if (binary === '') return false
|
|
459
|
-
if (binary.includes('/')) return isExecutable(resolve(cwd, binary))
|
|
460
|
-
return (path ?? '/usr/bin:/bin')
|
|
461
|
-
.split(delimiter)
|
|
462
|
-
.some((dir) => isExecutable(resolve(cwd, dir, binary)))
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/** Folds a finished `spawnSync` into a {@link CommandResult}. */
|
|
466
|
-
function fromSpawnSync(
|
|
475
|
+
export function fromSpawnSync(
|
|
467
476
|
proc: { status: number | null; signal: NodeJS.Signals | null; error?: Error | undefined },
|
|
468
477
|
stdout: string,
|
|
469
478
|
stderr: string,
|
|
470
479
|
): CommandResult {
|
|
480
|
+
const code = (proc.error as NodeJS.ErrnoException | undefined)?.code
|
|
471
481
|
return {
|
|
472
482
|
exitCode: proc.status ?? 1,
|
|
473
483
|
// Bun reports a failed spawn with `status` undefined, not null.
|
|
474
484
|
...(proc.status == null ? { neverExited: true as const, ...syncStop(proc) } : {}),
|
|
485
|
+
// A real status with a timeout: the child exited, but a process it started
|
|
486
|
+
// held the pipes until the deadline. With the cap: the child passed it and
|
|
487
|
+
// exited before the kill landed.
|
|
488
|
+
...(proc.status != null && (code === 'ETIMEDOUT' || code === 'ENOBUFS')
|
|
489
|
+
? { outputIncomplete: true as const }
|
|
490
|
+
: {}),
|
|
475
491
|
stdout,
|
|
476
492
|
stderr,
|
|
477
493
|
// `proc.error` is set for a failed spawn, a timeout, the output cap, or a
|
|
478
|
-
// failed stdin write
|
|
479
|
-
//
|
|
494
|
+
// failed stdin write. The first three usually leave `status` null; when
|
|
495
|
+
// they do not, `outputIncomplete` is set above. A stdin write error can
|
|
480
496
|
// come with a child that did exit, so it is kept either way.
|
|
481
497
|
...(proc.error ? { error: proc.error.message } : {}),
|
|
482
498
|
}
|
|
@@ -502,7 +518,7 @@ function runViaChildRedirect(
|
|
|
502
518
|
// the same stand-in the pipe path reports for a failed spawn. This also
|
|
503
519
|
// keeps a binary named like an option (`-a`) from reaching `exec` as one,
|
|
504
520
|
// unless an executable named `-a` is on PATH.
|
|
505
|
-
if (
|
|
521
|
+
if (which(binary, { cwd: opts.cwd, env: { PATH: env['PATH'] } }) === undefined) {
|
|
506
522
|
return spawnFailure(`spawnSync ${binary} ENOENT`)
|
|
507
523
|
}
|
|
508
524
|
const dir = mkdtempSync(join(tmpdir(), 'command-seam-'))
|
|
@@ -584,6 +600,7 @@ export function resolveRunner(binary: string, injected?: CommandRunner | undefin
|
|
|
584
600
|
*/
|
|
585
601
|
export function spawnRunner(binary: string): CommandRunner {
|
|
586
602
|
return {
|
|
603
|
+
binary,
|
|
587
604
|
run(argv: string[], opts: CommandRunOptions): CommandResult {
|
|
588
605
|
const timeoutMs = normalizeTimeout(opts.timeoutMs)
|
|
589
606
|
const cwdMessage = cwdError(`spawnSync ${binary}`, binary, argv, opts)
|
package/which.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `which`: find a binary on `PATH` without running it, the way the runners'
|
|
3
|
+
* spawn would find it. For code that picks a tool by what is installed;
|
|
4
|
+
* running `--version` to probe is slow for some CLIs and has side effects
|
|
5
|
+
* (update checks, telemetry) for others.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { accessSync, constants, statSync } from 'node:fs'
|
|
9
|
+
import { delimiter, resolve } from 'node:path'
|
|
10
|
+
|
|
11
|
+
import type { EnvOverrides } from './sync.ts'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Options for {@link which}.
|
|
15
|
+
*
|
|
16
|
+
* @experimental
|
|
17
|
+
*/
|
|
18
|
+
export interface WhichOptions {
|
|
19
|
+
/**
|
|
20
|
+
* The folder a relative path, or a relative `PATH` entry, is resolved
|
|
21
|
+
* against, as a spawn with this `cwd` would. Default `process.cwd()`.
|
|
22
|
+
*/
|
|
23
|
+
cwd?: string | undefined
|
|
24
|
+
/**
|
|
25
|
+
* Variables merged over the parent's environment, as the runners take
|
|
26
|
+
* them. Only `PATH` is read; an `undefined` value unsets it.
|
|
27
|
+
*/
|
|
28
|
+
env?: EnvOverrides | undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The absolute path of the file a spawn of `binary` would run, or
|
|
33
|
+
* `undefined` when there is none. A name with a `/` is taken relative to
|
|
34
|
+
* `cwd`; a bare name is looked up on `PATH`, first match wins. A match must be
|
|
35
|
+
* an executable file. With `PATH` unset, it searches `/usr/bin:/bin`. An
|
|
36
|
+
* empty `PATH` entry (`PATH=''`, `a::b`, a trailing `:`) means `cwd`, as it
|
|
37
|
+
* does for Node's spawn; Bun's spawn skips empty entries, so under Bun such
|
|
38
|
+
* a match may not start. POSIX only: `PATHEXT` is not read.
|
|
39
|
+
*
|
|
40
|
+
* Never throws for a missing binary, an unreadable folder or a bad `cwd`.
|
|
41
|
+
*
|
|
42
|
+
* @param binary - A name looked up on `PATH`, or a path.
|
|
43
|
+
* @param opts - `cwd` and `env`, as the runners take them.
|
|
44
|
+
* @returns The absolute path, or `undefined`.
|
|
45
|
+
* @experimental
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* const harnesses = ['codex', 'claude', 'cursor-agent'].filter((b) => which(b) !== undefined)
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function which(binary: string, opts: WhichOptions = {}): string | undefined {
|
|
53
|
+
const cwd = opts.cwd ?? process.cwd()
|
|
54
|
+
if (binary === '') return undefined
|
|
55
|
+
if (binary.includes('/')) return executable(resolve(cwd, binary))
|
|
56
|
+
const path = opts.env !== undefined && 'PATH' in opts.env ? opts.env['PATH'] : process.env['PATH']
|
|
57
|
+
for (const dir of (path ?? '/usr/bin:/bin').split(delimiter)) {
|
|
58
|
+
const found = executable(resolve(cwd, dir, binary))
|
|
59
|
+
if (found !== undefined) return found
|
|
60
|
+
}
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `p` when it is an executable file, else `undefined`. */
|
|
65
|
+
function executable(p: string): string | undefined {
|
|
66
|
+
try {
|
|
67
|
+
accessSync(p, constants.X_OK)
|
|
68
|
+
return statSync(p).isFile() ? p : undefined
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined
|
|
71
|
+
}
|
|
72
|
+
}
|
package/smoke.mjs
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
// Runs the BUILT package under Node, which no other check does: `bun test`
|
|
2
|
-
// resolves the `bun` export condition to the TypeScript sources, so dist/ is
|
|
3
|
-
// compiled and never executed. A broken emit — an unrewritten `.ts` import, a
|
|
4
|
-
// dependency with no dist/ of its own — passes every other gate and fails on
|
|
5
|
-
// the consumer's machine.
|
|
6
|
-
//
|
|
7
|
-
// Deliberately not a test-runner suite: it must run from the extracted
|
|
8
|
-
// snapshot with only `dist/` and production dependencies present.
|
|
9
|
-
|
|
10
|
-
import { strict as assert } from 'node:assert'
|
|
11
|
-
import { spawnRunner, runChecked, CommandFailed, asAsync, withDefaults } from './dist/index.js'
|
|
12
|
-
import { fakeProcess, fakeRunner } from './dist/testing.js'
|
|
13
|
-
|
|
14
|
-
const runner = spawnRunner('node')
|
|
15
|
-
const ok = runner.run(['-e', "process.stdout.write('hi')"], { cwd: process.cwd() })
|
|
16
|
-
assert.equal(ok.exitCode, 0)
|
|
17
|
-
assert.equal(ok.stdout, 'hi')
|
|
18
|
-
|
|
19
|
-
const bad = runner.run(['-e', "process.stderr.write('boom'); process.exit(3)"], {
|
|
20
|
-
cwd: process.cwd(),
|
|
21
|
-
})
|
|
22
|
-
assert.equal(bad.exitCode, 3)
|
|
23
|
-
assert.match(bad.stderr, /boom/)
|
|
24
|
-
|
|
25
|
-
// `run` never throws; `runChecked` does.
|
|
26
|
-
assert.throws(
|
|
27
|
-
() => runChecked(runner, ['-e', 'process.exit(1)'], { cwd: process.cwd() }),
|
|
28
|
-
(e) => e instanceof CommandFailed && e.argv.length === 2,
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
// detail joins both streams, empty ones dropped — the reason it exists is a
|
|
32
|
-
// command that explains itself on stdout while git reports failure on stderr.
|
|
33
|
-
const both = new CommandFailed(['x'], { exitCode: 1, stdout: 'why\n', stderr: 'what\n' })
|
|
34
|
-
assert.equal(both.detail, 'what\nwhy')
|
|
35
|
-
assert.equal(new CommandFailed(['x'], { exitCode: 1, stdout: 'only\n', stderr: '' }).detail, 'only')
|
|
36
|
-
|
|
37
|
-
// env merges over the parent environment.
|
|
38
|
-
const withEnv = runner.run(['-e', "process.stdout.write(process.env.SMOKE_VAR ?? '')"], {
|
|
39
|
-
cwd: process.cwd(),
|
|
40
|
-
env: { SMOKE_VAR: 'set' },
|
|
41
|
-
})
|
|
42
|
-
assert.equal(withEnv.stdout, 'set')
|
|
43
|
-
|
|
44
|
-
const async = asAsync(runner)
|
|
45
|
-
const viaAsync = await async.run(['-e', "process.stdout.write('async')"], { cwd: process.cwd() })
|
|
46
|
-
assert.equal(viaAsync.stdout, 'async')
|
|
47
|
-
|
|
48
|
-
// An invalid timeoutMs is a caller error.
|
|
49
|
-
assert.throws(() => runner.run(['-e', ''], { cwd: process.cwd(), timeoutMs: 0 }), RangeError)
|
|
50
|
-
|
|
51
|
-
const bound = withDefaults(runner, { cwd: process.cwd() })
|
|
52
|
-
assert.equal(bound.run(['-e', "process.stdout.write('bound')"]).stdout, 'bound')
|
|
53
|
-
|
|
54
|
-
// The /testing entry point, built.
|
|
55
|
-
const fake = fakeRunner({ 'rev-parse HEAD': { stdout: 'abc\n' } })
|
|
56
|
-
assert.equal(runChecked(fake.runner, ['rev-parse', 'HEAD'], { cwd: '/' }).stdout, 'abc\n')
|
|
57
|
-
const proc = fakeProcess()
|
|
58
|
-
proc.kill()
|
|
59
|
-
assert.equal((await proc.exited).reason, 'abort')
|
|
60
|
-
|
|
61
|
-
console.log('command-seam smoke: OK')
|