@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 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
- // Inherit the terminal, including interactive stdin. Resolves to undefined.
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 {stdout, stderr} = await $`git status --short`.output();
10
+ const result = await $`git status --short`.output();
11
+ await $`git fetch`.timeout(30_000);
21
12
 
22
- // Each interpolation is a whole argument; arrays expand into arguments.
23
- const paths = ['notes with spaces.md', 'draft.md'];
24
- await $`git diff -- ${paths}`;
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
- Commands inherit the current working directory and environment. Awaiting a command streams stdout and stderr to the terminal. `.text()` and `.output()` instead capture **both** streams; stdin remains inherited. `.text()` returns only stdout, while `.output()` returns both streams.
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
- ## Arguments, not Bash
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
- Literal text is split on whitespace. Interpolate strings or numbers for whole arguments; interpolate arrays of strings or numbers for multiple arguments. Empty strings are preserved and empty arrays add no arguments. Separate interpolations from adjacent arguments with whitespace. To combine a prefix and value, interpolate the complete string:
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
- ```js
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
- Construction does not spawn a process. Awaiting or calling `.then()`, `.text()` or `.output()` starts one shared execution. Repeated use returns the original result or failure, not a new process. `.text()` and `.output()` can share the same capture execution. Switching between inherited and captured stdio after starting is rejected. `.timeout(ms)` must be called before starting;
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 and limits
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
- * Lazy, shell-free command; awaiting inherits cwd, environment and stdio.
30
- * Interpolations occupy whole arguments; arrays expand into multiple arguments.
31
- * .text()/.output() capture raw UTF-8 (1 MiB per stream); .timeout(ms) configures before execution.
32
- * Execution is shared; capture mode cannot change and timeout is frozen after starting.
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
- const CAPTURE_LIMIT = 1024 * 1024; // Bytes per stream; overflow terminates rather than silently truncating.
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
- * Lazy, shell-free command; awaiting inherits cwd, environment and stdio.
18
- * Interpolations occupy whole arguments; arrays expand into multiple arguments.
19
- * .text()/.output() capture raw UTF-8 (1 MiB per stream); .timeout(ms) configures before execution.
20
- * Execution is shared; capture mode cannot change and timeout is frozen after starting.
21
- * Timeout/overflow sends SIGTERM, then SIGKILL after 500 ms; settlement always waits for close.
22
- */
23
- export function $(strings, ...values) {
24
- let execution;
25
- const options = {};
26
- const start = (capture) => {
27
- if (execution) {
28
- if (options.capture !== capture)
29
- throw new Error("Command capture mode cannot change after execution starts.");
30
- return execution;
31
- }
32
- options.capture = capture;
33
- execution = execute(options, strings, values);
34
- return execution;
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
- const command = {
37
- // biome-ignore lint/suspicious/noThenProperty: Awaiting this lazy command intentionally starts shared execution.
38
- then(onfulfilled, onrejected) {
39
- return start(false)
40
- .then(() => undefined)
41
- .then(onfulfilled, onrejected);
42
- },
43
- timeout(ms) {
44
- if (!Number.isFinite(ms) || ms <= 0 || ms > 2_147_483_647) {
45
- throw new TypeError("Timeout must be finite, positive and at most 2147483647 ms.");
46
- }
47
- if (execution)
48
- throw new Error("Command timeout cannot change after execution starts.");
49
- options.timeoutMs = ms;
50
- return command;
51
- },
52
- async text() {
53
- const { stdout } = await start(true);
54
- return stdout;
55
- },
56
- output() {
57
- return start(true);
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 value = values[index];
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
- const { code } = error;
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
- const bytes = { stdout: 0, stderr: 0 };
116
- const terminate = (reason) => {
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
- child.kill("SIGTERM");
121
- escalation = setTimeout(() => child.kill("SIGKILL"), TERMINATION_GRACE_MS);
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 = CAPTURE_LIMIT - bytes[stream];
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
- bytes[stream] += retained.length;
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 forwardSignal = (signal) => {
141
- interrupted ??= signal;
142
- child.kill(signal);
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 onInterrupt = () => forwardSignal("SIGINT");
145
- const onTerminate = () => forwardSignal("SIGTERM");
146
- process.on("SIGINT", onInterrupt);
147
- process.on("SIGTERM", onTerminate);
148
- child.on("error", onError);
149
- child.stdout?.on("data", onStdout);
150
- child.stderr?.on("data", onStderr);
151
- if (options.timeoutMs !== undefined)
152
- timeout = setTimeout(() => terminate("timeout"), options.timeoutMs);
153
- child.once("close", (exitCode, signal) => {
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
- clearTimeout(escalation);
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
- if (stopped === "timeout") {
170
- reason = stopped;
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 = stopped;
175
- message = `Command ${overflowStream} exceeded the ${CAPTURE_LIMIT}-byte capture limit.`;
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 failed (${failure.code ?? "process error"}).`;
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, { reason, exitCode, signal: interrupted ?? signal, code: failure?.code, output }));
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.1.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": ["dist/index.js", "dist/index.d.ts", "README.md", "LICENSE"],
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",