@mypolis.eu/command 0.1.0 → 0.2.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/README.md +16 -29
- package/dist/index.d.ts +16 -10
- package/dist/index.js +222 -84
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -2,46 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
Lazy, terminal-first commands without a shell. ESM JavaScript with TypeScript declarations, for Node.js 24+. No runtime dependencies.
|
|
4
4
|
|
|
5
|
-
```sh
|
|
6
|
-
npm install @mypolis.eu/command
|
|
7
|
-
```
|
|
8
|
-
|
|
9
5
|
```js
|
|
10
|
-
import {$, CommandError} from '@mypolis.eu/command';
|
|
6
|
+
import {$, createRunner, CommandError} from '@mypolis.eu/command';
|
|
11
7
|
|
|
12
|
-
//
|
|
13
|
-
await $`git status`;
|
|
14
|
-
|
|
15
|
-
// Optional timeout, configured before execution; no timeout by default.
|
|
16
|
-
await $`git fetch`.timeout(30_000);
|
|
17
|
-
|
|
18
|
-
// Capture raw UTF-8 stdout without trimming whitespace or trailing newlines.
|
|
8
|
+
await $`git status`; // inherited terminal, no timeout
|
|
19
9
|
const branch = await $`git branch --show-current`.text();
|
|
20
|
-
const
|
|
10
|
+
const result = await $`git status --short`.output();
|
|
11
|
+
await $`git fetch`.timeout(30_000);
|
|
21
12
|
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
await
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
const run = createRunner({timeoutMs: 30_000, maxBufferBytes: 4 * 1024 * 1024});
|
|
15
|
+
await run`some-command`.signal(controller.signal).output();
|
|
16
|
+
await run`interactive-command`.timeout(null); // disable an inherited timeout
|
|
25
17
|
```
|
|
26
18
|
|
|
27
|
-
|
|
19
|
+
Awaiting a command streams stdout and stderr to the terminal. `.text()` captures raw stdout and stderr for failures; `.output()` returns both captured streams. Captured output has a 4 MiB combined byte budget by default. Use `.maxBuffer(bytes)` or `createRunner({maxBufferBytes})` to change it. Output is never implicitly trimmed.
|
|
28
20
|
|
|
29
|
-
|
|
21
|
+
`.timeout(ms)` accepts a positive finite millisecond value; `$` has no default timeout. `.signal(signal)` accepts an `AbortSignal`; aborting terminates the command with SIGTERM and escalates to SIGKILL after 500 ms. A pre-aborted signal does not spawn the command, and its reason is available as `CommandError.cause`. `.processGroup()` opts into a separate Linux/macOS process group so timeout, abort, overflow and forwarded signals target the command's group. Group mode is false by default; configured runners may set it true, and `.processGroup(false)` overrides it. Normal execution settles when the immediate child closes. Controlled group termination waits for the escalation grace period, then signals the original process group ID before settling. This is best effort: descendants that detach into another group or session can escape.
|
|
30
22
|
|
|
31
|
-
|
|
23
|
+
Construction is lazy and awaiting, `.then()`, `.text()` or `.output()` starts one shared execution. Capture mode and options must be selected before starting. Repeated use returns the original result or failure. `.timeout(null)` explicitly disables a configured runner timeout.
|
|
32
24
|
|
|
33
|
-
|
|
34
|
-
const count = 5;
|
|
35
|
-
await $`git log ${`--max-count=${count}`}`;
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Quoting, pipes, redirects, globs, environment expansion and other shell syntax in literal text are rejected. Characters in interpolated values are passed literally, not evaluated by a shell. This prevents shell expansion, not interpretation of options by the executable: use `--` where supported when passing untrusted filenames. Choose the executable and its arguments appropriately.
|
|
39
|
-
|
|
40
|
-
## Lazy execution and reuse
|
|
25
|
+
## Arguments, not Bash
|
|
41
26
|
|
|
42
|
-
|
|
27
|
+
Literal text is split on whitespace. Interpolate strings or numbers for whole arguments; arrays expand into multiple arguments. Quoting, pipes, redirects, globs, environment expansion and other shell syntax in literal text are rejected. Interpolated values are passed literally. Use `--` where supported when passing untrusted filenames.
|
|
43
28
|
|
|
44
|
-
## Failures
|
|
29
|
+
## Failures
|
|
45
30
|
|
|
46
31
|
```js
|
|
47
32
|
try {
|
|
@@ -53,6 +38,8 @@ try {
|
|
|
53
38
|
}
|
|
54
39
|
```
|
|
55
40
|
|
|
41
|
+
`CommandError.reason` is one of `spawn`, `exit`, `signal`, `timeout`, `overflow`, or `abort`. Captured failures retain only the bounded output prefix; overflow is an explicit failure, not successful truncation.
|
|
42
|
+
|
|
56
43
|
## License
|
|
57
44
|
|
|
58
45
|
MIT © MyPolis
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,12 @@ type Output = {
|
|
|
4
4
|
stdout: string;
|
|
5
5
|
stderr: string;
|
|
6
6
|
};
|
|
7
|
-
type FailureReason = "spawn" | "exit" | "signal" | "timeout" | "overflow";
|
|
7
|
+
type FailureReason = "spawn" | "exit" | "signal" | "timeout" | "overflow" | "abort";
|
|
8
|
+
export type RunnerOptions = {
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
maxBufferBytes?: number;
|
|
11
|
+
processGroup?: boolean;
|
|
12
|
+
};
|
|
8
13
|
export declare class CommandError extends Error {
|
|
9
14
|
readonly reason: FailureReason;
|
|
10
15
|
readonly exitCode: number | null;
|
|
@@ -18,19 +23,20 @@ export declare class CommandError extends Error {
|
|
|
18
23
|
signal: NodeJS.Signals | null;
|
|
19
24
|
code?: string;
|
|
20
25
|
output?: Output;
|
|
26
|
+
cause?: unknown;
|
|
21
27
|
});
|
|
22
28
|
}
|
|
23
29
|
type Command = PromiseLike<undefined> & {
|
|
24
|
-
timeout(ms: number): Command;
|
|
30
|
+
timeout(ms: number | null): Command;
|
|
31
|
+
maxBuffer(bytes: number): Command;
|
|
32
|
+
processGroup(enabled?: boolean): Command;
|
|
33
|
+
signal(signal: AbortSignal): Command;
|
|
25
34
|
text(): Promise<string>;
|
|
26
35
|
output(): Promise<Output>;
|
|
27
36
|
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
* Timeout/overflow sends SIGTERM, then SIGKILL after 500 ms; settlement always waits for close.
|
|
34
|
-
*/
|
|
35
|
-
export declare function $(strings: TemplateStringsArray, ...values: Values): Command;
|
|
37
|
+
type Runner = (strings: TemplateStringsArray, ...values: Values) => Command;
|
|
38
|
+
/** Creates a command tag with reusable timeout, capture-budget and process-group defaults. */
|
|
39
|
+
export declare function createRunner(defaults?: RunnerOptions): Runner;
|
|
40
|
+
/** Lazy, shell-free command tag. Awaiting streams to the terminal; text/output capture raw UTF-8. */
|
|
41
|
+
export declare const $: Runner;
|
|
36
42
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
|
|
2
|
+
import { addAbortListener } from "node:events";
|
|
3
|
+
const DEFAULT_MAX_BUFFER_BYTES = 4 * 1024 * 1024;
|
|
3
4
|
const TERMINATION_GRACE_MS = 500;
|
|
5
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
4
6
|
export class CommandError extends Error {
|
|
5
7
|
constructor(message, details) {
|
|
6
|
-
super(message);
|
|
8
|
+
super(message, details.cause === undefined ? undefined : { cause: details.cause });
|
|
7
9
|
this.name = "CommandError";
|
|
8
10
|
this.reason = details.reason;
|
|
9
11
|
this.exitCode = details.exitCode;
|
|
@@ -13,58 +15,106 @@ export class CommandError extends Error {
|
|
|
13
15
|
this.stderr = details.output?.stderr;
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
18
|
+
/** Creates a command tag with reusable timeout, capture-budget and process-group defaults. */
|
|
19
|
+
export function createRunner(defaults = {}) {
|
|
20
|
+
if (defaults === null || typeof defaults !== "object" || Array.isArray(defaults))
|
|
21
|
+
throw new TypeError("Runner options must be an object.");
|
|
22
|
+
for (const key of Object.keys(defaults)) {
|
|
23
|
+
if (key !== "timeoutMs" && key !== "maxBufferBytes" && key !== "processGroup")
|
|
24
|
+
throw new TypeError(`Unknown runner option: ${key}.`);
|
|
25
|
+
}
|
|
26
|
+
if (defaults.timeoutMs !== undefined &&
|
|
27
|
+
(!Number.isFinite(defaults.timeoutMs) || defaults.timeoutMs <= 0 || defaults.timeoutMs > MAX_TIMEOUT_MS)) {
|
|
28
|
+
throw new TypeError("Timeout must be finite, positive and at most 2147483647 ms.");
|
|
29
|
+
}
|
|
30
|
+
if (defaults.maxBufferBytes !== undefined &&
|
|
31
|
+
(!Number.isSafeInteger(defaults.maxBufferBytes) || defaults.maxBufferBytes <= 0)) {
|
|
32
|
+
throw new TypeError("Maximum buffer must be a positive safe integer.");
|
|
33
|
+
}
|
|
34
|
+
if (defaults.processGroup !== undefined && typeof defaults.processGroup !== "boolean")
|
|
35
|
+
throw new TypeError("Process group must be a boolean.");
|
|
36
|
+
if (defaults.processGroup && process.platform !== "darwin" && process.platform !== "linux")
|
|
37
|
+
throw new Error("Process groups are only supported on Linux and macOS.");
|
|
38
|
+
const runnerDefaults = {
|
|
39
|
+
timeoutMs: defaults.timeoutMs,
|
|
40
|
+
maxBufferBytes: defaults.maxBufferBytes,
|
|
41
|
+
processGroup: defaults.processGroup === true
|
|
35
42
|
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
43
|
+
return (strings, ...values) => {
|
|
44
|
+
let execution;
|
|
45
|
+
const options = { ...runnerDefaults };
|
|
46
|
+
const start = (capture) => {
|
|
47
|
+
if (execution) {
|
|
48
|
+
if (options.capture !== capture)
|
|
49
|
+
throw new Error("Command capture mode cannot change after execution starts.");
|
|
50
|
+
return execution;
|
|
51
|
+
}
|
|
52
|
+
options.capture = capture;
|
|
53
|
+
execution = execute(options, strings, values);
|
|
54
|
+
return execution;
|
|
55
|
+
};
|
|
56
|
+
const command = {
|
|
57
|
+
// biome-ignore lint/suspicious/noThenProperty: Awaiting this lazy command intentionally starts shared execution.
|
|
58
|
+
then(onfulfilled, onrejected) {
|
|
59
|
+
return start(false)
|
|
60
|
+
.then(() => undefined)
|
|
61
|
+
.then(onfulfilled, onrejected);
|
|
62
|
+
},
|
|
63
|
+
timeout(ms) {
|
|
64
|
+
if (execution)
|
|
65
|
+
throw new Error("Command timeout cannot change after execution starts.");
|
|
66
|
+
if (ms === null)
|
|
67
|
+
delete options.timeoutMs;
|
|
68
|
+
else {
|
|
69
|
+
if (!Number.isFinite(ms) || ms <= 0 || ms > MAX_TIMEOUT_MS)
|
|
70
|
+
throw new TypeError("Timeout must be finite, positive and at most 2147483647 ms.");
|
|
71
|
+
options.timeoutMs = ms;
|
|
72
|
+
}
|
|
73
|
+
return command;
|
|
74
|
+
},
|
|
75
|
+
maxBuffer(bytes) {
|
|
76
|
+
if (execution)
|
|
77
|
+
throw new Error("Command maximum buffer cannot change after execution starts.");
|
|
78
|
+
if (!Number.isSafeInteger(bytes) || bytes <= 0)
|
|
79
|
+
throw new TypeError("Maximum buffer must be a positive safe integer.");
|
|
80
|
+
options.maxBufferBytes = bytes;
|
|
81
|
+
return command;
|
|
82
|
+
},
|
|
83
|
+
processGroup(enabled = true) {
|
|
84
|
+
if (execution)
|
|
85
|
+
throw new Error("Command process group cannot change after execution starts.");
|
|
86
|
+
if (typeof enabled !== "boolean")
|
|
87
|
+
throw new TypeError("Process group must be a boolean.");
|
|
88
|
+
if (enabled && process.platform !== "darwin" && process.platform !== "linux")
|
|
89
|
+
throw new Error("Process groups are only supported on Linux and macOS.");
|
|
90
|
+
options.processGroup = enabled;
|
|
91
|
+
return command;
|
|
92
|
+
},
|
|
93
|
+
signal(signal) {
|
|
94
|
+
if (execution)
|
|
95
|
+
throw new Error("Command signal cannot change after execution starts.");
|
|
96
|
+
if (!(signal instanceof AbortSignal))
|
|
97
|
+
throw new TypeError("Signal must be an AbortSignal.");
|
|
98
|
+
options.signal = signal;
|
|
99
|
+
return command;
|
|
100
|
+
},
|
|
101
|
+
text() {
|
|
102
|
+
return start(true).then(({ stdout }) => stdout);
|
|
103
|
+
},
|
|
104
|
+
output() {
|
|
105
|
+
return start(true);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
return command;
|
|
59
109
|
};
|
|
60
|
-
return command;
|
|
61
110
|
}
|
|
111
|
+
/** Lazy, shell-free command tag. Awaiting streams to the terminal; text/output capture raw UTF-8. */
|
|
112
|
+
export const $ = createRunner();
|
|
62
113
|
async function execute(options, strings, values) {
|
|
63
114
|
const args = [];
|
|
64
115
|
for (const [index, text] of strings.entries()) {
|
|
65
|
-
if (/["'`\\$|&;<>*?()[\]{}~#\0]/.test(text))
|
|
116
|
+
if (/["'`\\$|&;<>*?()[\]{}~#\0]/.test(text))
|
|
66
117
|
throw new Error("Shell syntax is not supported. Interpolate argument values instead.");
|
|
67
|
-
}
|
|
68
118
|
if ((index > 0 && text !== "" && !/^\s/.test(text)) ||
|
|
69
119
|
(index < values.length && text !== "" && !/\s$/.test(text)) ||
|
|
70
120
|
(index > 0 && index < values.length && text === "")) {
|
|
@@ -72,12 +122,10 @@ async function execute(options, strings, values) {
|
|
|
72
122
|
}
|
|
73
123
|
args.push(...text.split(/\s+/).filter(Boolean));
|
|
74
124
|
if (index < values.length) {
|
|
75
|
-
const
|
|
76
|
-
const items = Array.isArray(value) ? value : [value];
|
|
125
|
+
const items = Array.isArray(values[index]) ? values[index] : [values[index]];
|
|
77
126
|
for (const item of items) {
|
|
78
|
-
if (typeof item !== "string" && typeof item !== "number")
|
|
127
|
+
if (typeof item !== "string" && typeof item !== "number")
|
|
79
128
|
throw new TypeError("Arguments must be strings or numbers.");
|
|
80
|
-
}
|
|
81
129
|
if (String(item).includes("\0"))
|
|
82
130
|
throw new TypeError("Arguments must not contain null bytes.");
|
|
83
131
|
args.push(String(item));
|
|
@@ -87,15 +135,26 @@ async function execute(options, strings, values) {
|
|
|
87
135
|
const command = args.shift();
|
|
88
136
|
if (!command)
|
|
89
137
|
throw new Error("A command is required.");
|
|
138
|
+
if (options.signal?.aborted)
|
|
139
|
+
throw new CommandError("Command was aborted.", {
|
|
140
|
+
reason: "abort",
|
|
141
|
+
exitCode: null,
|
|
142
|
+
signal: null,
|
|
143
|
+
output: options.capture ? { stdout: "", stderr: "" } : undefined,
|
|
144
|
+
cause: options.signal.reason
|
|
145
|
+
});
|
|
90
146
|
let child;
|
|
91
147
|
try {
|
|
92
148
|
child = spawn(command, args, {
|
|
93
149
|
stdio: options.capture ? ["inherit", "pipe", "pipe"] : "inherit",
|
|
94
|
-
shell: false
|
|
150
|
+
shell: false,
|
|
151
|
+
detached: options.processGroup === true
|
|
95
152
|
});
|
|
96
153
|
}
|
|
97
154
|
catch (error) {
|
|
98
|
-
|
|
155
|
+
let code;
|
|
156
|
+
if (error instanceof Error && "code" in error && typeof error.code === "string")
|
|
157
|
+
code = error.code;
|
|
99
158
|
throw new CommandError(`Command could not be spawned (${code ?? "process error"}).`, {
|
|
100
159
|
reason: "spawn",
|
|
101
160
|
exitCode: null,
|
|
@@ -111,86 +170,165 @@ async function execute(options, strings, values) {
|
|
|
111
170
|
let overflowStream;
|
|
112
171
|
let timeout;
|
|
113
172
|
let escalation;
|
|
173
|
+
let closed = false;
|
|
174
|
+
let escalationDone = true;
|
|
175
|
+
let settled = false;
|
|
176
|
+
let managedSignal;
|
|
114
177
|
const chunks = { stdout: [], stderr: [] };
|
|
115
|
-
|
|
116
|
-
const
|
|
178
|
+
let capturedBytes = 0;
|
|
179
|
+
const maxBufferBytes = options.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;
|
|
180
|
+
const groupPid = child.pid;
|
|
181
|
+
const send = (signal) => {
|
|
182
|
+
try {
|
|
183
|
+
if (options.processGroup === true) {
|
|
184
|
+
if (groupPid === undefined) {
|
|
185
|
+
failure ??= new Error("Command process group has no process ID.");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
process.kill(-groupPid, signal);
|
|
189
|
+
}
|
|
190
|
+
else
|
|
191
|
+
child.kill(signal);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (error instanceof Error && !("code" in error && error.code === "ESRCH"))
|
|
195
|
+
failure ??= error;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const terminate = (reason, initialSignal = "SIGTERM") => {
|
|
117
199
|
if (stopped)
|
|
118
200
|
return;
|
|
119
201
|
stopped = reason;
|
|
120
|
-
|
|
121
|
-
|
|
202
|
+
managedSignal = initialSignal;
|
|
203
|
+
// Only managed group cancellation waits beyond direct-child close.
|
|
204
|
+
escalationDone = options.processGroup !== true;
|
|
205
|
+
send(initialSignal);
|
|
206
|
+
escalation = setTimeout(() => {
|
|
207
|
+
managedSignal = "SIGKILL";
|
|
208
|
+
send("SIGKILL");
|
|
209
|
+
escalationDone = true;
|
|
210
|
+
finish();
|
|
211
|
+
}, TERMINATION_GRACE_MS);
|
|
122
212
|
};
|
|
123
213
|
const collect = (stream, chunk) => {
|
|
124
|
-
const remaining =
|
|
214
|
+
const remaining = maxBufferBytes - capturedBytes;
|
|
125
215
|
if (remaining > 0) {
|
|
126
216
|
const retained = Buffer.from(chunk.subarray(0, remaining));
|
|
127
217
|
chunks[stream].push(retained);
|
|
128
|
-
|
|
218
|
+
capturedBytes += retained.length;
|
|
129
219
|
}
|
|
130
220
|
if (chunk.length > remaining) {
|
|
131
221
|
overflowStream ??= stream;
|
|
132
222
|
terminate("overflow");
|
|
133
223
|
}
|
|
134
224
|
};
|
|
135
|
-
const onStdout = (chunk) => collect("stdout", chunk);
|
|
136
|
-
const onStderr = (chunk) => collect("stderr", chunk);
|
|
137
225
|
const onError = (error) => {
|
|
138
226
|
failure ??= error;
|
|
139
227
|
};
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
228
|
+
const onAbort = () => terminate("abort");
|
|
229
|
+
const onInterrupt = () => {
|
|
230
|
+
if (!stopped) {
|
|
231
|
+
interrupted = "SIGINT";
|
|
232
|
+
terminate("signal", "SIGINT");
|
|
233
|
+
}
|
|
234
|
+
else
|
|
235
|
+
send("SIGINT");
|
|
143
236
|
};
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
237
|
+
const onTerminate = () => {
|
|
238
|
+
if (!stopped) {
|
|
239
|
+
interrupted = "SIGTERM";
|
|
240
|
+
terminate("signal", "SIGTERM");
|
|
241
|
+
}
|
|
242
|
+
else
|
|
243
|
+
send("SIGTERM");
|
|
244
|
+
};
|
|
245
|
+
let abortListener;
|
|
246
|
+
const finish = () => {
|
|
247
|
+
if (settled || !closed || !escalationDone)
|
|
248
|
+
return;
|
|
249
|
+
settled = true;
|
|
154
250
|
clearTimeout(timeout);
|
|
155
|
-
|
|
251
|
+
if (escalation)
|
|
252
|
+
clearTimeout(escalation);
|
|
156
253
|
process.removeListener("SIGINT", onInterrupt);
|
|
157
254
|
process.removeListener("SIGTERM", onTerminate);
|
|
255
|
+
abortListener?.[Symbol.dispose]();
|
|
158
256
|
child.removeListener("error", onError);
|
|
159
257
|
child.stdout?.removeListener("data", onStdout);
|
|
160
258
|
child.stderr?.removeListener("data", onStderr);
|
|
259
|
+
child.stdout?.destroy();
|
|
260
|
+
child.stderr?.destroy();
|
|
161
261
|
let output;
|
|
162
|
-
if (options.capture)
|
|
262
|
+
if (options.capture) {
|
|
163
263
|
output = {
|
|
164
264
|
stdout: Buffer.concat(chunks.stdout).toString("utf8"),
|
|
165
265
|
stderr: Buffer.concat(chunks.stderr).toString("utf8")
|
|
166
266
|
};
|
|
267
|
+
}
|
|
167
268
|
let reason;
|
|
168
269
|
let message;
|
|
169
|
-
|
|
170
|
-
|
|
270
|
+
let actualSignal = interrupted ?? signal ?? (managedSignal === "SIGKILL" ? managedSignal : null);
|
|
271
|
+
let actualExitCode = exitCode;
|
|
272
|
+
if (stopped === "abort") {
|
|
273
|
+
reason = "abort";
|
|
274
|
+
message = "Command was aborted.";
|
|
275
|
+
}
|
|
276
|
+
else if (stopped === "timeout") {
|
|
277
|
+
reason = "timeout";
|
|
171
278
|
message = `Command timed out after ${options.timeoutMs} ms.`;
|
|
172
279
|
}
|
|
173
280
|
else if (stopped === "overflow") {
|
|
174
|
-
reason =
|
|
175
|
-
message = `Command ${overflowStream} exceeded the ${
|
|
281
|
+
reason = "overflow";
|
|
282
|
+
message = `Command ${overflowStream} exceeded the ${maxBufferBytes}-byte combined capture limit.`;
|
|
176
283
|
}
|
|
177
284
|
else if (failure) {
|
|
178
285
|
reason = "spawn";
|
|
179
|
-
message = `Command
|
|
286
|
+
message = `Command could not be spawned (${failure.code ?? "process error"}).`;
|
|
287
|
+
actualExitCode = null;
|
|
288
|
+
actualSignal = null;
|
|
180
289
|
}
|
|
181
|
-
else if (interrupted || signal) {
|
|
290
|
+
else if (stopped === "signal" || interrupted || signal) {
|
|
182
291
|
reason = "signal";
|
|
183
|
-
message = `Command failed (${interrupted ?? signal}).`;
|
|
292
|
+
message = `Command failed (${interrupted ?? signal ?? managedSignal}).`;
|
|
184
293
|
}
|
|
185
294
|
else if (exitCode !== 0) {
|
|
186
295
|
reason = "exit";
|
|
187
296
|
message = `Command failed (exit ${exitCode}).`;
|
|
297
|
+
actualSignal = signal;
|
|
188
298
|
}
|
|
189
299
|
else {
|
|
190
300
|
resolve(output ?? { stdout: "", stderr: "" });
|
|
191
301
|
return;
|
|
192
302
|
}
|
|
193
|
-
reject(new CommandError(message, {
|
|
303
|
+
reject(new CommandError(message, {
|
|
304
|
+
reason,
|
|
305
|
+
exitCode: actualExitCode,
|
|
306
|
+
signal: actualSignal,
|
|
307
|
+
code: reason === "spawn" ? failure?.code : undefined,
|
|
308
|
+
output,
|
|
309
|
+
cause: reason === "abort" ? options.signal?.reason : undefined
|
|
310
|
+
}));
|
|
311
|
+
};
|
|
312
|
+
let exitCode = null;
|
|
313
|
+
let signal = null;
|
|
314
|
+
const onStdout = (chunk) => collect("stdout", chunk);
|
|
315
|
+
const onStderr = (chunk) => collect("stderr", chunk);
|
|
316
|
+
process.on("SIGINT", onInterrupt);
|
|
317
|
+
process.on("SIGTERM", onTerminate);
|
|
318
|
+
if (options.signal)
|
|
319
|
+
abortListener = addAbortListener(options.signal, onAbort);
|
|
320
|
+
child.on("error", onError);
|
|
321
|
+
child.stdout?.on("data", onStdout);
|
|
322
|
+
child.stderr?.on("data", onStderr);
|
|
323
|
+
if (options.timeoutMs !== undefined)
|
|
324
|
+
timeout = setTimeout(() => terminate("timeout"), options.timeoutMs);
|
|
325
|
+
child.once("close", (code, childSignal) => {
|
|
326
|
+
closed = true;
|
|
327
|
+
exitCode = code;
|
|
328
|
+
signal = childSignal;
|
|
329
|
+
finish();
|
|
194
330
|
});
|
|
331
|
+
if (options.signal?.aborted)
|
|
332
|
+
onAbort();
|
|
195
333
|
});
|
|
196
334
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mypolis.eu/command",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Lazy, terminal-first, shell-free commands for Node.js.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
"access": "public",
|
|
13
13
|
"registry": "https://registry.npmjs.org/"
|
|
14
14
|
},
|
|
15
|
-
"files": [
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/index.js",
|
|
17
|
+
"dist/index.d.ts",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
16
21
|
"scripts": {
|
|
17
22
|
"build": "tsc -p tsconfig.build.json",
|
|
18
23
|
"prepack": "npm run build",
|