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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +328 -0
  2. package/CONTRIBUTING.md +58 -0
  3. package/LICENSE +21 -0
  4. package/README.md +741 -2
  5. package/SECURITY.md +23 -0
  6. package/async.ts +466 -0
  7. package/defaults.ts +83 -0
  8. package/dist/async.d.ts +146 -0
  9. package/dist/async.d.ts.map +1 -0
  10. package/dist/async.js +368 -0
  11. package/dist/defaults.d.ts +52 -0
  12. package/dist/defaults.d.ts.map +1 -0
  13. package/dist/defaults.js +51 -0
  14. package/dist/groups.d.ts +155 -0
  15. package/dist/groups.d.ts.map +1 -0
  16. package/dist/groups.js +331 -0
  17. package/dist/index.d.ts +30 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +29 -0
  20. package/dist/process.d.ts +146 -0
  21. package/dist/process.d.ts.map +1 -0
  22. package/dist/process.js +175 -0
  23. package/dist/process.test.d.ts +1 -0
  24. package/dist/process.test.d.ts.map +1 -0
  25. package/dist/process.test.js +1 -0
  26. package/dist/quote.d.ts +18 -0
  27. package/dist/quote.d.ts.map +1 -0
  28. package/dist/quote.js +28 -0
  29. package/dist/result.d.ts +211 -0
  30. package/dist/result.d.ts.map +1 -0
  31. package/dist/result.js +219 -0
  32. package/dist/shell.d.ts +84 -0
  33. package/dist/shell.d.ts.map +1 -0
  34. package/dist/shell.js +83 -0
  35. package/dist/stream.d.ts +90 -0
  36. package/dist/stream.d.ts.map +1 -0
  37. package/dist/stream.js +73 -0
  38. package/dist/sync.d.ts +311 -0
  39. package/dist/sync.d.ts.map +1 -0
  40. package/dist/sync.js +406 -0
  41. package/dist/testing.d.ts +158 -0
  42. package/dist/testing.d.ts.map +1 -0
  43. package/dist/testing.js +173 -0
  44. package/groups.ts +360 -0
  45. package/index.ts +98 -0
  46. package/package.json +94 -3
  47. package/process.ts +270 -0
  48. package/quote.ts +31 -0
  49. package/result.ts +403 -0
  50. package/shell.ts +131 -0
  51. package/smoke.mjs +61 -0
  52. package/stream.ts +134 -0
  53. package/sync.ts +604 -0
  54. package/testing.ts +263 -0
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Live process groups, and forwarding of termination signals to them.
3
+ *
4
+ * A child started with `processGroup: true` leads its own group and no longer
5
+ * receives the terminal's Ctrl-C. When the host is then killed by `SIGINT`,
6
+ * `SIGTERM` or `SIGHUP`, the child and everything it started would be orphaned
7
+ * (ppid 1). The runners register each such group here, and
8
+ * {@link installGroupSignalForwarding} makes the host kill them when it is
9
+ * signalled. A group stays registered until it is empty. Once its run has
10
+ * settled, the host's normal exit leaves what is left of it running, such as a
11
+ * daemon; a forwarded signal still kills it.
12
+ *
13
+ * Once a signal has been forwarded and this module owns the exit, the host is
14
+ * stopping: the runners refuse to start a new group, and
15
+ * {@link forwardedSignal} tells the host's own code to stop.
16
+ *
17
+ * The signal listeners exist only while a group is starting or registered. A
18
+ * JS listener replaces the default "terminate" action and runs only when the
19
+ * event loop turns, so keeping one armed would make a host blocked in a sync
20
+ * spawn ignore a signal it would otherwise die from.
21
+ */
22
+ /**
23
+ * Options for {@link installGroupSignalForwarding}.
24
+ *
25
+ * @experimental
26
+ */
27
+ export interface GroupSignalForwardingOptions {
28
+ /** Signals to forward. Default `SIGINT`, `SIGTERM` and `SIGHUP`. */
29
+ signals?: readonly NodeJS.Signals[] | undefined;
30
+ /**
31
+ * How long the groups have between `SIGTERM` and `SIGKILL`, in ms. Default
32
+ * 1000: long enough for git to remove its lock files.
33
+ */
34
+ graceMs?: number | undefined;
35
+ /**
36
+ * Called to end the process, with `128 + signal number`. Default: set
37
+ * `process.exitCode` and let the event loop drain, so piped stdout is not
38
+ * truncated, then `process.exit` after 500 ms if handles still hold the loop.
39
+ * Tests replace it.
40
+ */
41
+ exit?: ((code: number) => void) | undefined;
42
+ }
43
+ /**
44
+ * Register a group leader. The returned function unregisters it; call it once
45
+ * the group is gone so a reused pid is never signalled. Registering the first
46
+ * group arms the signal listeners (when forwarding is installed); removing the
47
+ * last disarms them.
48
+ *
49
+ * @internal
50
+ */
51
+ export declare function trackGroup(pid: number): () => void;
52
+ /**
53
+ * Start a group leader with `start` and register it. The listeners are armed
54
+ * before `start` runs: the child can print, and the host be signalled, before
55
+ * `spawn` returns, and a signal that comes before the listeners would kill the
56
+ * host by default and orphan the group. `untrack` is `undefined` when the
57
+ * child got no pid.
58
+ *
59
+ * @internal
60
+ */
61
+ export declare function spawnTrackedGroup<C extends {
62
+ readonly pid?: number | undefined;
63
+ }>(start: () => C): {
64
+ child: C;
65
+ untrack: (() => void) | undefined;
66
+ };
67
+ /**
68
+ * Mark the run of the group led by `pid` as settled: the host's exit no longer
69
+ * kills what is left of the group. A forwarded signal still does. A runner
70
+ * does not call this for a group it is stopping itself.
71
+ *
72
+ * @internal
73
+ */
74
+ export declare function settleGroup(pid: number): void;
75
+ /**
76
+ * Why a new group must not start, or `undefined` when it may. The runners call
77
+ * this before they spawn with `processGroup: true`.
78
+ *
79
+ * @internal
80
+ */
81
+ export declare function groupStartRefusal(): string | undefined;
82
+ /**
83
+ * Run `release` once the group led by `pid` has no member: now if it is
84
+ * already empty, else polled on an unref'd timer.
85
+ *
86
+ * Unref'd on purpose: a ref'd poll would keep the host alive for as long as a
87
+ * member the group left behind. On a Bun version where an unref'd timer does
88
+ * not fire (see `async.ts`), the entry stays registered. The listeners then
89
+ * stay armed and a signal still kills that member, which is the safe side.
90
+ *
91
+ * @internal
92
+ */
93
+ export declare function releaseWhenEmpty(pid: number, release: () => void): void;
94
+ /**
95
+ * True while the group led by `pid` still has a member.
96
+ *
97
+ * @internal
98
+ */
99
+ export declare function groupExists(pid: number): boolean;
100
+ /**
101
+ * The pids of the live process groups started with `processGroup: true`.
102
+ *
103
+ * @experimental
104
+ */
105
+ export declare function liveProcessGroups(): number[];
106
+ /**
107
+ * The signal the host was sent and forwarded to its groups, or `undefined`
108
+ * while none has been. Set only when forwarding owns the exit, that is, the
109
+ * host has no listener of its own for that signal. Once set, the host is
110
+ * stopping: a loop that runs one command after another should check it and
111
+ * stop, and the runners refuse to start a new `processGroup` run. The
112
+ * uninstall function clears it.
113
+ *
114
+ * @experimental
115
+ */
116
+ export declare function forwardedSignal(): NodeJS.Signals | undefined;
117
+ /**
118
+ * The exit code for {@link forwardedSignal}: `128 + signal number`, or
119
+ * `undefined` while no signal has been forwarded. A host that sets its own
120
+ * exit code after its work returns keeps this one over it:
121
+ * `process.exitCode = forwardedExitCode() ?? code`.
122
+ *
123
+ * @experimental
124
+ */
125
+ export declare function forwardedExitCode(): number | undefined;
126
+ /**
127
+ * Enable forwarding of termination signals to live process groups.
128
+ *
129
+ * While at least one group started with `processGroup: true` is live, `SIGINT`,
130
+ * `SIGTERM` and `SIGHUP` send `SIGTERM` to every registered group, then
131
+ * `SIGKILL` to those still registered after `graceMs`, then end the process
132
+ * with `128 + signal number`, the shell convention. A second signal during the
133
+ * grace, or any later one, skips straight to `SIGKILL` and the exit. From the
134
+ * first signal on, {@link forwardedSignal} returns it and the runners refuse
135
+ * to start a new `processGroup` run, until the uninstall function resets
136
+ * that. A `process` `exit` handler, armed with the listeners, also `SIGKILL`s
137
+ * every group whose run is still in flight, so a host that exits first does
138
+ * not leave one behind. A member that a settled run left behind, such as a
139
+ * daemon, outlives the host's exit; a forwarded signal still kills it.
140
+ *
141
+ * The listeners are attached just before the first group starts and removed
142
+ * when the last one goes, so a host with no live group keeps each signal's default
143
+ * action. If the host has its own listener for the signal (`process.once`
144
+ * counts too), the groups still
145
+ * get `SIGTERM` at once and `SIGKILL` after `graceMs` (or on `exit`, if
146
+ * sooner and the run is in flight), but that listener keeps the exit, and later runs are not refused.
147
+ * Calling it again while installed is a no-op returning the same
148
+ * uninstall function. `SIGKILL` of the host cannot be caught and still orphans
149
+ * its groups. POSIX only; a no-op on Windows.
150
+ *
151
+ * @returns A function that removes the forwarding.
152
+ * @experimental
153
+ */
154
+ export declare function installGroupSignalForwarding(opts?: GroupSignalForwardingOptions): () => void;
155
+ //# sourceMappingURL=groups.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"groups.d.ts","sourceRoot":"","sources":["../groups.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAWH;;;;GAIG;AACH,MAAM,WAAW,4BAA4B;IAC3C,oEAAoE;IACpE,OAAO,CAAC,EAAE,SAAS,MAAM,CAAC,OAAO,EAAE,GAAG,SAAS,CAAA;IAC/C;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC5B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;CAC5C;AAyBD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,IAAI,CAOlD;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS;IAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,EAC/E,KAAK,EAAE,MAAM,CAAC,GACb;IAAE,KAAK,EAAE,CAAC,CAAC;IAAC,OAAO,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAA;CAAE,CAUjD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAG7C;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,SAAS,CAGtD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAWvE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOhD;AAYD;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,EAAE,CAE5C;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAAC,OAAO,GAAG,SAAS,CAE5D;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,SAAS,CAEtD;AA4DD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,GAAE,4BAAiC,GAAG,MAAM,IAAI,CAUhG"}
package/dist/groups.js ADDED
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Live process groups, and forwarding of termination signals to them.
3
+ *
4
+ * A child started with `processGroup: true` leads its own group and no longer
5
+ * receives the terminal's Ctrl-C. When the host is then killed by `SIGINT`,
6
+ * `SIGTERM` or `SIGHUP`, the child and everything it started would be orphaned
7
+ * (ppid 1). The runners register each such group here, and
8
+ * {@link installGroupSignalForwarding} makes the host kill them when it is
9
+ * signalled. A group stays registered until it is empty. Once its run has
10
+ * settled, the host's normal exit leaves what is left of it running, such as a
11
+ * daemon; a forwarded signal still kills it.
12
+ *
13
+ * Once a signal has been forwarded and this module owns the exit, the host is
14
+ * stopping: the runners refuse to start a new group, and
15
+ * {@link forwardedSignal} tells the host's own code to stop.
16
+ *
17
+ * The signal listeners exist only while a group is starting or registered. A
18
+ * JS listener replaces the default "terminate" action and runs only when the
19
+ * event loop turns, so keeping one armed would make a host blocked in a sync
20
+ * spawn ignore a signal it would otherwise die from.
21
+ */
22
+ import { constants } from 'node:os';
23
+ const osSignals = constants.signals;
24
+ /** Live group leaders, in start order, and whether each one's run has settled. */
25
+ const live = new Map();
26
+ /** Group spawns in progress: the listeners are armed before their pid is known. */
27
+ let spawning = 0;
28
+ const DEFAULT_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
29
+ const POLL_MS = 25;
30
+ /** Set while forwarding is installed. */
31
+ let config;
32
+ /** Set while the listeners are attached: forwarding installed and a group starting or live. */
33
+ let armed;
34
+ /**
35
+ * The first signal forwarded since install whose exit this module owns (the
36
+ * host has no listener of its own for it). Cleared only by uninstall.
37
+ */
38
+ let forwarded;
39
+ /** Set while a graceful kill is waiting out its grace. */
40
+ let killing = false;
41
+ /** A repeat signal already sent `SIGKILL`: the graceful kill stops waiting. */
42
+ let escalated = false;
43
+ /**
44
+ * Register a group leader. The returned function unregisters it; call it once
45
+ * the group is gone so a reused pid is never signalled. Registering the first
46
+ * group arms the signal listeners (when forwarding is installed); removing the
47
+ * last disarms them.
48
+ *
49
+ * @internal
50
+ */
51
+ export function trackGroup(pid) {
52
+ live.set(pid, { settled: false });
53
+ arm();
54
+ return () => {
55
+ live.delete(pid);
56
+ disarmIfIdle();
57
+ };
58
+ }
59
+ /**
60
+ * Start a group leader with `start` and register it. The listeners are armed
61
+ * before `start` runs: the child can print, and the host be signalled, before
62
+ * `spawn` returns, and a signal that comes before the listeners would kill the
63
+ * host by default and orphan the group. `untrack` is `undefined` when the
64
+ * child got no pid.
65
+ *
66
+ * @internal
67
+ */
68
+ export function spawnTrackedGroup(start) {
69
+ spawning += 1;
70
+ arm();
71
+ try {
72
+ const child = start();
73
+ return { child, untrack: child.pid === undefined ? undefined : trackGroup(child.pid) };
74
+ }
75
+ finally {
76
+ spawning -= 1;
77
+ disarmIfIdle();
78
+ }
79
+ }
80
+ /**
81
+ * Mark the run of the group led by `pid` as settled: the host's exit no longer
82
+ * kills what is left of the group. A forwarded signal still does. A runner
83
+ * does not call this for a group it is stopping itself.
84
+ *
85
+ * @internal
86
+ */
87
+ export function settleGroup(pid) {
88
+ const entry = live.get(pid);
89
+ if (entry !== undefined)
90
+ entry.settled = true;
91
+ }
92
+ /**
93
+ * Why a new group must not start, or `undefined` when it may. The runners call
94
+ * this before they spawn with `processGroup: true`.
95
+ *
96
+ * @internal
97
+ */
98
+ export function groupStartRefusal() {
99
+ if (forwarded === undefined)
100
+ return undefined;
101
+ return `not started: the host got ${forwarded} and is stopping`;
102
+ }
103
+ /**
104
+ * Run `release` once the group led by `pid` has no member: now if it is
105
+ * already empty, else polled on an unref'd timer.
106
+ *
107
+ * Unref'd on purpose: a ref'd poll would keep the host alive for as long as a
108
+ * member the group left behind. On a Bun version where an unref'd timer does
109
+ * not fire (see `async.ts`), the entry stays registered. The listeners then
110
+ * stay armed and a signal still kills that member, which is the safe side.
111
+ *
112
+ * @internal
113
+ */
114
+ export function releaseWhenEmpty(pid, release) {
115
+ if (!groupExists(pid)) {
116
+ release();
117
+ return;
118
+ }
119
+ const timer = setInterval(() => {
120
+ if (groupExists(pid))
121
+ return;
122
+ clearInterval(timer);
123
+ release();
124
+ }, 250);
125
+ timer.unref();
126
+ }
127
+ /**
128
+ * True while the group led by `pid` still has a member.
129
+ *
130
+ * @internal
131
+ */
132
+ export function groupExists(pid) {
133
+ try {
134
+ process.kill(-pid, 0);
135
+ return true;
136
+ }
137
+ catch {
138
+ return false;
139
+ }
140
+ }
141
+ /** Send `sig` to the group led by `pid`, only while it is registered. */
142
+ function signalGroup(pid, sig) {
143
+ if (!live.has(pid))
144
+ return;
145
+ try {
146
+ process.kill(-pid, sig);
147
+ }
148
+ catch {
149
+ // Already gone.
150
+ }
151
+ }
152
+ /**
153
+ * The pids of the live process groups started with `processGroup: true`.
154
+ *
155
+ * @experimental
156
+ */
157
+ export function liveProcessGroups() {
158
+ return [...live.keys()];
159
+ }
160
+ /**
161
+ * The signal the host was sent and forwarded to its groups, or `undefined`
162
+ * while none has been. Set only when forwarding owns the exit, that is, the
163
+ * host has no listener of its own for that signal. Once set, the host is
164
+ * stopping: a loop that runs one command after another should check it and
165
+ * stop, and the runners refuse to start a new `processGroup` run. The
166
+ * uninstall function clears it.
167
+ *
168
+ * @experimental
169
+ */
170
+ export function forwardedSignal() {
171
+ return forwarded;
172
+ }
173
+ /**
174
+ * The exit code for {@link forwardedSignal}: `128 + signal number`, or
175
+ * `undefined` while no signal has been forwarded. A host that sets its own
176
+ * exit code after its work returns keeps this one over it:
177
+ * `process.exitCode = forwardedExitCode() ?? code`.
178
+ *
179
+ * @experimental
180
+ */
181
+ export function forwardedExitCode() {
182
+ return forwarded === undefined ? undefined : 128 + signalNumber(forwarded);
183
+ }
184
+ function arm() {
185
+ if (config === undefined || armed !== undefined || (live.size === 0 && spawning === 0))
186
+ return;
187
+ const { signals, graceMs, exit } = config;
188
+ const onExit = () => {
189
+ // The host is going away: nothing async runs after this, so end every group
190
+ // whose run is still in flight now. A settled run's leftovers stay.
191
+ for (const [pid, { settled }] of live)
192
+ if (!settled)
193
+ signalGroup(pid, 'SIGKILL');
194
+ };
195
+ const handlers = signals.map((sig) => {
196
+ const handler = () => {
197
+ const code = 128 + signalNumber(sig);
198
+ if (forwarded !== undefined || killing) {
199
+ // A repeat signal: stop waiting.
200
+ escalated = true;
201
+ for (const pid of live.keys())
202
+ signalGroup(pid, 'SIGKILL');
203
+ if (!othersListen(sig))
204
+ exit(code);
205
+ return;
206
+ }
207
+ // A host listener owns the exit and may keep running (a REPL cancelling
208
+ // a job, a daemon reloading on SIGHUP): kill the groups, but do not
209
+ // refuse later runs.
210
+ const othersOwnExit = othersListen(sig);
211
+ if (!othersOwnExit)
212
+ forwarded = sig;
213
+ killing = true;
214
+ escalated = false;
215
+ void killAll(graceMs).then(() => {
216
+ killing = false;
217
+ if (!escalated && !othersOwnExit)
218
+ exit(code);
219
+ });
220
+ };
221
+ // First in line: Node and Bun drop a host's `process.once` wrapper before
222
+ // calling it, so a handler that ran later would not count that listener.
223
+ process.prependListener(sig, handler);
224
+ return [sig, handler];
225
+ });
226
+ process.on('exit', onExit);
227
+ armed = {
228
+ off: () => {
229
+ for (const [sig, handler] of handlers)
230
+ process.off(sig, handler);
231
+ process.off('exit', onExit);
232
+ },
233
+ };
234
+ }
235
+ function disarm() {
236
+ armed?.off();
237
+ armed = undefined;
238
+ }
239
+ function disarmIfIdle() {
240
+ if (live.size === 0 && spawning === 0)
241
+ disarm();
242
+ }
243
+ /** Another listener than ours is attached for `sig`: it owns the exit. */
244
+ function othersListen(sig) {
245
+ return process.listenerCount(sig) > 1;
246
+ }
247
+ /**
248
+ * Enable forwarding of termination signals to live process groups.
249
+ *
250
+ * While at least one group started with `processGroup: true` is live, `SIGINT`,
251
+ * `SIGTERM` and `SIGHUP` send `SIGTERM` to every registered group, then
252
+ * `SIGKILL` to those still registered after `graceMs`, then end the process
253
+ * with `128 + signal number`, the shell convention. A second signal during the
254
+ * grace, or any later one, skips straight to `SIGKILL` and the exit. From the
255
+ * first signal on, {@link forwardedSignal} returns it and the runners refuse
256
+ * to start a new `processGroup` run, until the uninstall function resets
257
+ * that. A `process` `exit` handler, armed with the listeners, also `SIGKILL`s
258
+ * every group whose run is still in flight, so a host that exits first does
259
+ * not leave one behind. A member that a settled run left behind, such as a
260
+ * daemon, outlives the host's exit; a forwarded signal still kills it.
261
+ *
262
+ * The listeners are attached just before the first group starts and removed
263
+ * when the last one goes, so a host with no live group keeps each signal's default
264
+ * action. If the host has its own listener for the signal (`process.once`
265
+ * counts too), the groups still
266
+ * get `SIGTERM` at once and `SIGKILL` after `graceMs` (or on `exit`, if
267
+ * sooner and the run is in flight), but that listener keeps the exit, and later runs are not refused.
268
+ * Calling it again while installed is a no-op returning the same
269
+ * uninstall function. `SIGKILL` of the host cannot be caught and still orphans
270
+ * its groups. POSIX only; a no-op on Windows.
271
+ *
272
+ * @returns A function that removes the forwarding.
273
+ * @experimental
274
+ */
275
+ export function installGroupSignalForwarding(opts = {}) {
276
+ if (process.platform === 'win32')
277
+ return () => { };
278
+ if (config !== undefined)
279
+ return uninstall;
280
+ config = {
281
+ signals: opts.signals ?? DEFAULT_SIGNALS,
282
+ graceMs: opts.graceMs ?? 1000,
283
+ exit: opts.exit ?? defaultExit,
284
+ };
285
+ arm();
286
+ return uninstall;
287
+ }
288
+ function uninstall() {
289
+ disarm();
290
+ config = undefined;
291
+ forwarded = undefined;
292
+ killing = false;
293
+ escalated = false;
294
+ }
295
+ /**
296
+ * Set the exit code and let output drain; force the exit after 500 ms. Not
297
+ * unref'd: an unref'd timer does not reliably fire on every Bun version, and
298
+ * holding the loop for 500 ms after a signal is cheap.
299
+ */
300
+ function defaultExit(code) {
301
+ process.exitCode = code;
302
+ setTimeout(() => process.exit(code), 500);
303
+ }
304
+ /**
305
+ * `SIGTERM` every registered group, wait up to `graceMs` for them to go, then
306
+ * `SIGKILL` those still registered. Stops early when `escalated` says a repeat
307
+ * signal already did the kill.
308
+ */
309
+ async function killAll(graceMs) {
310
+ // Drop entries whose group is already empty, so a stale pid is never signalled.
311
+ for (const pid of liveProcessGroups())
312
+ if (!groupExists(pid))
313
+ live.delete(pid);
314
+ for (const pid of liveProcessGroups())
315
+ signalGroup(pid, 'SIGTERM');
316
+ const deadline = performance.now() + graceMs;
317
+ while (!escalated && performance.now() < deadline) {
318
+ if (!liveProcessGroups().some(groupExists))
319
+ return;
320
+ await new Promise((res) => setTimeout(res, POLL_MS));
321
+ }
322
+ if (escalated)
323
+ return;
324
+ for (const pid of liveProcessGroups())
325
+ if (groupExists(pid))
326
+ signalGroup(pid, 'SIGKILL');
327
+ }
328
+ function signalNumber(sig) {
329
+ const n = osSignals[sig];
330
+ return typeof n === 'number' ? n : 15;
331
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `@osdlc/command-seam`: run external commands through a small interface
3
+ * that tests can replace.
4
+ *
5
+ * A runner is an object with a `run` method. Code that needs a command takes
6
+ * a runner as a parameter. Production passes a real one; tests pass a fake.
7
+ *
8
+ * - `spawnRunner` / `spawnRunnerAsync` capture a finished run's output
9
+ * ({@link CommandResult}).
10
+ * - `runShell` / `shellRunner` run a shell string through `sh -c --`.
11
+ * - `streamRunner` runs with inherited stdio, so output appears live.
12
+ * - `managedProcessRunner` starts a process and returns a handle to it
13
+ * without waiting for it to finish. `stopProcess` stops it with a grace
14
+ * period.
15
+ * - `withDefaults` binds default options to any of them.
16
+ *
17
+ * The capture runners share {@link CommandResult}, {@link CommandRunOptions}
18
+ * and {@link CommandFailed}, so one `instanceof CommandFailed` check covers
19
+ * the sync and async forms.
20
+ */
21
+ export { catchFailed, CommandFailed, mapFailed, resolveRunner, runChecked, spawnRunner, type CommandResult, type CommandRunner, type CommandRunOptions, type EnvOverrides, type NeverExitedReason, } from './sync.ts';
22
+ export { asAsync, catchFailedAsync, mapFailedAsync, resolveRunnerAsync, runCheckedAsync, spawnRunnerAsync, type AsyncCommandRunner, type AsyncCommandRunOptions, } from './async.ts';
23
+ export { failureText, firstLine, parseJsonResult, ran, runJson, runJsonAsync, stderrOrError, type FailureTextOptions, type JsonRun, type RunJsonOptions, } from './result.ts';
24
+ export { shellJoin, shellQuote } from './quote.ts';
25
+ export { runShell, runShellAsync, shellRunner, shellRunnerAsync, type AsyncShellRunner, type AsyncShellRunOptions, type ShellRunner, type ShellPositionals, type ShellRunOptions, } from './shell.ts';
26
+ export { streamRunner, type StreamRunner, type StreamRunOptions, type StreamResult, } from './stream.ts';
27
+ export { managedProcessRunner, stopProcess, type ManagedProcessRunner, type ManagedProcessHandle, type ManagedProcessRunOptions, type ManagedProcessExit, } from './process.ts';
28
+ export { forwardedExitCode, forwardedSignal, installGroupSignalForwarding, liveProcessGroups, type GroupSignalForwardingOptions, } from './groups.ts';
29
+ export { withDefaults, type DefaultedRunner } from './defaults.ts';
30
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;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"}
package/dist/index.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `@osdlc/command-seam`: run external commands through a small interface
3
+ * that tests can replace.
4
+ *
5
+ * A runner is an object with a `run` method. Code that needs a command takes
6
+ * a runner as a parameter. Production passes a real one; tests pass a fake.
7
+ *
8
+ * - `spawnRunner` / `spawnRunnerAsync` capture a finished run's output
9
+ * ({@link CommandResult}).
10
+ * - `runShell` / `shellRunner` run a shell string through `sh -c --`.
11
+ * - `streamRunner` runs with inherited stdio, so output appears live.
12
+ * - `managedProcessRunner` starts a process and returns a handle to it
13
+ * without waiting for it to finish. `stopProcess` stops it with a grace
14
+ * period.
15
+ * - `withDefaults` binds default options to any of them.
16
+ *
17
+ * The capture runners share {@link CommandResult}, {@link CommandRunOptions}
18
+ * and {@link CommandFailed}, so one `instanceof CommandFailed` check covers
19
+ * the sync and async forms.
20
+ */
21
+ export { catchFailed, CommandFailed, mapFailed, resolveRunner, runChecked, spawnRunner, } from "./sync.js";
22
+ export { asAsync, catchFailedAsync, mapFailedAsync, resolveRunnerAsync, runCheckedAsync, spawnRunnerAsync, } from "./async.js";
23
+ export { failureText, firstLine, parseJsonResult, ran, runJson, runJsonAsync, stderrOrError, } from "./result.js";
24
+ export { shellJoin, shellQuote } from "./quote.js";
25
+ export { runShell, runShellAsync, shellRunner, shellRunnerAsync, } from "./shell.js";
26
+ export { streamRunner, } from "./stream.js";
27
+ export { managedProcessRunner, stopProcess, } from "./process.js";
28
+ export { forwardedExitCode, forwardedSignal, installGroupSignalForwarding, liveProcessGroups, } from "./groups.js";
29
+ export { withDefaults } from "./defaults.js";