@j-o-r/sh 1.1.31 → 1.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/lib/SH.js +174 -149
- package/lib/SHDispatch.js +29 -26
- package/lib/SHExecute.js +300 -235
- package/lib/internal.js +179 -0
- package/package.json +3 -3
- package/types/SH.d.ts +74 -34
- package/types/SHDispatch.d.ts +14 -30
- package/types/SHExecute.d.ts +41 -13
- package/types/internal.d.ts +91 -0
- package/TODO.md +0 -8
package/lib/internal.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Explicit default `cwd` override, set via `SH.cwd = dir`.
|
|
14
|
+
*
|
|
15
|
+
* @type {string | undefined}
|
|
16
|
+
*/
|
|
17
|
+
let cwdOverride;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Default maximum buffered bytes per stdout/stderr stream: 512000 (500 KiB).
|
|
21
|
+
* Used by `SHExecute` when `options.maxBuffer` is unset or invalid.
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_MAX_BUFFER = 500 * 1024;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Global (non-scoped) default option values, excluding the lazy `cwd`.
|
|
27
|
+
*
|
|
28
|
+
* `env` is a live reference to `process.env` by default. These are the values
|
|
29
|
+
* read/written by the `SH` proxy when no `within()` scope is active.
|
|
30
|
+
*
|
|
31
|
+
* @type {Omit<import('./SHDispatch.js').SHOptions, 'cwd'>}
|
|
32
|
+
*/
|
|
33
|
+
const globalDefaults = {
|
|
34
|
+
env: process.env,
|
|
35
|
+
shell: 'bash',
|
|
36
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
37
|
+
timeout: 0, // absolute wall-clock timeout; 0 disables
|
|
38
|
+
maxBuffer: DEFAULT_MAX_BUFFER,
|
|
39
|
+
detached: false,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Per-`within()`-block option overrides.
|
|
44
|
+
*
|
|
45
|
+
* A plain object keyed by option name. A key present in the store shadows the
|
|
46
|
+
* global default for every command created inside the `within()` block; a key
|
|
47
|
+
* absent from the store inherits the enclosing scope (or the global default).
|
|
48
|
+
*
|
|
49
|
+
* @typedef {Object<string, any>} OptionScopeStore
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* AsyncLocalStorage backing `within()` scoping (decision D2).
|
|
54
|
+
*
|
|
55
|
+
* `within()` runs its callback inside a fresh store seeded with a shallow copy
|
|
56
|
+
* of the enclosing store, so nested blocks inherit their parent's scoped
|
|
57
|
+
* values but never leak their own assignments outward. `getDefault`/`setDefault`
|
|
58
|
+
* consult the current store first and fall back to the global defaults.
|
|
59
|
+
*
|
|
60
|
+
* @type {AsyncLocalStorage<OptionScopeStore>}
|
|
61
|
+
*/
|
|
62
|
+
const optionScope = new AsyncLocalStorage();
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reads the effective default for a key, honoring the current `within()` scope.
|
|
66
|
+
*
|
|
67
|
+
* A key present in the active scope store shadows the global default; `cwd`
|
|
68
|
+
* additionally falls back to the lazy `process.cwd()` resolution.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} key - Option key.
|
|
71
|
+
* @returns {any} Effective default value.
|
|
72
|
+
*/
|
|
73
|
+
const getDefault = (key) => {
|
|
74
|
+
const store = optionScope.getStore();
|
|
75
|
+
if (store && store[key] !== undefined) return store[key];
|
|
76
|
+
if (key === 'cwd') return cwdOverride ?? process.cwd();
|
|
77
|
+
return globalDefaults[key];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Writes a default, scoping to the current `within()` block when active.
|
|
82
|
+
*
|
|
83
|
+
* Inside a `within()` block the assignment lands in the block's store and is
|
|
84
|
+
* discarded when the block ends; outside it mutates the global default.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} key - Option key.
|
|
87
|
+
* @param {any} value - New default value.
|
|
88
|
+
*/
|
|
89
|
+
const setDefault = (key, value) => {
|
|
90
|
+
const store = optionScope.getStore();
|
|
91
|
+
if (store) { store[key] = value; return; }
|
|
92
|
+
if (key === 'cwd') { cwdOverride = value; return; }
|
|
93
|
+
globalDefaults[key] = value;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Default options applied to all SH commands unless overridden.
|
|
98
|
+
*
|
|
99
|
+
* `cwd` is intentionally lazy: the getter resolves `process.cwd()` at read
|
|
100
|
+
* time, so commands created after a `cd()` run in the new directory instead of
|
|
101
|
+
* the directory the process was started in. `SHDispatch` spreads these
|
|
102
|
+
* defaults (`{ ...defaultOptions }`) per command, which invokes the getters and
|
|
103
|
+
* freezes the values for that command — "defaults captured at creation time".
|
|
104
|
+
* Do not "optimize" this into a static snapshot.
|
|
105
|
+
*
|
|
106
|
+
* Every getter/setter is scope-aware: inside a `within()` block it reads/writes
|
|
107
|
+
* the block's scoped override (see {@link optionScope}); outside it reads/writes
|
|
108
|
+
* the global default. An explicit `SH.cwd = dir` assignment takes precedence
|
|
109
|
+
* over `process.cwd()` (it only redirects SH commands; it does not `chdir` the
|
|
110
|
+
* process). `cd()` clears the override again, so the most recent of the two
|
|
111
|
+
* always wins.
|
|
112
|
+
*
|
|
113
|
+
* @type {import('./SHDispatch.js').SHOptions}
|
|
114
|
+
*/
|
|
115
|
+
const defaultOptions = {};
|
|
116
|
+
for (const key of ['cwd', ...Object.keys(globalDefaults)]) {
|
|
117
|
+
Object.defineProperty(defaultOptions, key, {
|
|
118
|
+
enumerable: true,
|
|
119
|
+
configurable: true,
|
|
120
|
+
get() { return getDefault(key); },
|
|
121
|
+
set(value) { setDefault(key, value); }
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Known global option keys exposed by the `SH` proxy in `lib/SH.js`.
|
|
127
|
+
*
|
|
128
|
+
* Derived from the own keys of {@link defaultOptions}, so the key set can
|
|
129
|
+
* never drift from the defaults. (The previous hard-coded list in `lib/SH.js`
|
|
130
|
+
* readable-exposed `maxBuffer`/`detached`, which were uninitialized in the
|
|
131
|
+
* defaults.) The proxy routes reads/writes for these keys to
|
|
132
|
+
* `defaultOptions` and throws a `TypeError` on writes to any other key
|
|
133
|
+
* (decision D1).
|
|
134
|
+
*
|
|
135
|
+
* @type {Set<string>}
|
|
136
|
+
*/
|
|
137
|
+
const defaultOptionKeys = new Set(Object.keys(defaultOptions));
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Clears the `SH.cwd` override so the lazy `cwd` getter follows
|
|
141
|
+
* `process.cwd()` again. Called by `cd()` after a successful
|
|
142
|
+
* `process.chdir()`.
|
|
143
|
+
*/
|
|
144
|
+
const clearCwdOverride = () => {
|
|
145
|
+
cwdOverride = undefined;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Parses a human-readable duration into milliseconds.
|
|
150
|
+
*
|
|
151
|
+
* Accepts finite non-negative numbers (milliseconds) and strings in the exact
|
|
152
|
+
* forms `'Nms'`, `'Ns'`, or a bare `'N'`. The bare-number form is treated as
|
|
153
|
+
* milliseconds on purpose: it preserves the leniency of the former `SHExecute`
|
|
154
|
+
* parser, so e.g. `timeout: '100'` keeps working.
|
|
155
|
+
*
|
|
156
|
+
* `null`/`undefined` are not special-cased here; call sites that allow an
|
|
157
|
+
* absent duration handle it themselves (e.g. `parseDuration(timeout ?? 0)`).
|
|
158
|
+
*
|
|
159
|
+
* @param {number|string} d - Duration as number (ms) or string ('5s', '100ms', '100').
|
|
160
|
+
* @returns {number} Duration in milliseconds.
|
|
161
|
+
* @throws {Error} If the duration type or format is invalid.
|
|
162
|
+
*/
|
|
163
|
+
const parseDuration = (d) => {
|
|
164
|
+
if (typeof d == 'number') {
|
|
165
|
+
if (!Number.isFinite(d) || d < 0)
|
|
166
|
+
throw new Error(`Invalid duration: "${d}".`);
|
|
167
|
+
return d;
|
|
168
|
+
}
|
|
169
|
+
if (typeof d == 'string') {
|
|
170
|
+
const match = d.match(/^(\d+)(ms|s)?$/);
|
|
171
|
+
if (!match)
|
|
172
|
+
throw new Error(`Unknown duration: "${d}".`);
|
|
173
|
+
const amount = Number(match[1]);
|
|
174
|
+
return match[2] == 's' ? amount * 1000 : amount;
|
|
175
|
+
}
|
|
176
|
+
throw new Error(`Invalid duration type: "${d === null ? 'Null' : typeof d}".`);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export { defaultOptions, defaultOptionKeys, clearCwdOverride, DEFAULT_MAX_BUFFER, parseDuration, optionScope };
|
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.
|
|
5
|
+
"version": "1.2.0",
|
|
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": "
|
|
25
|
+
"@types/node": "*"
|
|
26
26
|
},
|
|
27
27
|
"bugs": {
|
|
28
28
|
"url": "https://codeberg.org/duin/sh/issues"
|
|
@@ -49,4 +49,4 @@
|
|
|
49
49
|
"process-promise",
|
|
50
50
|
"process-output"
|
|
51
51
|
]
|
|
52
|
-
}
|
|
52
|
+
}
|
package/types/SH.d.ts
CHANGED
|
@@ -7,11 +7,8 @@
|
|
|
7
7
|
export type ArgsObject = {
|
|
8
8
|
_: string[];
|
|
9
9
|
} & {
|
|
10
|
-
[x: string]: string | true
|
|
10
|
+
[x: string]: string | true;
|
|
11
11
|
};
|
|
12
|
-
export type RejectCallback = Function;
|
|
13
|
-
export type ResolveCallback = Function;
|
|
14
|
-
export type SHOptions = import("./SHDispatch.js").SHOptions;
|
|
15
12
|
export type AbortableInput = {
|
|
16
13
|
/**
|
|
17
14
|
* - Promise that resolves to user input or void if aborted.
|
|
@@ -23,9 +20,9 @@ export type AbortableInput = {
|
|
|
23
20
|
abort: () => void;
|
|
24
21
|
};
|
|
25
22
|
/**
|
|
26
|
-
* Generator
|
|
23
|
+
* Generator that yields retry delay durations in milliseconds.
|
|
27
24
|
*/
|
|
28
|
-
export type ExpBackoffGenerator =
|
|
25
|
+
export type ExpBackoffGenerator = Generator<number, void, unknown>;
|
|
29
26
|
/**
|
|
30
27
|
* Template tag for building and executing shell commands.
|
|
31
28
|
*
|
|
@@ -39,10 +36,15 @@ export type ExpBackoffGenerator = Iterator<number | string>;
|
|
|
39
36
|
* command separators. It is not safe for untrusted input. Wrap untrusted values
|
|
40
37
|
* with {@link bashEscape} before interpolating them.
|
|
41
38
|
*
|
|
42
|
-
* SH acts as
|
|
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).
|
|
43
45
|
*
|
|
44
46
|
* @param {TemplateStringsArray} pieces - String literals from template.
|
|
45
|
-
* @param {...unknown
|
|
47
|
+
* @param {...unknown} args - Values to interpolate.
|
|
46
48
|
* @returns {SHDispatch} Command dispatcher.
|
|
47
49
|
* @throws {Error} If pieces contain undefined.
|
|
48
50
|
* @example
|
|
@@ -54,11 +56,23 @@ export type ExpBackoffGenerator = Iterator<number | string>;
|
|
|
54
56
|
*
|
|
55
57
|
* // Quote untrusted values explicitly
|
|
56
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();
|
|
57
63
|
*/
|
|
58
64
|
export const SH: any;
|
|
59
65
|
/**
|
|
60
66
|
* Changes the current working directory.
|
|
61
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
|
+
*
|
|
72
|
+
* This is a genuine process-wide operation (`process.chdir`), so it is NOT
|
|
73
|
+
* scoped by {@link within}. Use `SH.cwd = dir` inside a `within()` block for a
|
|
74
|
+
* scoped working directory.
|
|
75
|
+
*
|
|
62
76
|
* @param {string} dir - Path to new directory.
|
|
63
77
|
* @example
|
|
64
78
|
* cd('/tmp');
|
|
@@ -76,21 +90,29 @@ export function sleep(duration: string | number): Promise<void>;
|
|
|
76
90
|
/**
|
|
77
91
|
* Retries an async function up to N times with optional delays.
|
|
78
92
|
*
|
|
79
|
-
* Delay
|
|
93
|
+
* Delay precedence: when `delayOrCallback` is a function it is the retried
|
|
94
|
+
* callback and there is no delay between attempts. Otherwise it is a static
|
|
95
|
+
* duration ('1s', 100) or a delay generator (e.g. {@link expBackoff}), and
|
|
96
|
+
* `callback` is the retried function. A generator yield of `undefined`
|
|
97
|
+
* (exhausted generator) or `0` means "no further delay" — the next attempt
|
|
98
|
+
* runs immediately.
|
|
80
99
|
*
|
|
81
100
|
* @param {number} count - Positive integer number of attempts.
|
|
82
|
-
* @param {string|number|
|
|
83
|
-
* @param {
|
|
101
|
+
* @param {string|number|Iterator<number|string>|(() => (Promise<any>|any))} delayOrCallback - Delay, delay generator, or the callback when no separate callback is supplied.
|
|
102
|
+
* @param {() => (Promise<any>|any)} [callback] - Callback to retry when a delay is supplied.
|
|
84
103
|
* @returns {Promise<any>} Successful result.
|
|
85
104
|
* @throws {Error} If arguments are invalid, or the last callback error if all attempts fail.
|
|
86
105
|
* @example
|
|
87
106
|
* await retry(3, '1s', () => SH`curl http://unreliable`.run());
|
|
88
107
|
* await retry(3, expBackoff(), () => flakyOp());
|
|
89
108
|
*/
|
|
90
|
-
export function retry(count: number, delayOrCallback: string | number |
|
|
109
|
+
export function retry(count: number, delayOrCallback: string | number | Iterator<number | string> | (() => (Promise<any> | any)), callback?: () => (Promise<any> | any)): Promise<any>;
|
|
91
110
|
/**
|
|
92
111
|
* Reads entire stdin as UTF-8 string. Resolves to empty string if TTY (no pipe).
|
|
93
112
|
*
|
|
113
|
+
* Side effect: permanently sets the `process.stdin` encoding to utf8 and
|
|
114
|
+
* switches it to flowing mode.
|
|
115
|
+
*
|
|
94
116
|
* @returns {Promise<string>} Stdin content, or an empty string if TTY.
|
|
95
117
|
* @example
|
|
96
118
|
* const input = await readIn(); // Use in piped scripts
|
|
@@ -99,7 +121,14 @@ export function readIn(): Promise<string>;
|
|
|
99
121
|
/**
|
|
100
122
|
* Prompts user for input on stdin with custom prompt.
|
|
101
123
|
*
|
|
102
|
-
*
|
|
124
|
+
* Submit heuristics (non-obvious): each 'line' event is accumulated into a
|
|
125
|
+
* multi-line buffer and (re)starts a debounce timer
|
|
126
|
+
* ({@link SUBMIT_DEBOUNCE_MS}); when the timer fires without further input,
|
|
127
|
+
* the promise auto-resolves with the accumulated text plus any partially
|
|
128
|
+
* typed readline line. A paste — a stdin chunk longer than
|
|
129
|
+
* {@link PASTE_MIN_CHUNK} chars containing a newline — (re)starts the same
|
|
130
|
+
* timer, so pasted multi-line text resolves as one input. `abort()` resolves
|
|
131
|
+
* the promise with `undefined`.
|
|
103
132
|
*
|
|
104
133
|
* @param {string} prompt - Prompt text to display.
|
|
105
134
|
* @returns {AbortableInput} Object with input Promise and abort function.
|
|
@@ -110,14 +139,24 @@ export function readIn(): Promise<string>;
|
|
|
110
139
|
*/
|
|
111
140
|
export function userIn(prompt: string): AbortableInput;
|
|
112
141
|
/**
|
|
113
|
-
*
|
|
142
|
+
* Awaits `callback()` and returns its result, running it in a fresh async
|
|
143
|
+
* context (decision D2).
|
|
144
|
+
*
|
|
145
|
+
* Real isolation via `AsyncLocalStorage`: the callback runs inside a new
|
|
146
|
+
* scope whose `SH.*` default-option assignments are scoped to the block. Any
|
|
147
|
+
* `SH.timeout = …`, `SH.cwd = …`, etc. made inside the callback apply only to
|
|
148
|
+
* commands created within it and are discarded when the block ends — they do
|
|
149
|
+
* not leak to the enclosing scope. Nested `within()` blocks inherit their
|
|
150
|
+
* parent's scoped defaults but never leak their own outward.
|
|
114
151
|
*
|
|
115
|
-
*
|
|
152
|
+
* Note: `cd()` is a genuine process-wide operation (`process.chdir`), so it is
|
|
153
|
+
* not scoped by `within()`; use `SH.cwd = dir` for a scoped working directory.
|
|
116
154
|
*
|
|
117
155
|
* @param {() => Promise<any>} callback - Async function to execute.
|
|
118
156
|
* @returns {Promise<any>} Result of callback.
|
|
119
157
|
* @example
|
|
120
158
|
* const results = await within(async () => {
|
|
159
|
+
* SH.timeout = 5000; // scoped to this block
|
|
121
160
|
* return Promise.all([SH`sleep 1; echo 1`.run(), sleep(2)]);
|
|
122
161
|
* });
|
|
123
162
|
*/
|
|
@@ -129,18 +168,21 @@ export function within(callback: () => Promise<any>): Promise<any>;
|
|
|
129
168
|
* @param {string} [max='60s'] - Max backoff duration.
|
|
130
169
|
* @param {string} [rand='100ms'] - Max jitter.
|
|
131
170
|
* @yields {number} Next backoff ms.
|
|
171
|
+
* @returns {ExpBackoffGenerator} Infinite generator of backoff delays in ms.
|
|
132
172
|
* @example
|
|
133
173
|
* const backoff = expBackoff();
|
|
134
174
|
* await sleep(backoff.next().value);
|
|
135
175
|
*/
|
|
136
|
-
export function expBackoff(max?: string, rand?: string):
|
|
176
|
+
export function expBackoff(max?: string, rand?: string): ExpBackoffGenerator;
|
|
137
177
|
/**
|
|
138
178
|
* Parses CLI arguments into an object.
|
|
139
179
|
*
|
|
140
180
|
* Supports --key value, -k value (no shorts grouped).
|
|
141
181
|
* Bares go to _[]. Duplicates error. No = syntax.
|
|
142
182
|
* Negative number tokens such as -1, -0.5, and -1e3 are parsed as values or
|
|
143
|
-
* positionals, not options.
|
|
183
|
+
* positionals, not options. A lone `-` is a positional (stdin convention).
|
|
184
|
+
* `--` is the conventional end-of-options terminator: every token after it
|
|
185
|
+
* goes into `_`, even option-looking ones.
|
|
144
186
|
*
|
|
145
187
|
* Defaults to process.argv.slice(2).
|
|
146
188
|
*
|
|
@@ -150,6 +192,8 @@ export function expBackoff(max?: string, rand?: string): Generator<number, void,
|
|
|
150
192
|
* @example
|
|
151
193
|
* parseArgs(['--port', '8080', 'file.txt']); // { port: '8080', _: ['file.txt'] }
|
|
152
194
|
* parseArgs(['--n', '-1']); // { n: '-1', _: [] }
|
|
195
|
+
* parseArgs(['-', '--port', '1']); // { port: '1', _: ['-'] }
|
|
196
|
+
* parseArgs(['--', '--x']); // { _: ['--x'] }
|
|
153
197
|
*/
|
|
154
198
|
export function parseArgs(args?: string[]): ArgsObject;
|
|
155
199
|
/**
|
|
@@ -158,18 +202,7 @@ export function parseArgs(args?: string[]): ArgsObject;
|
|
|
158
202
|
* Named options map to their string value, or `true` when no value is supplied.
|
|
159
203
|
* Bare positional arguments are collected in `_`.
|
|
160
204
|
*
|
|
161
|
-
* @typedef {{_: string[]} & Object.<string, string|true
|
|
162
|
-
*/
|
|
163
|
-
/**
|
|
164
|
-
* @typedef {Function} RejectCallback
|
|
165
|
-
* @param {Error} error - The error object passed to the callback.
|
|
166
|
-
*/
|
|
167
|
-
/**
|
|
168
|
-
* @typedef {Function} ResolveCallback
|
|
169
|
-
* @param {any} [param] - Optional callback any value
|
|
170
|
-
*/
|
|
171
|
-
/**
|
|
172
|
-
* @typedef {import('./SHDispatch.js').SHOptions} SHOptions
|
|
205
|
+
* @typedef {{_: string[]} & Object.<string, string|true>} ArgsObject
|
|
173
206
|
*/
|
|
174
207
|
/**
|
|
175
208
|
* @typedef {Object} AbortableInput
|
|
@@ -177,22 +210,29 @@ export function parseArgs(args?: string[]): ArgsObject;
|
|
|
177
210
|
* @property {() => void} abort - Function to abort input collection.
|
|
178
211
|
*/
|
|
179
212
|
/**
|
|
180
|
-
* Generator
|
|
213
|
+
* Generator that yields retry delay durations in milliseconds.
|
|
181
214
|
*
|
|
182
|
-
* @typedef {
|
|
215
|
+
* @typedef {Generator<number, void, unknown>} ExpBackoffGenerator
|
|
183
216
|
*/
|
|
184
217
|
/**
|
|
185
218
|
* Utility to determine the JavaScript type of a value.
|
|
186
219
|
*
|
|
187
|
-
*
|
|
220
|
+
* Uses `Object.prototype.toString`, so `jsType(null)` returns `'Null'`;
|
|
221
|
+
* `undefined` is special-cased to `'undefined'`. Plain objects report their
|
|
222
|
+
* constructor name, or `'Object'` when there is none (null-prototype objects).
|
|
223
|
+
*
|
|
224
|
+
* @param {unknown} value - Any value to inspect.
|
|
188
225
|
* @returns {string} The "real" object type name (e.g., 'Array', 'Promise') or primitive typeof.
|
|
189
226
|
* @example
|
|
190
227
|
* jsType([]); // 'Array'
|
|
191
228
|
* jsType(Promise.resolve()); // 'Promise'
|
|
229
|
+
* jsType(null); // 'Null'
|
|
230
|
+
* jsType(undefined); // 'undefined'
|
|
192
231
|
*/
|
|
193
|
-
export function jsType(
|
|
232
|
+
export function jsType(value: unknown): string;
|
|
194
233
|
/**
|
|
195
|
-
* Checks if an object has a specific own property
|
|
234
|
+
* Checks if an object has a specific own property. Safe for null-prototype
|
|
235
|
+
* objects and objects shadowing `hasOwnProperty`.
|
|
196
236
|
*
|
|
197
237
|
* @param {any} o - Object to examine.
|
|
198
238
|
* @param {string} p - Property name to check.
|
package/types/SHDispatch.d.ts
CHANGED
|
@@ -1,30 +1,4 @@
|
|
|
1
1
|
export default SHDispatch;
|
|
2
|
-
export type SpawnSyncResponse = {
|
|
3
|
-
/**
|
|
4
|
-
* - Exit code (null if signal).
|
|
5
|
-
*/
|
|
6
|
-
status: number | null;
|
|
7
|
-
/**
|
|
8
|
-
* - Terminating signal.
|
|
9
|
-
*/
|
|
10
|
-
signal: string | null;
|
|
11
|
-
/**
|
|
12
|
-
* - [stdin, stdout, stderr].
|
|
13
|
-
*/
|
|
14
|
-
output: (string | Buffer | null)[];
|
|
15
|
-
/**
|
|
16
|
-
* - Process ID.
|
|
17
|
-
*/
|
|
18
|
-
pid: number;
|
|
19
|
-
/**
|
|
20
|
-
* - Captured stdout.
|
|
21
|
-
*/
|
|
22
|
-
stdout: string | Buffer | null;
|
|
23
|
-
/**
|
|
24
|
-
* - Captured stderr.
|
|
25
|
-
*/
|
|
26
|
-
stderr: string | Buffer | null;
|
|
27
|
-
};
|
|
28
2
|
export type StdioOption = ("pipe" | "ignore" | "inherit" | number);
|
|
29
3
|
export type StdioOptions = Array<StdioOption> | StdioOption;
|
|
30
4
|
/**
|
|
@@ -35,8 +9,8 @@ export type StdioOptions = Array<StdioOption> | StdioOption;
|
|
|
35
9
|
* - `env`: `process.env`
|
|
36
10
|
* - `shell`: `'bash'` (string/true → shell; false → no-shell `/usr/bin/env -S`)
|
|
37
11
|
* - `stdio`: `['inherit', 'pipe', 'pipe']`
|
|
38
|
-
* - `timeout`: `0` (no timeout;
|
|
39
|
-
* - `maxBuffer`: `512000` (500
|
|
12
|
+
* - `timeout`: `0` (no timeout; absolute wall-clock cap when set)
|
|
13
|
+
* - `maxBuffer`: `512000` (500 KiB per stream in SHExecute)
|
|
40
14
|
*
|
|
41
15
|
* Prefix (`.options(undefined, prefix)`) only for shell mode.
|
|
42
16
|
*/
|
|
@@ -58,7 +32,11 @@ export type SHOptions = {
|
|
|
58
32
|
*/
|
|
59
33
|
stdio: StdioOptions;
|
|
60
34
|
/**
|
|
61
|
-
* -
|
|
35
|
+
* - Absolute wall-clock timeout in ms or
|
|
36
|
+
* duration string; 0 disables. The process is killed after the full duration
|
|
37
|
+
* no matter how much output it produces, in BOTH `run()` and `runSync()`.
|
|
38
|
+
* This matches Node's native `spawn`/`spawnSync` `timeout` semantics; SH is
|
|
39
|
+
* not meant to run long-lived services, so there is no rolling/idle reset.
|
|
62
40
|
*/
|
|
63
41
|
timeout: number | string;
|
|
64
42
|
/**
|
|
@@ -66,7 +44,9 @@ export type SHOptions = {
|
|
|
66
44
|
*/
|
|
67
45
|
maxBuffer?: number | undefined;
|
|
68
46
|
/**
|
|
69
|
-
* - Run process detached and resolve early.
|
|
47
|
+
* - Run process detached and resolve early (~1s).
|
|
48
|
+
* Unless stdio is explicitly set for the command, stdio is forced to
|
|
49
|
+
* `'ignore'`, because open pipes would keep the parent's event loop alive.
|
|
70
50
|
*/
|
|
71
51
|
detached?: boolean | undefined;
|
|
72
52
|
};
|
|
@@ -104,6 +84,10 @@ declare class SHDispatch {
|
|
|
104
84
|
/**
|
|
105
85
|
* Async run: Captures stdout; rejects on error/timeout.
|
|
106
86
|
*
|
|
87
|
+
* Replaces the internal process handle: calling `run()` again while a
|
|
88
|
+
* previous run is still active makes that first process unkillable via
|
|
89
|
+
* `kill()`.
|
|
90
|
+
*
|
|
107
91
|
* @param {string} [payload] - Stdin payload.
|
|
108
92
|
* @returns {Promise<string>} Stdout.
|
|
109
93
|
*/
|
package/types/SHExecute.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
export default SHExecute;
|
|
2
|
-
export type SHExecuteOptions =
|
|
3
|
-
maxBuffer?: number;
|
|
4
|
-
};
|
|
2
|
+
export type SHExecuteOptions = import("./SHDispatch.js").SHOptions;
|
|
5
3
|
/**
|
|
6
|
-
* @typedef {import('
|
|
7
|
-
* @description
|
|
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.
|
|
@@ -14,11 +13,27 @@ export type SHExecuteOptions = any & {
|
|
|
14
13
|
* Key features:
|
|
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
|
-
* - **
|
|
18
|
-
*
|
|
16
|
+
* - **Timeouts**: `options.timeout` (ms/'2s') is an absolute wall-clock timeout —
|
|
17
|
+
* the process is killed after the full duration regardless of output, in both
|
|
18
|
+
* `run()` and `runSync()`. It is stripped from the spawn options so Node's
|
|
19
|
+
* native absolute spawn timeout never interferes with the custom timer (which
|
|
20
|
+
* reports a clear `Process timed out after <N>ms.` error and tears down the
|
|
21
|
+
* whole process group via `kill()`). `runSync()` passes it to `spawnSync`, whose
|
|
22
|
+
* native semantics are the same (absolute).
|
|
23
|
+
* - **Process-group teardown**: The child is always spawned as a new process-group
|
|
24
|
+
* leader (`detached: true` in the spawn options, independent of the public
|
|
25
|
+
* `detached` option). `kill()` sends the signal to the whole group via a
|
|
26
|
+
* negative PID, tearing down the entire tree (children, grandchildren, …) in
|
|
27
|
+
* one shot — no `pgrep` discovery needed. A graceful SIGTERM is escalated to
|
|
28
|
+
* SIGKILL after {@link ESCALATION_GRACE_MS} for processes that ignore SIGTERM.
|
|
29
|
+
* - **Buffering**: Captures stdout/stderr up to `maxBuffer` (512000 bytes / 500 KiB default); appends truncation markers.
|
|
19
30
|
* - **Payload**: `run(payload)` writes string to stdin (forces pipe).
|
|
20
|
-
* - **Detached**: If `options.detached
|
|
21
|
-
*
|
|
31
|
+
* - **Detached**: If the public `options.detached` is set, resolves early (~1s)
|
|
32
|
+
* and unrefs. Unless the caller explicitly set `stdio`, SHDispatch forces
|
|
33
|
+
* `stdio: 'ignore'` for detached runs, because open pipes keep the parent's
|
|
34
|
+
* event loop alive and defeat detachment.
|
|
35
|
+
* - **Kill**: Terminates the whole process group (negative-PID kill) and
|
|
36
|
+
* escalates SIGTERM → SIGKILL after a grace period.
|
|
22
37
|
*
|
|
23
38
|
* @example
|
|
24
39
|
* const exec = new SHExecute('ls', 'set -euo pipefail', { timeout: '5s' });
|
|
@@ -34,6 +49,10 @@ declare class SHExecute {
|
|
|
34
49
|
/**
|
|
35
50
|
* Synchronous execution.
|
|
36
51
|
*
|
|
52
|
+
* Process-group teardown does not apply here: `spawnSync` blocks until the
|
|
53
|
+
* process exits or its native absolute `timeout` fires, so there is no async
|
|
54
|
+
* kill to escalate. Grandchildren may survive a `spawnSync` timeout.
|
|
55
|
+
*
|
|
37
56
|
* @param {string} [payload] - Stdin data (forces pipe).
|
|
38
57
|
* @returns {import('child_process').SpawnSyncReturns<Buffer>}
|
|
39
58
|
* @throws {Error} Invalid payload type.
|
|
@@ -42,18 +61,27 @@ declare class SHExecute {
|
|
|
42
61
|
/**
|
|
43
62
|
* Asynchronous execution with buffering/timeout/kill.
|
|
44
63
|
*
|
|
45
|
-
* Resolves stdout (trimmed) on success
|
|
64
|
+
* Resolves stdout (trimmed) on success.
|
|
46
65
|
*
|
|
47
66
|
* @param {string} [payload] - Stdin data (forces pipe).
|
|
48
67
|
* @returns {Promise<string>} Trimmed UTF-8 stdout (+ truncation marker if exceeded).
|
|
49
|
-
*
|
|
68
|
+
* Rejects with an Error on command failure (message includes the exit code,
|
|
69
|
+
* or the signal name for signal kills, plus any already-received stdout and
|
|
70
|
+
* stderr), timeout expiry, or forced kill. On timeout/forced-kill the
|
|
71
|
+
* already-received stdout/stderr is preserved in the error message.
|
|
50
72
|
*/
|
|
51
73
|
run(payload?: string): Promise<string>;
|
|
52
74
|
/**
|
|
53
|
-
* Terminates
|
|
75
|
+
* Terminates the whole process group (negative-PID kill).
|
|
76
|
+
*
|
|
77
|
+
* The child is spawned as a process-group leader, so a negative-PID kill
|
|
78
|
+
* tears down the entire tree (children, grandchildren, …) in one shot — no
|
|
79
|
+
* `pgrep` discovery required. A graceful SIGTERM is escalated to SIGKILL
|
|
80
|
+
* after {@link ESCALATION_GRACE_MS} for processes that ignore SIGTERM.
|
|
81
|
+
* Best-effort: an already-exited group (ESRCH) is treated as a no-op.
|
|
54
82
|
*
|
|
55
83
|
* @param {number | string} [signal='SIGTERM'] - Signal to send.
|
|
56
|
-
* @returns {Promise<number[]>} Killed PIDs.
|
|
84
|
+
* @returns {Promise<number[]>} Killed PIDs (the group-leader PID).
|
|
57
85
|
* @throws {Error} No process/PID.
|
|
58
86
|
*/
|
|
59
87
|
kill(signal?: number | string): Promise<number[]>;
|