@j-o-r/sh 1.1.31 → 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.
- package/.editorconfig +21 -0
- package/TODO.md +347 -0
- package/lib/SH.js +161 -148
- package/lib/SHDispatch.js +27 -25
- package/lib/SHExecute.js +233 -228
- package/lib/internal.js +113 -0
- package/package.json +3 -3
- package/types/SH.d.ts +64 -34
- package/types/SHDispatch.d.ts +12 -29
- package/types/SHExecute.d.ts +21 -10
- package/types/internal.d.ts +62 -0
package/lib/SH.js
CHANGED
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
|
-
//
|
|
14
|
+
//
|
|
15
15
|
//
|
|
16
16
|
// Original Source: zx
|
|
17
17
|
// Link to Original Source: https://github.com/google/zx
|
|
18
|
-
// Reason for Using This Code:
|
|
19
|
-
// The core functionality of this code is highly beneficial. However, certain parts of the original code
|
|
20
|
-
// were overwriting the global namespace with core libraries and variables. This was causing conflicts with
|
|
18
|
+
// Reason for Using This Code:
|
|
19
|
+
// The core functionality of this code is highly beneficial. However, certain parts of the original code
|
|
20
|
+
// were overwriting the global namespace with core libraries and variables. This was causing conflicts with
|
|
21
21
|
// other packages (for instance, fetch) and introducing unexpected elements into my code base.
|
|
22
22
|
// The main $/SH method is all there is left, with barebone Promises and readable code.
|
|
23
|
-
// Changes Made:
|
|
23
|
+
// Changes Made:
|
|
24
24
|
// - The code has been or is being reformatted to comply with ES2020 standards.
|
|
25
25
|
// - Some methods were added and existing ones were modified or deleted to enhance usability.
|
|
26
26
|
// - The namespace has been changed from '$' to 'SH'.
|
|
@@ -29,8 +29,9 @@
|
|
|
29
29
|
import assert from 'node:assert';
|
|
30
30
|
import readline from 'node:readline/promises';
|
|
31
31
|
import SHDispatch from './SHDispatch.js';
|
|
32
|
-
import Test from './Test.js'
|
|
33
|
-
import AsyncTracker from './AsyncTracker.js'
|
|
32
|
+
import Test from './Test.js';
|
|
33
|
+
import AsyncTracker from './AsyncTracker.js';
|
|
34
|
+
import { defaultOptions, defaultOptionKeys, clearCwdOverride, parseDuration } from './internal.js';
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* Parsed command-line arguments.
|
|
@@ -38,21 +39,7 @@ import AsyncTracker from './AsyncTracker.js'
|
|
|
38
39
|
* Named options map to their string value, or `true` when no value is supplied.
|
|
39
40
|
* Bare positional arguments are collected in `_`.
|
|
40
41
|
*
|
|
41
|
-
* @typedef {{_: string[]} & Object.<string, string|true
|
|
42
|
-
*/
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* @typedef {Function} RejectCallback
|
|
46
|
-
* @param {Error} error - The error object passed to the callback.
|
|
47
|
-
*/
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* @typedef {Function} ResolveCallback
|
|
51
|
-
* @param {any} [param] - Optional callback any value
|
|
52
|
-
*/
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* @typedef {import('./SHDispatch.js').SHOptions} SHOptions
|
|
42
|
+
* @typedef {{_: string[]} & Object.<string, string|true>} ArgsObject
|
|
56
43
|
*/
|
|
57
44
|
|
|
58
45
|
/**
|
|
@@ -62,64 +49,46 @@ import AsyncTracker from './AsyncTracker.js'
|
|
|
62
49
|
*/
|
|
63
50
|
|
|
64
51
|
/**
|
|
65
|
-
* Generator
|
|
52
|
+
* Generator that yields retry delay durations in milliseconds.
|
|
66
53
|
*
|
|
67
|
-
* @typedef {
|
|
54
|
+
* @typedef {Generator<number, void, unknown>} ExpBackoffGenerator
|
|
68
55
|
*/
|
|
69
56
|
|
|
70
57
|
/**
|
|
71
58
|
* Utility to determine the JavaScript type of a value.
|
|
72
59
|
*
|
|
73
|
-
*
|
|
60
|
+
* Uses `Object.prototype.toString`, so `jsType(null)` returns `'Null'`;
|
|
61
|
+
* `undefined` is special-cased to `'undefined'`. Plain objects report their
|
|
62
|
+
* constructor name, or `'Object'` when there is none (null-prototype objects).
|
|
63
|
+
*
|
|
64
|
+
* @param {unknown} value - Any value to inspect.
|
|
74
65
|
* @returns {string} The "real" object type name (e.g., 'Array', 'Promise') or primitive typeof.
|
|
75
66
|
* @example
|
|
76
67
|
* jsType([]); // 'Array'
|
|
77
68
|
* jsType(Promise.resolve()); // 'Promise'
|
|
69
|
+
* jsType(null); // 'Null'
|
|
70
|
+
* jsType(undefined); // 'undefined'
|
|
78
71
|
*/
|
|
79
|
-
const jsType = (
|
|
80
|
-
if (
|
|
81
|
-
const type = Object.prototype.toString.call(
|
|
82
|
-
|
|
72
|
+
const jsType = (value) => {
|
|
73
|
+
if (value === undefined) return 'undefined';
|
|
74
|
+
const type = Object.prototype.toString.call(value).slice(8, -1);
|
|
75
|
+
// Null-prototype objects have no constructor; fall back to 'Object'.
|
|
76
|
+
return type === 'Object' ? (/** @type {any} */ (value).constructor?.name ?? 'Object') : type;
|
|
83
77
|
};
|
|
84
78
|
|
|
85
79
|
/**
|
|
86
|
-
* Checks if an object has a specific own property
|
|
80
|
+
* Checks if an object has a specific own property. Safe for null-prototype
|
|
81
|
+
* objects and objects shadowing `hasOwnProperty`.
|
|
87
82
|
*
|
|
88
83
|
* @param {any} o - Object to examine.
|
|
89
84
|
* @param {string} p - Property name to check.
|
|
90
85
|
* @returns {boolean} True if the object has the own property.
|
|
91
86
|
*/
|
|
92
87
|
const hasProp = (o, p) => {
|
|
93
|
-
|
|
94
|
-
|
|
88
|
+
if (o == null) return false;
|
|
89
|
+
return Object.hasOwn(o, p);
|
|
95
90
|
};
|
|
96
91
|
|
|
97
|
-
/**
|
|
98
|
-
* Parses a human-readable duration string or number into milliseconds.
|
|
99
|
-
*
|
|
100
|
-
* Supports finite non-negative numbers as milliseconds, plus exact string forms
|
|
101
|
-
* with units: '5s' (5000ms) and '100ms' (100ms).
|
|
102
|
-
*
|
|
103
|
-
* @param {number|string} d - Duration as number (ms) or string ('5s', '100ms').
|
|
104
|
-
* @returns {number} Duration in milliseconds.
|
|
105
|
-
* @throws {Error} If the duration type or format is invalid.
|
|
106
|
-
*/
|
|
107
|
-
const parseDuration = (d) => {
|
|
108
|
-
if (typeof d == 'number') {
|
|
109
|
-
if (!Number.isFinite(d) || d < 0)
|
|
110
|
-
throw new Error(`Invalid duration: "${d}".`);
|
|
111
|
-
return d;
|
|
112
|
-
}
|
|
113
|
-
if (typeof d == 'string') {
|
|
114
|
-
const match = d.match(/^(\d+)(ms|s)$/);
|
|
115
|
-
if (!match)
|
|
116
|
-
throw new Error(`Unknown duration: "${d}".`);
|
|
117
|
-
const amount = Number(match[1]);
|
|
118
|
-
return match[2] == 's' ? amount * 1000 : amount;
|
|
119
|
-
}
|
|
120
|
-
throw new Error(`Invalid duration type: "${jsType(d)}".`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
92
|
/**
|
|
124
93
|
* Quotes a value as one POSIX shell argument.
|
|
125
94
|
*
|
|
@@ -136,32 +105,16 @@ const parseDuration = (d) => {
|
|
|
136
105
|
*/
|
|
137
106
|
const bashEscape = (x) => `'${String(x).replace(/'/g, `'\\''`)}'`;
|
|
138
107
|
|
|
139
|
-
/** Default options applied to all SH commands unless overridden. */
|
|
140
|
-
const defaultOptions = {
|
|
141
|
-
cwd: process.cwd(),
|
|
142
|
-
env: process.env,
|
|
143
|
-
shell: 'bash',
|
|
144
|
-
stdio: ['inherit', 'pipe', 'pipe'],
|
|
145
|
-
timeout: 0, // when 0 there is no timeout
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
/** Known global option properties exposed for `SH.option` readback. */
|
|
149
|
-
const defaultOptionKeys = new Set([
|
|
150
|
-
'cwd',
|
|
151
|
-
'env',
|
|
152
|
-
'shell',
|
|
153
|
-
'stdio',
|
|
154
|
-
'timeout',
|
|
155
|
-
'maxBuffer',
|
|
156
|
-
'detached',
|
|
157
|
-
]);
|
|
158
|
-
|
|
159
108
|
/**
|
|
160
109
|
* Proxy traps for the SH template tag and global option defaults.
|
|
161
110
|
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
111
|
+
* Reads and writes are symmetric: only the known default option keys
|
|
112
|
+
* (`defaultOptionKeys`, derived from `defaultOptions` in `lib/internal.js`)
|
|
113
|
+
* are routed to the shared defaults. Assigning an unknown key throws a
|
|
114
|
+
* `TypeError` listing the known keys (decision D1) — this catches typos like
|
|
115
|
+
* `SH.timout = 1` instead of silently storing junk keys that read back as
|
|
116
|
+
* `undefined` and leak into every command's options. Reads of unknown keys
|
|
117
|
+
* keep normal function-proxy behavior (usually `undefined`).
|
|
165
118
|
*/
|
|
166
119
|
const defaultOptionHandler = {
|
|
167
120
|
get(target, prop, receiver) {
|
|
@@ -171,10 +124,13 @@ const defaultOptionHandler = {
|
|
|
171
124
|
return Reflect.get(target, prop, receiver);
|
|
172
125
|
},
|
|
173
126
|
set(target, prop, value) {
|
|
127
|
+
if (!defaultOptionKeys.has(prop)) {
|
|
128
|
+
throw new TypeError(`Unknown SH option: "${String(prop)}". Known options: ${[...defaultOptionKeys].join(', ')}.`);
|
|
129
|
+
}
|
|
174
130
|
defaultOptions[prop] = value;
|
|
175
131
|
return true;
|
|
176
132
|
}
|
|
177
|
-
}
|
|
133
|
+
};
|
|
178
134
|
|
|
179
135
|
/**
|
|
180
136
|
* Template tag for building and executing shell commands.
|
|
@@ -189,10 +145,15 @@ const defaultOptionHandler = {
|
|
|
189
145
|
* command separators. It is not safe for untrusted input. Wrap untrusted values
|
|
190
146
|
* with {@link bashEscape} before interpolating them.
|
|
191
147
|
*
|
|
192
|
-
* SH acts as
|
|
148
|
+
* SH also acts as a global options setter (see the last example). Global
|
|
149
|
+
* defaults are captured when each command is created, not when it runs — see
|
|
150
|
+
* the "defaults captured at creation time" note in {@link SHDispatch#options}.
|
|
151
|
+
* Only the known option keys (cwd, env, shell, stdio, timeout, maxBuffer,
|
|
152
|
+
* detached) may be assigned; assigning anything else throws a TypeError
|
|
153
|
+
* listing the known keys (typo guard, decision D1).
|
|
193
154
|
*
|
|
194
155
|
* @param {TemplateStringsArray} pieces - String literals from template.
|
|
195
|
-
* @param {...unknown
|
|
156
|
+
* @param {...unknown} args - Values to interpolate.
|
|
196
157
|
* @returns {SHDispatch} Command dispatcher.
|
|
197
158
|
* @throws {Error} If pieces contain undefined.
|
|
198
159
|
* @example
|
|
@@ -204,6 +165,10 @@ const defaultOptionHandler = {
|
|
|
204
165
|
*
|
|
205
166
|
* // Quote untrusted values explicitly
|
|
206
167
|
* SH`printf '%s\n' ${bashEscape('semi; colon')}`.run();
|
|
168
|
+
*
|
|
169
|
+
* // Set global defaults for subsequently created commands
|
|
170
|
+
* SH.timeout = 5000;
|
|
171
|
+
* await SH`cmd`.run();
|
|
207
172
|
*/
|
|
208
173
|
const SH = new Proxy(function(pieces, ...args) {
|
|
209
174
|
if (pieces.some((p) => p == undefined)) {
|
|
@@ -212,7 +177,7 @@ const SH = new Proxy(function(pieces, ...args) {
|
|
|
212
177
|
let cmd = pieces[0], i = 0;
|
|
213
178
|
while (i < args.length) {
|
|
214
179
|
let s;
|
|
215
|
-
|
|
180
|
+
|
|
216
181
|
if (Array.isArray(args[i])) {
|
|
217
182
|
s = args[i].map((x) => String(x)).join(' ');
|
|
218
183
|
} else {
|
|
@@ -224,9 +189,13 @@ const SH = new Proxy(function(pieces, ...args) {
|
|
|
224
189
|
}, defaultOptionHandler);
|
|
225
190
|
|
|
226
191
|
/**
|
|
227
|
-
*
|
|
192
|
+
* Awaits `callback()` and returns its result.
|
|
228
193
|
*
|
|
229
|
-
*
|
|
194
|
+
* Despite the historical name, this is currently a thin wrapper: no new async
|
|
195
|
+
* context or fresh callstack is created — the callback runs in the current
|
|
196
|
+
* one. It is kept as an intent marker for grouping async work (and for API
|
|
197
|
+
* compatibility); real isolation via AsyncLocalStorage is a pending decision
|
|
198
|
+
* (see the project TODO, decision D2).
|
|
230
199
|
*
|
|
231
200
|
* @param {() => Promise<any>} callback - Async function to execute.
|
|
232
201
|
* @returns {Promise<any>} Result of callback.
|
|
@@ -237,11 +206,14 @@ const SH = new Proxy(function(pieces, ...args) {
|
|
|
237
206
|
*/
|
|
238
207
|
const within = async (callback) => {
|
|
239
208
|
return await callback();
|
|
240
|
-
}
|
|
209
|
+
};
|
|
241
210
|
|
|
242
211
|
/**
|
|
243
212
|
* Reads entire stdin as UTF-8 string. Resolves to empty string if TTY (no pipe).
|
|
244
213
|
*
|
|
214
|
+
* Side effect: permanently sets the `process.stdin` encoding to utf8 and
|
|
215
|
+
* switches it to flowing mode.
|
|
216
|
+
*
|
|
245
217
|
* @returns {Promise<string>} Stdin content, or an empty string if TTY.
|
|
246
218
|
* @example
|
|
247
219
|
* const input = await readIn(); // Use in piped scripts
|
|
@@ -254,12 +226,31 @@ const readIn = async () => {
|
|
|
254
226
|
buf += chunk;
|
|
255
227
|
}
|
|
256
228
|
return buf;
|
|
257
|
-
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Minimum stdin chunk length (containing a newline) that {@link userIn}
|
|
233
|
+
* treats as a paste instead of typed input.
|
|
234
|
+
*/
|
|
235
|
+
const PASTE_MIN_CHUNK = 4;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Debounce delay after the last 'line' event or paste before {@link userIn}
|
|
239
|
+
* auto-resolves with the accumulated input.
|
|
240
|
+
*/
|
|
241
|
+
const SUBMIT_DEBOUNCE_MS = 50;
|
|
258
242
|
|
|
259
243
|
/**
|
|
260
244
|
* Prompts user for input on stdin with custom prompt.
|
|
261
245
|
*
|
|
262
|
-
*
|
|
246
|
+
* Submit heuristics (non-obvious): each 'line' event is accumulated into a
|
|
247
|
+
* multi-line buffer and (re)starts a debounce timer
|
|
248
|
+
* ({@link SUBMIT_DEBOUNCE_MS}); when the timer fires without further input,
|
|
249
|
+
* the promise auto-resolves with the accumulated text plus any partially
|
|
250
|
+
* typed readline line. A paste — a stdin chunk longer than
|
|
251
|
+
* {@link PASTE_MIN_CHUNK} chars containing a newline — (re)starts the same
|
|
252
|
+
* timer, so pasted multi-line text resolves as one input. `abort()` resolves
|
|
253
|
+
* the promise with `undefined`.
|
|
263
254
|
*
|
|
264
255
|
* @param {string} prompt - Prompt text to display.
|
|
265
256
|
* @returns {AbortableInput} Object with input Promise and abort function.
|
|
@@ -269,51 +260,49 @@ const readIn = async () => {
|
|
|
269
260
|
* // abort(); // Cancel anytime
|
|
270
261
|
*/
|
|
271
262
|
const userIn = (prompt) => {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
};
|
|
263
|
+
let resolvePromise;
|
|
264
|
+
const input = new Promise((resolve) => { resolvePromise = resolve; });
|
|
265
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
266
|
+
let buffer = '';
|
|
267
|
+
let timer;
|
|
268
|
+
/** @param {string} chunk */
|
|
269
|
+
const onData = (chunk) => {
|
|
270
|
+
if (chunk.length > PASTE_MIN_CHUNK && chunk.includes('\n')) {
|
|
271
|
+
clearTimeout(timer);
|
|
272
|
+
timer = setTimeout(resolveFunc, SUBMIT_DEBOUNCE_MS);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const resolveFunc = () => {
|
|
277
|
+
let fullText = buffer;
|
|
278
|
+
if (rl.line) fullText += rl.line;
|
|
279
|
+
cleanup();
|
|
280
|
+
resolvePromise(fullText);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const cleanup = () => {
|
|
284
|
+
process.stdin.removeListener('data', onData);
|
|
285
|
+
rl.removeAllListeners('line');
|
|
286
|
+
clearTimeout(timer);
|
|
287
|
+
rl.close();
|
|
288
|
+
};
|
|
289
|
+
rl.on('line', (line) => {
|
|
290
|
+
buffer += line + '\n';
|
|
291
|
+
clearTimeout(timer);
|
|
292
|
+
timer = setTimeout(resolveFunc, SUBMIT_DEBOUNCE_MS);
|
|
293
|
+
});
|
|
294
|
+
process.stdin.on('data', onData);
|
|
295
|
+
|
|
296
|
+
rl.setPrompt(prompt);
|
|
297
|
+
rl.prompt();
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
input,
|
|
301
|
+
abort: () => {
|
|
302
|
+
cleanup();
|
|
303
|
+
resolvePromise();
|
|
304
|
+
}
|
|
305
|
+
};
|
|
317
306
|
};
|
|
318
307
|
|
|
319
308
|
/**
|
|
@@ -327,11 +316,16 @@ const isDelayGenerator = (value) => value != null && typeof value == 'object' &&
|
|
|
327
316
|
/**
|
|
328
317
|
* Retries an async function up to N times with optional delays.
|
|
329
318
|
*
|
|
330
|
-
* Delay
|
|
319
|
+
* Delay precedence: when `delayOrCallback` is a function it is the retried
|
|
320
|
+
* callback and there is no delay between attempts. Otherwise it is a static
|
|
321
|
+
* duration ('1s', 100) or a delay generator (e.g. {@link expBackoff}), and
|
|
322
|
+
* `callback` is the retried function. A generator yield of `undefined`
|
|
323
|
+
* (exhausted generator) or `0` means "no further delay" — the next attempt
|
|
324
|
+
* runs immediately.
|
|
331
325
|
*
|
|
332
326
|
* @param {number} count - Positive integer number of attempts.
|
|
333
|
-
* @param {string|number|
|
|
334
|
-
* @param {
|
|
327
|
+
* @param {string|number|Iterator<number|string>|(() => (Promise<any>|any))} delayOrCallback - Delay, delay generator, or the callback when no separate callback is supplied.
|
|
328
|
+
* @param {() => (Promise<any>|any)} [callback] - Callback to retry when a delay is supplied.
|
|
335
329
|
* @returns {Promise<any>} Successful result.
|
|
336
330
|
* @throws {Error} If arguments are invalid, or the last callback error if all attempts fail.
|
|
337
331
|
* @example
|
|
@@ -381,7 +375,7 @@ const retry = async (count, delayOrCallback, callback) => {
|
|
|
381
375
|
}
|
|
382
376
|
}
|
|
383
377
|
}
|
|
384
|
-
}
|
|
378
|
+
};
|
|
385
379
|
|
|
386
380
|
/**
|
|
387
381
|
* Sleeps for a specified duration.
|
|
@@ -395,19 +389,23 @@ const sleep = (duration) => {
|
|
|
395
389
|
return new Promise((resolve) => {
|
|
396
390
|
setTimeout(resolve, parseDuration(duration));
|
|
397
391
|
});
|
|
398
|
-
}
|
|
392
|
+
};
|
|
399
393
|
|
|
400
394
|
/**
|
|
401
395
|
* Changes the current working directory.
|
|
402
396
|
*
|
|
397
|
+
* Affects the cwd of subsequent `SH` commands: default options capture
|
|
398
|
+
* `process.cwd()` lazily when each command is created. Also clears any
|
|
399
|
+
* `SH.cwd` override, so the most recent cwd change always wins.
|
|
400
|
+
*
|
|
403
401
|
* @param {string} dir - Path to new directory.
|
|
404
402
|
* @example
|
|
405
403
|
* cd('/tmp');
|
|
406
404
|
*/
|
|
407
405
|
const cd = (dir) => {
|
|
408
|
-
// @ts-ignore
|
|
409
406
|
process.chdir(dir);
|
|
410
|
-
|
|
407
|
+
clearCwdOverride();
|
|
408
|
+
};
|
|
411
409
|
|
|
412
410
|
/**
|
|
413
411
|
* Generator for exponential backoff delays with jitter.
|
|
@@ -416,6 +414,7 @@ const cd = (dir) => {
|
|
|
416
414
|
* @param {string} [max='60s'] - Max backoff duration.
|
|
417
415
|
* @param {string} [rand='100ms'] - Max jitter.
|
|
418
416
|
* @yields {number} Next backoff ms.
|
|
417
|
+
* @returns {ExpBackoffGenerator} Infinite generator of backoff delays in ms.
|
|
419
418
|
* @example
|
|
420
419
|
* const backoff = expBackoff();
|
|
421
420
|
* await sleep(backoff.next().value);
|
|
@@ -444,12 +443,15 @@ const isNegativeNumberArg = (arg) => /^-(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i
|
|
|
444
443
|
* Checks whether a token should be parsed as an option.
|
|
445
444
|
*
|
|
446
445
|
* Negative numeric values are intentionally excluded so calls like
|
|
447
|
-
* `parseArgs(['--n', '-1'])` parse `-1` as the value for `--n`.
|
|
446
|
+
* `parseArgs(['--n', '-1'])` parse `-1` as the value for `--n`. A lone `-`
|
|
447
|
+
* is not an option either; it is parsed as a positional (the stdin
|
|
448
|
+
* convention). The `--` end-of-options terminator is handled separately in
|
|
449
|
+
* {@link parseArgs}.
|
|
448
450
|
*
|
|
449
451
|
* @param {string} arg - CLI argument token.
|
|
450
452
|
* @returns {boolean} True when the token should be parsed as an option.
|
|
451
453
|
*/
|
|
452
|
-
const isOptionArg = (arg) => arg.startsWith('-') && !isNegativeNumberArg(arg);
|
|
454
|
+
const isOptionArg = (arg) => arg.length > 1 && arg.startsWith('-') && !isNegativeNumberArg(arg);
|
|
453
455
|
|
|
454
456
|
/**
|
|
455
457
|
* Parses CLI arguments into an object.
|
|
@@ -457,7 +459,9 @@ const isOptionArg = (arg) => arg.startsWith('-') && !isNegativeNumberArg(arg);
|
|
|
457
459
|
* Supports --key value, -k value (no shorts grouped).
|
|
458
460
|
* Bares go to _[]. Duplicates error. No = syntax.
|
|
459
461
|
* Negative number tokens such as -1, -0.5, and -1e3 are parsed as values or
|
|
460
|
-
* positionals, not options.
|
|
462
|
+
* positionals, not options. A lone `-` is a positional (stdin convention).
|
|
463
|
+
* `--` is the conventional end-of-options terminator: every token after it
|
|
464
|
+
* goes into `_`, even option-looking ones.
|
|
461
465
|
*
|
|
462
466
|
* Defaults to process.argv.slice(2).
|
|
463
467
|
*
|
|
@@ -467,14 +471,23 @@ const isOptionArg = (arg) => arg.startsWith('-') && !isNegativeNumberArg(arg);
|
|
|
467
471
|
* @example
|
|
468
472
|
* parseArgs(['--port', '8080', 'file.txt']); // { port: '8080', _: ['file.txt'] }
|
|
469
473
|
* parseArgs(['--n', '-1']); // { n: '-1', _: [] }
|
|
474
|
+
* parseArgs(['-', '--port', '1']); // { port: '1', _: ['-'] }
|
|
475
|
+
* parseArgs(['--', '--x']); // { _: ['--x'] }
|
|
470
476
|
*/
|
|
471
477
|
const parseArgs = (args) => {
|
|
472
478
|
if (!args) args = process.argv.slice(2);
|
|
473
|
-
|
|
479
|
+
// @ts-ignore — '_: string[]' is the intended exception to the 'string|true' index signature of ArgsObject.
|
|
480
|
+
const result = /** @type {ArgsObject} */ ({ _: [] });
|
|
474
481
|
const seenKeys = new Set();
|
|
475
482
|
for (let i = 0; i < args.length; i++) {
|
|
476
483
|
const arg = args[i];
|
|
477
484
|
|
|
485
|
+
if (arg === '--') {
|
|
486
|
+
// End-of-options terminator: all remaining tokens are positionals.
|
|
487
|
+
result._.push(...args.slice(i + 1));
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
|
|
478
491
|
if (isOptionArg(arg)) {
|
|
479
492
|
if (arg.startsWith('-') && !arg.startsWith('--') && arg.length > 2) {
|
|
480
493
|
throw new Error(`Invalid argument: ${arg}. Use '--' for long options.`);
|
|
@@ -484,6 +497,7 @@ const parseArgs = (args) => {
|
|
|
484
497
|
throw new Error(`Duplicate argument: ${arg}`);
|
|
485
498
|
}
|
|
486
499
|
seenKeys.add(key);
|
|
500
|
+
/** @type {string|true} */
|
|
487
501
|
let value;
|
|
488
502
|
if (i + 1 < args.length && !isOptionArg(args[i + 1])) {
|
|
489
503
|
value = args[i + 1];
|
|
@@ -496,7 +510,6 @@ const parseArgs = (args) => {
|
|
|
496
510
|
result._.push(arg);
|
|
497
511
|
}
|
|
498
512
|
}
|
|
499
|
-
// @ts-ignore
|
|
500
513
|
return result;
|
|
501
514
|
};
|
|
502
515
|
|
|
@@ -516,4 +529,4 @@ export {
|
|
|
516
529
|
assert,
|
|
517
530
|
AsyncTracker,
|
|
518
531
|
bashEscape
|
|
519
|
-
}
|
|
532
|
+
};
|
package/lib/SHDispatch.js
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
import SHExecute from './SHExecute.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @typedef {Object} SpawnSyncResponse
|
|
5
|
-
* @property {number|null} status - Exit code (null if signal).
|
|
6
|
-
* @property {string|null} signal - Terminating signal.
|
|
7
|
-
* @property {(string|Buffer|null)[]} output - [stdin, stdout, stderr].
|
|
8
|
-
* @property {number} pid - Process ID.
|
|
9
|
-
* @property {string|Buffer|null} stdout - Captured stdout.
|
|
10
|
-
* @property {string|Buffer|null} stderr - Captured stderr.
|
|
11
|
-
*/
|
|
2
|
+
import { defaultOptions } from './internal.js';
|
|
12
3
|
|
|
13
4
|
/**
|
|
14
5
|
* @typedef {('pipe' | 'ignore' | 'inherit' | number)} StdioOption
|
|
@@ -34,7 +25,7 @@ import SHExecute from './SHExecute.js';
|
|
|
34
25
|
* - `shell`: `'bash'` (string/true → shell; false → no-shell `/usr/bin/env -S`)
|
|
35
26
|
* - `stdio`: `['inherit', 'pipe', 'pipe']`
|
|
36
27
|
* - `timeout`: `0` (no timeout; rolling on data)
|
|
37
|
-
* - `maxBuffer`: `512000` (500
|
|
28
|
+
* - `maxBuffer`: `512000` (500 KiB per stream in SHExecute)
|
|
38
29
|
*
|
|
39
30
|
* Prefix (`.options(undefined, prefix)`) only for shell mode.
|
|
40
31
|
*
|
|
@@ -43,21 +34,17 @@ import SHExecute from './SHExecute.js';
|
|
|
43
34
|
* @property {NodeJS.ProcessEnv} env - Environment for spawned commands.
|
|
44
35
|
* @property {string|boolean} shell - Shell executable, true for bash, or false for no-shell mode.
|
|
45
36
|
* @property {StdioOptions} stdio - Stdio config passed to child_process.
|
|
46
|
-
* @property {number|string} timeout -
|
|
37
|
+
* @property {number|string} timeout - Timeout in ms or duration string; 0 disables.
|
|
38
|
+
* Async `run()` uses a rolling timeout that resets on stdout/stderr data.
|
|
39
|
+
* `runSync()` passes it to `spawnSync`, where it is absolute: the process is
|
|
40
|
+
* killed after the full duration no matter how much output it produces.
|
|
47
41
|
* @property {number} [maxBuffer] - Maximum buffered bytes per stdout/stderr stream.
|
|
48
|
-
* @property {boolean} [detached] - Run process detached and resolve early.
|
|
42
|
+
* @property {boolean} [detached] - Run process detached and resolve early (~1s).
|
|
43
|
+
* Unless stdio is explicitly set for the command, stdio is forced to
|
|
44
|
+
* `'ignore'`, because open pipes would keep the parent's event loop alive.
|
|
49
45
|
* @example { timeout: '5s', stdio: 'inherit', shell: false, maxBuffer: 10 * 1024}
|
|
50
46
|
*/
|
|
51
47
|
|
|
52
|
-
/** @type {SHOptions} */
|
|
53
|
-
const defaultSHOptions = {
|
|
54
|
-
cwd: process.cwd(),
|
|
55
|
-
env: process.env,
|
|
56
|
-
shell: 'bash',
|
|
57
|
-
stdio: ['inherit', 'pipe', 'pipe'],
|
|
58
|
-
timeout: 0,
|
|
59
|
-
};
|
|
60
|
-
|
|
61
48
|
/**
|
|
62
49
|
* Merges user options into predefined defaults (non-destructive).
|
|
63
50
|
*
|
|
@@ -94,9 +81,11 @@ class SHDispatch {
|
|
|
94
81
|
#prefix = '';
|
|
95
82
|
#cmd = '';
|
|
96
83
|
/** @type {SHOptions} */
|
|
97
|
-
#options = { ...
|
|
84
|
+
#options = { ...defaultOptions };
|
|
98
85
|
/** @type {SHExecute | null} */
|
|
99
86
|
#proc = null;
|
|
87
|
+
/** Whether the user explicitly supplied `stdio` for this command. */
|
|
88
|
+
#stdioProvided = false;
|
|
100
89
|
|
|
101
90
|
/**
|
|
102
91
|
* @param {string} cmd - Command string.
|
|
@@ -106,7 +95,7 @@ class SHDispatch {
|
|
|
106
95
|
*/
|
|
107
96
|
constructor(cmd, options = {}, prefix) {
|
|
108
97
|
if (typeof cmd !== 'string' || cmd === '') {
|
|
109
|
-
throw new Error('
|
|
98
|
+
throw new Error('Invalid or empty command');
|
|
110
99
|
}
|
|
111
100
|
this.#cmd = cmd;
|
|
112
101
|
this.options(options, prefix);
|
|
@@ -131,6 +120,9 @@ class SHDispatch {
|
|
|
131
120
|
return this;
|
|
132
121
|
}
|
|
133
122
|
const nextOptions = { ...options };
|
|
123
|
+
if (nextOptions.stdio !== undefined) {
|
|
124
|
+
this.#stdioProvided = true;
|
|
125
|
+
}
|
|
134
126
|
if (nextOptions.stdio && typeof nextOptions.stdio === 'string') {
|
|
135
127
|
// convert stdio to array
|
|
136
128
|
const io = nextOptions.stdio;
|
|
@@ -143,11 +135,21 @@ class SHDispatch {
|
|
|
143
135
|
/**
|
|
144
136
|
* Async run: Captures stdout; rejects on error/timeout.
|
|
145
137
|
*
|
|
138
|
+
* Replaces the internal process handle: calling `run()` again while a
|
|
139
|
+
* previous run is still active makes that first process unkillable via
|
|
140
|
+
* `kill()`.
|
|
141
|
+
*
|
|
146
142
|
* @param {string} [payload] - Stdin payload.
|
|
147
143
|
* @returns {Promise<string>} Stdout.
|
|
148
144
|
*/
|
|
149
145
|
run(payload) {
|
|
150
|
-
|
|
146
|
+
// Detached processes must not keep piped stdio: open pipe handles hold
|
|
147
|
+
// the parent's event loop alive and defeat detachment. Force 'ignore'
|
|
148
|
+
// unless the user explicitly chose a stdio setup for this command.
|
|
149
|
+
const options = this.#options.detached && !this.#stdioProvided
|
|
150
|
+
? { ...this.#options, stdio: 'ignore' }
|
|
151
|
+
: this.#options;
|
|
152
|
+
this.#proc = new SHExecute(this.#cmd, this.#prefix, options);
|
|
151
153
|
return this.#proc.run(payload);
|
|
152
154
|
}
|
|
153
155
|
|