@j-o-r/sh 1.1.29 → 1.1.32

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.
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Shared internals for the SH library.
3
+ *
4
+ * Single source of truth for the default command options used by the `SH`
5
+ * template tag (`lib/SH.js`) and `SHDispatch` (`lib/SHDispatch.js`), and for
6
+ * the shared `parseDuration` used by `lib/SH.js` and `lib/SHExecute.js`.
7
+ * Not part of the public API; nothing here is re-exported from `lib/SH.js`.
8
+ */
9
+
10
+ /**
11
+ * Explicit default `cwd` override, set via `SH.cwd = dir`.
12
+ *
13
+ * @type {string | undefined}
14
+ */
15
+ let cwdOverride;
16
+
17
+ /**
18
+ * Default maximum buffered bytes per stdout/stderr stream: 512000 (500 KiB).
19
+ * Used by `SHExecute` when `options.maxBuffer` is unset or invalid.
20
+ */
21
+ const DEFAULT_MAX_BUFFER = 500 * 1024;
22
+
23
+ /**
24
+ * Default options applied to all SH commands unless overridden.
25
+ *
26
+ * `cwd` is intentionally lazy: the getter resolves `process.cwd()` at read
27
+ * time, so commands created after a `cd()` run in the new directory instead of
28
+ * the directory the process was started in. `SHDispatch` spreads these
29
+ * defaults (`{ ...defaultOptions }`) per command, which invokes the getter and
30
+ * freezes the value for that command — "defaults captured at creation time".
31
+ * Do not "optimize" this into a static snapshot.
32
+ *
33
+ * An explicit `SH.cwd = dir` assignment takes precedence over `process.cwd()`
34
+ * (it only redirects SH commands; it does not `chdir` the process). `cd()`
35
+ * clears the override again, so the most recent of the two always wins.
36
+ *
37
+ * `maxBuffer` and `detached` are initialized explicitly so every key the `SH`
38
+ * proxy exposes ({@link defaultOptionKeys}) also has a defined value here.
39
+ * This is behavior-neutral: `SHExecute` already fell back to
40
+ * `DEFAULT_MAX_BUFFER` for unset/invalid values and truthy-checks `detached`.
41
+ *
42
+ * @type {import('./SHDispatch.js').SHOptions}
43
+ */
44
+ const defaultOptions = {
45
+ get cwd() {
46
+ return cwdOverride ?? process.cwd();
47
+ },
48
+ set cwd(value) {
49
+ cwdOverride = value;
50
+ },
51
+ env: process.env,
52
+ shell: 'bash',
53
+ stdio: ['inherit', 'pipe', 'pipe'],
54
+ timeout: 0, // when 0 there is no timeout
55
+ maxBuffer: DEFAULT_MAX_BUFFER,
56
+ detached: false,
57
+ };
58
+
59
+ /**
60
+ * Known global option keys exposed by the `SH` proxy in `lib/SH.js`.
61
+ *
62
+ * Derived from the own keys of {@link defaultOptions}, so the key set can
63
+ * never drift from the defaults. (The previous hard-coded list in `lib/SH.js`
64
+ * readable-exposed `maxBuffer`/`detached`, which were uninitialized in the
65
+ * defaults.) The proxy routes reads/writes for these keys to
66
+ * `defaultOptions` and throws a `TypeError` on writes to any other key
67
+ * (decision D1).
68
+ *
69
+ * @type {Set<string>}
70
+ */
71
+ const defaultOptionKeys = new Set(Object.keys(defaultOptions));
72
+
73
+ /**
74
+ * Clears the `SH.cwd` override so the lazy `cwd` getter follows
75
+ * `process.cwd()` again. Called by `cd()` after a successful
76
+ * `process.chdir()`.
77
+ */
78
+ const clearCwdOverride = () => {
79
+ cwdOverride = undefined;
80
+ };
81
+
82
+ /**
83
+ * Parses a human-readable duration into milliseconds.
84
+ *
85
+ * Accepts finite non-negative numbers (milliseconds) and strings in the exact
86
+ * forms `'Nms'`, `'Ns'`, or a bare `'N'`. The bare-number form is treated as
87
+ * milliseconds on purpose: it preserves the leniency of the former `SHExecute`
88
+ * parser, so e.g. `timeout: '100'` keeps working.
89
+ *
90
+ * `null`/`undefined` are not special-cased here; call sites that allow an
91
+ * absent duration handle it themselves (e.g. `parseDuration(timeout ?? 0)`).
92
+ *
93
+ * @param {number|string} d - Duration as number (ms) or string ('5s', '100ms', '100').
94
+ * @returns {number} Duration in milliseconds.
95
+ * @throws {Error} If the duration type or format is invalid.
96
+ */
97
+ const parseDuration = (d) => {
98
+ if (typeof d == 'number') {
99
+ if (!Number.isFinite(d) || d < 0)
100
+ throw new Error(`Invalid duration: "${d}".`);
101
+ return d;
102
+ }
103
+ if (typeof d == 'string') {
104
+ const match = d.match(/^(\d+)(ms|s)?$/);
105
+ if (!match)
106
+ throw new Error(`Unknown duration: "${d}".`);
107
+ const amount = Number(match[1]);
108
+ return match[2] == 's' ? amount * 1000 : amount;
109
+ }
110
+ throw new Error(`Invalid duration type: "${d === null ? 'Null' : typeof d}".`);
111
+ };
112
+
113
+ export { defaultOptions, defaultOptionKeys, clearCwdOverride, DEFAULT_MAX_BUFFER, parseDuration };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@j-o-r/sh",
3
3
  "author": "Jorrit Duin <j-o-r@duin.work>",
4
4
  "type": "module",
5
- "version": "1.1.29",
5
+ "version": "1.1.32",
6
6
  "description": "Execute shell commands on Linux-based systems from javascript",
7
7
  "main": "lib/SH.js",
8
8
  "types": "types/SH.d.ts",
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "license": "Apache License, Version 2.0",
24
24
  "devDependencies": {
25
- "@types/node": "^22.10.10"
25
+ "@types/node": "*"
26
26
  },
27
27
  "bugs": {
28
28
  "url": "https://codeberg.org/duin/sh/issues"
package/types/SH.d.ts CHANGED
@@ -1,9 +1,14 @@
1
+ /**
2
+ * Parsed command-line arguments.
3
+ *
4
+ * Named options map to their string value, or `true` when no value is supplied.
5
+ * Bare positional arguments are collected in `_`.
6
+ */
1
7
  export type ArgsObject = {
2
- [x: string]: string;
8
+ _: string[];
9
+ } & {
10
+ [x: string]: string | true;
3
11
  };
4
- export type RejectCallback = Function;
5
- export type ResolveCallback = Function;
6
- export type SHOptions = import("./SHDispatch.js").SHOptions;
7
12
  export type AbortableInput = {
8
13
  /**
9
14
  * - Promise that resolves to user input or void if aborted.
@@ -14,33 +19,56 @@ export type AbortableInput = {
14
19
  */
15
20
  abort: () => void;
16
21
  };
17
- export type ExpBackoffGenerator = Object;
22
+ /**
23
+ * Generator that yields retry delay durations in milliseconds.
24
+ */
25
+ export type ExpBackoffGenerator = Generator<number, void, unknown>;
18
26
  /**
19
27
  * Template tag for building and executing shell commands.
20
28
  *
21
29
  * Returns an {@link SHDispatch} instance for configuration (.options()) and execution (.run(), .runSync()).
22
30
  *
23
- * Interpolation rules:
24
- * - Arrays: Elements trimmed, newlines/tabs escaped, shell-special chars single-quoted with '\'' escape.
25
- * - Other values: String(value) inserted as-is (NOT auto-escaped; use {@link bashEscape} for safety).
31
+ * Interpolation rules are raw-by-default:
32
+ * - Arrays: Each element is converted with `String(value)` and joined with one space.
33
+ * - Other values: `String(value)` is inserted directly into the shell source.
34
+ *
35
+ * Raw interpolation allows trusted shell fragments such as pipes, redirects, and
36
+ * command separators. It is not safe for untrusted input. Wrap untrusted values
37
+ * with {@link bashEscape} before interpolating them.
26
38
  *
27
- * SH acts as both template tag and options setter: `SH.timeout = 5000; SH`cmd``
39
+ * SH also acts as a global options setter (see the last example). Global
40
+ * defaults are captured when each command is created, not when it runs — see
41
+ * the "defaults captured at creation time" note in {@link SHDispatch#options}.
42
+ * Only the known option keys (cwd, env, shell, stdio, timeout, maxBuffer,
43
+ * detached) may be assigned; assigning anything else throws a TypeError
44
+ * listing the known keys (typo guard, decision D1).
28
45
  *
29
46
  * @param {TemplateStringsArray} pieces - String literals from template.
30
- * @param {...unknown[]} args - Values to interpolate.
47
+ * @param {...unknown} args - Values to interpolate.
31
48
  * @returns {SHDispatch} Command dispatcher.
32
49
  * @throws {Error} If pieces contain undefined.
33
50
  * @example
34
51
  * const cmd = SH`echo ${'Hello'}`;
35
52
  * await cmd.run(); // Executes: echo Hello
36
53
  *
37
- * // Array interpolation
38
- * SH`ls ${['-la', '/']}`.run(); // ls '-la' '/'
54
+ * // Array interpolation is raw and joins with spaces
55
+ * SH`ls ${['-la', '/']}`.run(); // Executes: ls -la /
56
+ *
57
+ * // Quote untrusted values explicitly
58
+ * SH`printf '%s\n' ${bashEscape('semi; colon')}`.run();
59
+ *
60
+ * // Set global defaults for subsequently created commands
61
+ * SH.timeout = 5000;
62
+ * await SH`cmd`.run();
39
63
  */
40
64
  export const SH: any;
41
65
  /**
42
66
  * Changes the current working directory.
43
67
  *
68
+ * Affects the cwd of subsequent `SH` commands: default options capture
69
+ * `process.cwd()` lazily when each command is created. Also clears any
70
+ * `SH.cwd` override, so the most recent cwd change always wins.
71
+ *
44
72
  * @param {string} dir - Path to new directory.
45
73
  * @example
46
74
  * cd('/tmp');
@@ -58,30 +86,45 @@ export function sleep(duration: string | number): Promise<void>;
58
86
  /**
59
87
  * Retries an async function up to N times with optional delays.
60
88
  *
61
- * Delay can be fixed ('1s'), generator (expBackoff()), or none.
89
+ * Delay precedence: when `delayOrCallback` is a function it is the retried
90
+ * callback and there is no delay between attempts. Otherwise it is a static
91
+ * duration ('1s', 100) or a delay generator (e.g. {@link expBackoff}), and
92
+ * `callback` is the retried function. A generator yield of `undefined`
93
+ * (exhausted generator) or `0` means "no further delay" — the next attempt
94
+ * runs immediately.
62
95
  *
63
- * @param {number} count - Number of attempts.
64
- * @param {string|number|ExpBackoffGenerator|Function} delayOrFn - Delay or callback if no separate fn.
65
- * @param {Function} [fn] - Callback to retry (if delayOrFn not function).
96
+ * @param {number} count - Positive integer number of attempts.
97
+ * @param {string|number|Iterator<number|string>|(() => (Promise<any>|any))} delayOrCallback - Delay, delay generator, or the callback when no separate callback is supplied.
98
+ * @param {() => (Promise<any>|any)} [callback] - Callback to retry when a delay is supplied.
66
99
  * @returns {Promise<any>} Successful result.
67
- * @throws {Error} Last error if all attempts fail.
100
+ * @throws {Error} If arguments are invalid, or the last callback error if all attempts fail.
68
101
  * @example
69
102
  * await retry(3, '1s', () => SH`curl http://unreliable`.run());
70
103
  * await retry(3, expBackoff(), () => flakyOp());
71
104
  */
72
- export function retry(count: number, a: any, b: any): Promise<any>;
105
+ export function retry(count: number, delayOrCallback: string | number | Iterator<number | string> | (() => (Promise<any> | any)), callback?: () => (Promise<any> | any)): Promise<any>;
73
106
  /**
74
107
  * Reads entire stdin as UTF-8 string. Resolves to empty string if TTY (no pipe).
75
108
  *
76
- * @returns {Promise<string|undefined>} Stdin content or undefined if TTY.
109
+ * Side effect: permanently sets the `process.stdin` encoding to utf8 and
110
+ * switches it to flowing mode.
111
+ *
112
+ * @returns {Promise<string>} Stdin content, or an empty string if TTY.
77
113
  * @example
78
114
  * const input = await readIn(); // Use in piped scripts
79
115
  */
80
- export function readIn(): Promise<string | undefined>;
116
+ export function readIn(): Promise<string>;
81
117
  /**
82
118
  * Prompts user for input on stdin with custom prompt.
83
119
  *
84
- * Supports aborting input collection.
120
+ * Submit heuristics (non-obvious): each 'line' event is accumulated into a
121
+ * multi-line buffer and (re)starts a debounce timer
122
+ * ({@link SUBMIT_DEBOUNCE_MS}); when the timer fires without further input,
123
+ * the promise auto-resolves with the accumulated text plus any partially
124
+ * typed readline line. A paste — a stdin chunk longer than
125
+ * {@link PASTE_MIN_CHUNK} chars containing a newline — (re)starts the same
126
+ * timer, so pasted multi-line text resolves as one input. `abort()` resolves
127
+ * the promise with `undefined`.
85
128
  *
86
129
  * @param {string} prompt - Prompt text to display.
87
130
  * @returns {AbortableInput} Object with input Promise and abort function.
@@ -92,9 +135,13 @@ export function readIn(): Promise<string | undefined>;
92
135
  */
93
136
  export function userIn(prompt: string): AbortableInput;
94
137
  /**
95
- * Executes a callback in a new async context (fresh callstack).
138
+ * Awaits `callback()` and returns its result.
96
139
  *
97
- * Useful for parallel operations without nesting.
140
+ * Despite the historical name, this is currently a thin wrapper: no new async
141
+ * context or fresh callstack is created — the callback runs in the current
142
+ * one. It is kept as an intent marker for grouping async work (and for API
143
+ * compatibility); real isolation via AsyncLocalStorage is a pending decision
144
+ * (see the project TODO, decision D2).
98
145
  *
99
146
  * @param {() => Promise<any>} callback - Async function to execute.
100
147
  * @returns {Promise<any>} Result of callback.
@@ -111,16 +158,21 @@ export function within(callback: () => Promise<any>): Promise<any>;
111
158
  * @param {string} [max='60s'] - Max backoff duration.
112
159
  * @param {string} [rand='100ms'] - Max jitter.
113
160
  * @yields {number} Next backoff ms.
161
+ * @returns {ExpBackoffGenerator} Infinite generator of backoff delays in ms.
114
162
  * @example
115
163
  * const backoff = expBackoff();
116
164
  * await sleep(backoff.next().value);
117
165
  */
118
- export function expBackoff(max?: string, rand?: string): Generator<number, void, unknown>;
166
+ export function expBackoff(max?: string, rand?: string): ExpBackoffGenerator;
119
167
  /**
120
168
  * Parses CLI arguments into an object.
121
169
  *
122
170
  * Supports --key value, -k value (no shorts grouped).
123
171
  * Bares go to _[]. Duplicates error. No = syntax.
172
+ * Negative number tokens such as -1, -0.5, and -1e3 are parsed as values or
173
+ * positionals, not options. A lone `-` is a positional (stdin convention).
174
+ * `--` is the conventional end-of-options terminator: every token after it
175
+ * goes into `_`, even option-looking ones.
124
176
  *
125
177
  * Defaults to process.argv.slice(2).
126
178
  *
@@ -129,24 +181,18 @@ export function expBackoff(max?: string, rand?: string): Generator<number, void,
129
181
  * @throws {Error} On invalid/dupe args.
130
182
  * @example
131
183
  * parseArgs(['--port', '8080', 'file.txt']); // { port: '8080', _: ['file.txt'] }
184
+ * parseArgs(['--n', '-1']); // { n: '-1', _: [] }
185
+ * parseArgs(['-', '--port', '1']); // { port: '1', _: ['-'] }
186
+ * parseArgs(['--', '--x']); // { _: ['--x'] }
132
187
  */
133
188
  export function parseArgs(args?: string[]): ArgsObject;
134
189
  /**
135
- * @typedef {Object.<string, string>} ArgsObject
136
- * @property {string[]} _ - Array of strings, unnamed parameters
137
- * @property {string} [key: string] - Any string key maps to a string value
138
- * @description Parsed parameters result.
139
- */
140
- /**
141
- * @typedef {Function} RejectCallback
142
- * @param {Error} error - The error object passed to the callback.
143
- */
144
- /**
145
- * @typedef {Function} ResolveCallback
146
- * @param {any} [param] - Optional callback any value
147
- */
148
- /**
149
- * @typedef {import('./SHDispatch.js').SHOptions} SHOptions
190
+ * Parsed command-line arguments.
191
+ *
192
+ * Named options map to their string value, or `true` when no value is supplied.
193
+ * Bare positional arguments are collected in `_`.
194
+ *
195
+ * @typedef {{_: string[]} & Object.<string, string|true>} ArgsObject
150
196
  */
151
197
  /**
152
198
  * @typedef {Object} AbortableInput
@@ -154,22 +200,29 @@ export function parseArgs(args?: string[]): ArgsObject;
154
200
  * @property {() => void} abort - Function to abort input collection.
155
201
  */
156
202
  /**
157
- * @typedef {Object} ExpBackoffGenerator
158
- * @generator
159
- * @yields {number} Backoff time in ms.
203
+ * Generator that yields retry delay durations in milliseconds.
204
+ *
205
+ * @typedef {Generator<number, void, unknown>} ExpBackoffGenerator
160
206
  */
161
207
  /**
162
208
  * Utility to determine the JavaScript type of a value.
163
209
  *
164
- * @param {any} fn - Any value to inspect.
210
+ * Uses `Object.prototype.toString`, so `jsType(null)` returns `'Null'`;
211
+ * `undefined` is special-cased to `'undefined'`. Plain objects report their
212
+ * constructor name, or `'Object'` when there is none (null-prototype objects).
213
+ *
214
+ * @param {unknown} value - Any value to inspect.
165
215
  * @returns {string} The "real" object type name (e.g., 'Array', 'Promise') or primitive typeof.
166
216
  * @example
167
217
  * jsType([]); // 'Array'
168
218
  * jsType(Promise.resolve()); // 'Promise'
219
+ * jsType(null); // 'Null'
220
+ * jsType(undefined); // 'undefined'
169
221
  */
170
- export function jsType(fn: any): string;
222
+ export function jsType(value: unknown): string;
171
223
  /**
172
- * Checks if an object has a specific own property (code-safe).
224
+ * Checks if an object has a specific own property. Safe for null-prototype
225
+ * objects and objects shadowing `hasOwnProperty`.
173
226
  *
174
227
  * @param {any} o - Object to examine.
175
228
  * @param {string} p - Property name to check.
@@ -180,12 +233,18 @@ import Test from './Test.js';
180
233
  import assert from 'node:assert';
181
234
  import AsyncTracker from './AsyncTracker.js';
182
235
  /**
183
- * Escapes a string for safe use as a bash command-line argument.
236
+ * Quotes a value as one POSIX shell argument.
184
237
  *
185
- * Handles quotes, backticks, $, newlines, etc.
238
+ * The `SH` template tag interpolates values as raw shell source by default. Use
239
+ * this helper when a JavaScript value must be passed as data instead of shell
240
+ * syntax. The returned string is single-quoted and preserves the exact string
241
+ * value, including leading/trailing whitespace, empty strings, quotes,
242
+ * semicolons, glob characters, tabs, and newlines.
186
243
  *
187
- * @param {string} x - Input string to escape.
188
- * @returns {string} Bash-escaped string.
244
+ * @param {unknown} x - Value to quote as a single shell argument.
245
+ * @returns {string} POSIX-shell-quoted argument string.
246
+ * @example
247
+ * await SH`printf '%s\n' ${bashEscape(userInput)}`.run();
189
248
  */
190
- export function bashEscape(x: string): string;
249
+ export function bashEscape(x: unknown): string;
191
250
  export { Test, assert, AsyncTracker };
@@ -1,32 +1,54 @@
1
1
  export default SHDispatch;
2
- export type SpawnSyncResponse = {
2
+ export type StdioOption = ("pipe" | "ignore" | "inherit" | number);
3
+ export type StdioOptions = Array<StdioOption> | StdioOption;
4
+ /**
5
+ * Core options for SH/SHDispatch.
6
+ *
7
+ * Defaults (merged from global SH):
8
+ * - `cwd`: `process.cwd()`
9
+ * - `env`: `process.env`
10
+ * - `shell`: `'bash'` (string/true → shell; false → no-shell `/usr/bin/env -S`)
11
+ * - `stdio`: `['inherit', 'pipe', 'pipe']`
12
+ * - `timeout`: `0` (no timeout; rolling on data)
13
+ * - `maxBuffer`: `512000` (500 KiB per stream in SHExecute)
14
+ *
15
+ * Prefix (`.options(undefined, prefix)`) only for shell mode.
16
+ */
17
+ export type SHOptions = {
3
18
  /**
4
- * - Exit code (null if signal).
19
+ * - Working directory for spawned commands.
5
20
  */
6
- status: number | null;
21
+ cwd: string;
7
22
  /**
8
- * - Terminating signal.
23
+ * - Environment for spawned commands.
9
24
  */
10
- signal: string | null;
25
+ env: NodeJS.ProcessEnv;
11
26
  /**
12
- * - [stdin, stdout, stderr].
27
+ * - Shell executable, true for bash, or false for no-shell mode.
13
28
  */
14
- output: (string | Buffer | null)[];
29
+ shell: string | boolean;
15
30
  /**
16
- * - Process ID.
31
+ * - Stdio config passed to child_process.
17
32
  */
18
- pid: number;
33
+ stdio: StdioOptions;
19
34
  /**
20
- * - Captured stdout.
35
+ * - Timeout in ms or duration string; 0 disables.
36
+ * Async `run()` uses a rolling timeout that resets on stdout/stderr data.
37
+ * `runSync()` passes it to `spawnSync`, where it is absolute: the process is
38
+ * killed after the full duration no matter how much output it produces.
21
39
  */
22
- stdout: string | Buffer | null;
40
+ timeout: number | string;
23
41
  /**
24
- * - Captured stderr.
42
+ * - Maximum buffered bytes per stdout/stderr stream.
25
43
  */
26
- stderr: string | Buffer | null;
44
+ maxBuffer?: number | undefined;
45
+ /**
46
+ * - Run process detached and resolve early (~1s).
47
+ * Unless stdio is explicitly set for the command, stdio is forced to
48
+ * `'ignore'`, because open pipes would keep the parent's event loop alive.
49
+ */
50
+ detached?: boolean | undefined;
27
51
  };
28
- export type StdioOption = ("pipe" | "ignore" | "inherit" | number);
29
- export type StdioOptions = Array<StdioOption> | StdioOption;
30
52
  /**
31
53
  * High-level command dispatcher.
32
54
  *
@@ -41,24 +63,30 @@ export type StdioOptions = Array<StdioOption> | StdioOption;
41
63
  declare class SHDispatch {
42
64
  /**
43
65
  * @param {string} cmd - Command string.
44
- * @param {Partial<typeof SHOptions>} [options] - Initial options.
66
+ * @param {Partial<SHOptions>} [options] - Initial options.
45
67
  * @param {string} [prefix] - Shell prefix (e.g., 'set -euo pipefail').
46
68
  * @throws {Error} Invalid/empty cmd.
47
69
  */
48
- constructor(cmd: string, options?: Partial<typeof SHOptions>, prefix?: string);
70
+ constructor(cmd: string, options?: Partial<SHOptions>, prefix?: string);
49
71
  /**
50
- * Updates options/prefix; resets to defaults + user overrides (non-cumulative).
72
+ * Updates options/prefix by merging into the dispatch instance's current options.
51
73
  *
52
- * Strings for `stdio` → array fill.
74
+ * This preserves global `SH.*` defaults captured when the command was created
75
+ * and only overrides options explicitly supplied for this command. Strings for
76
+ * `stdio` are expanded to `[value, value, value]`.
53
77
  *
54
- * @param {Partial<typeof SHOptions>} [options] - New options.
78
+ * @param {Partial<SHOptions>} [options] - New options.
55
79
  * @param {string} [prefix] - New prefix.
56
80
  * @returns {SHDispatch} Self for chaining.
57
81
  */
58
- options(options?: Partial<typeof SHOptions>, prefix?: string): SHDispatch;
82
+ options(options?: Partial<SHOptions>, prefix?: string): SHDispatch;
59
83
  /**
60
84
  * Async run: Captures stdout; rejects on error/timeout.
61
85
  *
86
+ * Replaces the internal process handle: calling `run()` again while a
87
+ * previous run is still active makes that first process unkillable via
88
+ * `kill()`.
89
+ *
62
90
  * @param {string} [payload] - Stdin payload.
63
91
  * @returns {Promise<string>} Stdout.
64
92
  */
@@ -81,10 +109,3 @@ declare class SHDispatch {
81
109
  kill(signal?: number | string): Promise<number[]>;
82
110
  #private;
83
111
  }
84
- declare namespace SHOptions {
85
- let cwd: string;
86
- let env: NodeJS.ProcessEnv;
87
- let shell: string;
88
- let stdio: string[];
89
- let timeout: number;
90
- }
@@ -1,10 +1,9 @@
1
1
  export default SHExecute;
2
- export type SHExecuteOptions = any & {
3
- maxBuffer?: number;
4
- };
2
+ export type SHExecuteOptions = import("./SHDispatch.js").SHOptions;
5
3
  /**
6
- * @typedef {import('../SH.js').SHOptions & { maxBuffer?: number }} SHExecuteOptions
7
- * @description Extended options for SHExecute: adds `maxBuffer` (bytes per stream, default 1MB).
4
+ * @typedef {import('./SHDispatch.js').SHOptions} SHExecuteOptions
5
+ * @description Options accepted by SHExecute — the shared SHOptions; `maxBuffer`
6
+ * (bytes per stream) defaults to 512000 (500 KiB).
8
7
  */
9
8
  /**
10
9
  * Low-level process executor for shell commands.
@@ -15,10 +14,16 @@ export type SHExecuteOptions = any & {
15
14
  * - **Shell mode**: If `options.shell` is string/true, runs `${prefix}; ${command}` via shell ('bash' default).
16
15
  * - **No-shell mode**: Uses `/usr/bin/env -S ${command}` for direct exec (ignores prefix).
17
16
  * - **Rolling timeout**: `options.timeout` (ms/'2s'); resets on stdout/stderr data. SIGTERM on expiry.
18
- * - **Buffering**: Captures stdout/stderr up to `maxBuffer` (1MB default); appends truncation markers.
17
+ * Async `run()` only — the timeout is stripped from the spawn options so Node's
18
+ * native absolute spawn timeout never interferes. `runSync()` instead passes the
19
+ * timeout to `spawnSync`, whose semantics are absolute (kills after the full
20
+ * duration, regardless of output).
21
+ * - **Buffering**: Captures stdout/stderr up to `maxBuffer` (512000 bytes / 500 KiB default); appends truncation markers.
19
22
  * - **Payload**: `run(payload)` writes string to stdin (forces pipe).
20
- * - **Detached**: If `options.detached`, resolves early (~1s) and unrefs.
21
- * - **Kill**: Terminates process + children via pgrep.
23
+ * - **Detached**: If `options.detached`, resolves early (~1s) and unrefs. Unless the
24
+ * caller explicitly set `stdio`, SHDispatch forces `stdio: 'ignore'` for detached
25
+ * runs, because open pipes keep the parent's event loop alive and defeat detachment.
26
+ * - **Kill**: Terminates process + direct children via pgrep (requires procps; grandchildren survive).
22
27
  *
23
28
  * @example
24
29
  * const exec = new SHExecute('ls', 'set -euo pipefail', { timeout: '5s' });
@@ -42,16 +47,22 @@ declare class SHExecute {
42
47
  /**
43
48
  * Asynchronous execution with buffering/timeout/kill.
44
49
  *
45
- * Resolves stdout (trimmed) on success; rejects on error/timeout/kill.
50
+ * Resolves stdout (trimmed) on success.
46
51
  *
47
52
  * @param {string} [payload] - Stdin data (forces pipe).
48
53
  * @returns {Promise<string>} Trimmed UTF-8 stdout (+ truncation marker if exceeded).
49
- * @throws {Error} Command failure (incl. code, stderr), timeout, kill, spawn error.
54
+ * Rejects with an Error on command failure (message includes the exit code,
55
+ * or the signal name for signal kills, plus stderr), rolling-timeout expiry,
56
+ * forced kill, or spawn failure.
50
57
  */
51
58
  run(payload?: string): Promise<string>;
52
59
  /**
53
60
  * Terminates process and its children (via pgrep).
54
61
  *
62
+ * Requires `pgrep` (procps) at runtime. Only direct children are
63
+ * discovered — grandchildren and deeper descendants survive. Best-effort:
64
+ * pgrep failures resolve to an empty child list instead of rejecting.
65
+ *
55
66
  * @param {number | string} [signal='SIGTERM'] - Signal to send.
56
67
  * @returns {Promise<number[]>} Killed PIDs.
57
68
  * @throws {Error} No process/PID.
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Default options applied to all SH commands unless overridden.
3
+ *
4
+ * `cwd` is intentionally lazy: the getter resolves `process.cwd()` at read
5
+ * time, so commands created after a `cd()` run in the new directory instead of
6
+ * the directory the process was started in. `SHDispatch` spreads these
7
+ * defaults (`{ ...defaultOptions }`) per command, which invokes the getter and
8
+ * freezes the value for that command — "defaults captured at creation time".
9
+ * Do not "optimize" this into a static snapshot.
10
+ *
11
+ * An explicit `SH.cwd = dir` assignment takes precedence over `process.cwd()`
12
+ * (it only redirects SH commands; it does not `chdir` the process). `cd()`
13
+ * clears the override again, so the most recent of the two always wins.
14
+ *
15
+ * `maxBuffer` and `detached` are initialized explicitly so every key the `SH`
16
+ * proxy exposes ({@link defaultOptionKeys}) also has a defined value here.
17
+ * This is behavior-neutral: `SHExecute` already fell back to
18
+ * `DEFAULT_MAX_BUFFER` for unset/invalid values and truthy-checks `detached`.
19
+ *
20
+ * @type {import('./SHDispatch.js').SHOptions}
21
+ */
22
+ export const defaultOptions: import("./SHDispatch.js").SHOptions;
23
+ /**
24
+ * Known global option keys exposed by the `SH` proxy in `lib/SH.js`.
25
+ *
26
+ * Derived from the own keys of {@link defaultOptions}, so the key set can
27
+ * never drift from the defaults. (The previous hard-coded list in `lib/SH.js`
28
+ * readable-exposed `maxBuffer`/`detached`, which were uninitialized in the
29
+ * defaults.) The proxy routes reads/writes for these keys to
30
+ * `defaultOptions` and throws a `TypeError` on writes to any other key
31
+ * (decision D1).
32
+ *
33
+ * @type {Set<string>}
34
+ */
35
+ export const defaultOptionKeys: Set<string>;
36
+ /**
37
+ * Clears the `SH.cwd` override so the lazy `cwd` getter follows
38
+ * `process.cwd()` again. Called by `cd()` after a successful
39
+ * `process.chdir()`.
40
+ */
41
+ export function clearCwdOverride(): void;
42
+ /**
43
+ * Default maximum buffered bytes per stdout/stderr stream: 512000 (500 KiB).
44
+ * Used by `SHExecute` when `options.maxBuffer` is unset or invalid.
45
+ */
46
+ export const DEFAULT_MAX_BUFFER: number;
47
+ /**
48
+ * Parses a human-readable duration into milliseconds.
49
+ *
50
+ * Accepts finite non-negative numbers (milliseconds) and strings in the exact
51
+ * forms `'Nms'`, `'Ns'`, or a bare `'N'`. The bare-number form is treated as
52
+ * milliseconds on purpose: it preserves the leniency of the former `SHExecute`
53
+ * parser, so e.g. `timeout: '100'` keeps working.
54
+ *
55
+ * `null`/`undefined` are not special-cased here; call sites that allow an
56
+ * absent duration handle it themselves (e.g. `parseDuration(timeout ?? 0)`).
57
+ *
58
+ * @param {number|string} d - Duration as number (ms) or string ('5s', '100ms', '100').
59
+ * @returns {number} Duration in milliseconds.
60
+ * @throws {Error} If the duration type or format is invalid.
61
+ */
62
+ export function parseDuration(d: number | string): number;