@j-o-r/sh 1.1.28 → 1.1.31
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 +145 -151
- package/TODO.md +8 -0
- package/lib/AsyncTracker.js +126 -107
- package/lib/SH.js +281 -224
- package/lib/SHDispatch.js +122 -94
- package/lib/SHExecute.js +83 -41
- package/lib/Test.js +153 -123
- package/package.json +2 -2
- package/types/AsyncTracker.d.ts +65 -31
- package/types/SH.d.ts +190 -168
- package/types/SHDispatch.d.ts +81 -66
- package/types/SHExecute.d.ts +44 -23
- package/types/Test.d.ts +66 -30
package/lib/SH.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
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'.
|
|
27
27
|
// Modified by: jorrit.duin+sh[AT]gmail.com
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
import assert from 'node:assert';
|
|
30
30
|
import readline from 'node:readline/promises';
|
|
31
31
|
import SHDispatch from './SHDispatch.js';
|
|
@@ -33,40 +33,49 @@ import Test from './Test.js'
|
|
|
33
33
|
import AsyncTracker from './AsyncTracker.js'
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
|
|
36
|
+
* Parsed command-line arguments.
|
|
37
|
+
*
|
|
38
|
+
* Named options map to their string value, or `true` when no value is supplied.
|
|
39
|
+
* Bare positional arguments are collected in `_`.
|
|
40
|
+
*
|
|
41
|
+
* @typedef {{_: string[]} & Object.<string, string|true|string[]>} ArgsObject
|
|
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
|
+
|
|
41
54
|
/**
|
|
42
|
-
* @typedef {
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
* @typedef {import('./SHDispatch.js').SHOptions} SHOptions
|
|
56
|
+
*/
|
|
57
|
+
|
|
45
58
|
/**
|
|
46
|
-
* @typedef {
|
|
47
|
-
* @
|
|
48
|
-
|
|
59
|
+
* @typedef {Object} AbortableInput
|
|
60
|
+
* @property {Promise<string|void>} input - Promise that resolves to user input or void if aborted.
|
|
61
|
+
* @property {() => void} abort - Function to abort input collection.
|
|
62
|
+
*/
|
|
63
|
+
|
|
49
64
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* @typedef {
|
|
53
|
-
|
|
54
|
-
*
|
|
55
|
-
* @param {Array} pieces - An array of string literals from a template literal.
|
|
56
|
-
* @param {...*} args - The values to be interpolated into the string literals.
|
|
57
|
-
* @returns {SHDispatch} Trigger for the command.
|
|
58
|
-
* @throws {Error} Throws an error if any of the string literals in `pieces` is undefined.
|
|
59
|
-
*
|
|
60
|
-
* @example
|
|
61
|
-
* const command = await SH`echo 'Hello, world!'`.run();
|
|
62
|
-
*/
|
|
65
|
+
* Generator-like object that yields retry delay durations.
|
|
66
|
+
*
|
|
67
|
+
* @typedef {Iterator<number|string>} ExpBackoffGenerator
|
|
68
|
+
*/
|
|
63
69
|
|
|
64
70
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* @param {any} fn - Any
|
|
68
|
-
* @returns {string} The "real" object
|
|
69
|
-
|
|
71
|
+
* Utility to determine the JavaScript type of a value.
|
|
72
|
+
*
|
|
73
|
+
* @param {any} fn - Any value to inspect.
|
|
74
|
+
* @returns {string} The "real" object type name (e.g., 'Array', 'Promise') or primitive typeof.
|
|
75
|
+
* @example
|
|
76
|
+
* jsType([]); // 'Array'
|
|
77
|
+
* jsType(Promise.resolve()); // 'Promise'
|
|
78
|
+
*/
|
|
70
79
|
const jsType = (fn) => {
|
|
71
80
|
if (fn === undefined) return 'undefined';
|
|
72
81
|
const type = Object.prototype.toString.call(fn).slice(8, -1);
|
|
@@ -74,59 +83,60 @@ const jsType = (fn) => {
|
|
|
74
83
|
};
|
|
75
84
|
|
|
76
85
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* @param {any} o -
|
|
80
|
-
* @param {string} p -
|
|
81
|
-
* @returns {boolean}
|
|
82
|
-
*/
|
|
86
|
+
* Checks if an object has a specific own property (code-safe).
|
|
87
|
+
*
|
|
88
|
+
* @param {any} o - Object to examine.
|
|
89
|
+
* @param {string} p - Property name to check.
|
|
90
|
+
* @returns {boolean} True if the object has the own property.
|
|
91
|
+
*/
|
|
83
92
|
const hasProp = (o, p) => {
|
|
84
93
|
if (o == null) return false;
|
|
85
94
|
return Object.prototype.hasOwnProperty.call(o, p);
|
|
86
95
|
};
|
|
96
|
+
|
|
87
97
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
|
|
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
|
+
*/
|
|
92
107
|
const parseDuration = (d) => {
|
|
93
108
|
if (typeof d == 'number') {
|
|
94
|
-
if (
|
|
109
|
+
if (!Number.isFinite(d) || d < 0)
|
|
95
110
|
throw new Error(`Invalid duration: "${d}".`);
|
|
96
111
|
return d;
|
|
97
112
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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;
|
|
103
119
|
}
|
|
104
|
-
throw new Error(`
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* bash escape a string suitable as an argument on the commandline
|
|
108
|
-
* for javascript code
|
|
109
|
-
* @param {string} x
|
|
110
|
-
* @returns {string}
|
|
111
|
-
*/
|
|
112
|
-
const bashEscape = (x) => {
|
|
113
|
-
let str = String(x).trim();
|
|
114
|
-
// the trick is to double escape escape vars first
|
|
115
|
-
str = str.replace(/\\/g, '\\\\');
|
|
116
|
-
// then add escaping for oddities
|
|
117
|
-
// Replace literal escape sequences like \n, \t, \r in quoted strings
|
|
118
|
-
str = str.replace(/(['"])\\(.)\1/g, '$1\\\\$2$1');
|
|
119
|
-
// escape $ (is a BASH var)
|
|
120
|
-
str = str.replace(/\$/g, '\\$');
|
|
121
|
-
// Escape backticks
|
|
122
|
-
str = str.replace(/`/g, '\\`');
|
|
123
|
-
// Escape quotes
|
|
124
|
-
str = str.replace(/"/g, '\\"');
|
|
125
|
-
return str;
|
|
120
|
+
throw new Error(`Invalid duration type: "${jsType(d)}".`);
|
|
126
121
|
}
|
|
127
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Quotes a value as one POSIX shell argument.
|
|
125
|
+
*
|
|
126
|
+
* The `SH` template tag interpolates values as raw shell source by default. Use
|
|
127
|
+
* this helper when a JavaScript value must be passed as data instead of shell
|
|
128
|
+
* syntax. The returned string is single-quoted and preserves the exact string
|
|
129
|
+
* value, including leading/trailing whitespace, empty strings, quotes,
|
|
130
|
+
* semicolons, glob characters, tabs, and newlines.
|
|
131
|
+
*
|
|
132
|
+
* @param {unknown} x - Value to quote as a single shell argument.
|
|
133
|
+
* @returns {string} POSIX-shell-quoted argument string.
|
|
134
|
+
* @example
|
|
135
|
+
* await SH`printf '%s\n' ${bashEscape(userInput)}`.run();
|
|
136
|
+
*/
|
|
137
|
+
const bashEscape = (x) => `'${String(x).replace(/'/g, `'\\''`)}'`;
|
|
128
138
|
|
|
129
|
-
/**
|
|
139
|
+
/** Default options applied to all SH commands unless overridden. */
|
|
130
140
|
const defaultOptions = {
|
|
131
141
|
cwd: process.cwd(),
|
|
132
142
|
env: process.env,
|
|
@@ -135,30 +145,66 @@ const defaultOptions = {
|
|
|
135
145
|
timeout: 0, // when 0 there is no timeout
|
|
136
146
|
};
|
|
137
147
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
]);
|
|
146
158
|
|
|
147
159
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
|
|
160
|
+
* Proxy traps for the SH template tag and global option defaults.
|
|
161
|
+
*
|
|
162
|
+
* Assignments keep the existing compatibility behavior by storing every property
|
|
163
|
+
* in defaultOptions. Reads only expose known default option keys, so unknown
|
|
164
|
+
* property access continues to behave like a normal function proxy read.
|
|
165
|
+
*/
|
|
166
|
+
const defaultOptionHandler = {
|
|
167
|
+
get(target, prop, receiver) {
|
|
168
|
+
if (defaultOptionKeys.has(prop)) {
|
|
169
|
+
return defaultOptions[prop];
|
|
170
|
+
}
|
|
171
|
+
return Reflect.get(target, prop, receiver);
|
|
172
|
+
},
|
|
173
|
+
set(target, prop, value) {
|
|
174
|
+
defaultOptions[prop] = value;
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
151
179
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
|
|
180
|
+
* Template tag for building and executing shell commands.
|
|
181
|
+
*
|
|
182
|
+
* Returns an {@link SHDispatch} instance for configuration (.options()) and execution (.run(), .runSync()).
|
|
183
|
+
*
|
|
184
|
+
* Interpolation rules are raw-by-default:
|
|
185
|
+
* - Arrays: Each element is converted with `String(value)` and joined with one space.
|
|
186
|
+
* - Other values: `String(value)` is inserted directly into the shell source.
|
|
187
|
+
*
|
|
188
|
+
* Raw interpolation allows trusted shell fragments such as pipes, redirects, and
|
|
189
|
+
* command separators. It is not safe for untrusted input. Wrap untrusted values
|
|
190
|
+
* with {@link bashEscape} before interpolating them.
|
|
191
|
+
*
|
|
192
|
+
* SH acts as both template tag and options setter: `SH.timeout = 5000; SH`cmd``
|
|
193
|
+
*
|
|
194
|
+
* @param {TemplateStringsArray} pieces - String literals from template.
|
|
195
|
+
* @param {...unknown[]} args - Values to interpolate.
|
|
196
|
+
* @returns {SHDispatch} Command dispatcher.
|
|
197
|
+
* @throws {Error} If pieces contain undefined.
|
|
198
|
+
* @example
|
|
199
|
+
* const cmd = SH`echo ${'Hello'}`;
|
|
200
|
+
* await cmd.run(); // Executes: echo Hello
|
|
201
|
+
*
|
|
202
|
+
* // Array interpolation is raw and joins with spaces
|
|
203
|
+
* SH`ls ${['-la', '/']}`.run(); // Executes: ls -la /
|
|
204
|
+
*
|
|
205
|
+
* // Quote untrusted values explicitly
|
|
206
|
+
* SH`printf '%s\n' ${bashEscape('semi; colon')}`.run();
|
|
207
|
+
*/
|
|
162
208
|
const SH = new Proxy(function(pieces, ...args) {
|
|
163
209
|
if (pieces.some((p) => p == undefined)) {
|
|
164
210
|
throw new Error(`Malformed command ${pieces}`);
|
|
@@ -168,57 +214,40 @@ const SH = new Proxy(function(pieces, ...args) {
|
|
|
168
214
|
let s;
|
|
169
215
|
|
|
170
216
|
if (Array.isArray(args[i])) {
|
|
171
|
-
|
|
172
|
-
s = args[i].map(/** @param {string} x */(x) => {
|
|
173
|
-
// Trim every element
|
|
174
|
-
let str = String(x).trim();
|
|
175
|
-
str = str.replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
|
|
176
|
-
// If the string contains special characters, wrap in single quotes
|
|
177
|
-
if (str.match(/[ "'$`(){}[\]]/)) {
|
|
178
|
-
// Escape single quotes within the string
|
|
179
|
-
return `'${str.replace(/'/g, `'\\''`)}'`;
|
|
180
|
-
}
|
|
181
|
-
return str;
|
|
182
|
-
}).join(' ');
|
|
217
|
+
s = args[i].map((x) => String(x)).join(' ');
|
|
183
218
|
} else {
|
|
184
219
|
s = String(args[i]);
|
|
185
220
|
}
|
|
186
221
|
cmd += s + pieces[++i];
|
|
187
222
|
}
|
|
188
223
|
return new SHDispatch(cmd, defaultOptions);
|
|
189
|
-
},
|
|
190
|
-
|
|
224
|
+
}, defaultOptionHandler);
|
|
191
225
|
|
|
192
226
|
/**
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
|
|
204
|
-
* });
|
|
205
|
-
*/
|
|
227
|
+
* Executes a callback in a new async context (fresh callstack).
|
|
228
|
+
*
|
|
229
|
+
* Useful for parallel operations without nesting.
|
|
230
|
+
*
|
|
231
|
+
* @param {() => Promise<any>} callback - Async function to execute.
|
|
232
|
+
* @returns {Promise<any>} Result of callback.
|
|
233
|
+
* @example
|
|
234
|
+
* const results = await within(async () => {
|
|
235
|
+
* return Promise.all([SH`sleep 1; echo 1`.run(), sleep(2)]);
|
|
236
|
+
* });
|
|
237
|
+
*/
|
|
206
238
|
const within = async (callback) => {
|
|
207
239
|
return await callback();
|
|
208
240
|
}
|
|
209
241
|
|
|
210
242
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
* @
|
|
214
|
-
*
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Read entire stdin as UTF-8. If stdin is a TTY, resolves to an empty string.
|
|
218
|
-
* @returns {Promise<string|undefined>}
|
|
243
|
+
* Reads entire stdin as UTF-8 string. Resolves to empty string if TTY (no pipe).
|
|
244
|
+
*
|
|
245
|
+
* @returns {Promise<string>} Stdin content, or an empty string if TTY.
|
|
246
|
+
* @example
|
|
247
|
+
* const input = await readIn(); // Use in piped scripts
|
|
219
248
|
*/
|
|
220
249
|
const readIn = async () => {
|
|
221
|
-
if (process.stdin.isTTY) return;
|
|
250
|
+
if (process.stdin.isTTY) return '';
|
|
222
251
|
let buf = '';
|
|
223
252
|
process.stdin.setEncoding('utf8');
|
|
224
253
|
for await (const chunk of process.stdin) {
|
|
@@ -227,17 +256,18 @@ const readIn = async () => {
|
|
|
227
256
|
return buf;
|
|
228
257
|
}
|
|
229
258
|
|
|
230
|
-
|
|
231
259
|
/**
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* @
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
|
|
260
|
+
* Prompts user for input on stdin with custom prompt.
|
|
261
|
+
*
|
|
262
|
+
* Supports aborting input collection.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} prompt - Prompt text to display.
|
|
265
|
+
* @returns {AbortableInput} Object with input Promise and abort function.
|
|
266
|
+
* @example
|
|
267
|
+
* const {input, abort} = userIn('Enter name: ');
|
|
268
|
+
* const name = await input;
|
|
269
|
+
* // abort(); // Cancel anytime
|
|
270
|
+
*/
|
|
241
271
|
const userIn = (prompt) => {
|
|
242
272
|
let resolvePromise;
|
|
243
273
|
const input = new Promise((resolve) => { resolvePromise = resolve; });
|
|
@@ -285,94 +315,110 @@ const userIn = (prompt) => {
|
|
|
285
315
|
}
|
|
286
316
|
};
|
|
287
317
|
};
|
|
318
|
+
|
|
288
319
|
/**
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
* @param {
|
|
292
|
-
* @
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
320
|
+
* Checks whether a value is a generator-like retry delay source.
|
|
321
|
+
*
|
|
322
|
+
* @param {unknown} value - Value to inspect.
|
|
323
|
+
* @returns {boolean} True when value has a callable `next()` method.
|
|
324
|
+
*/
|
|
325
|
+
const isDelayGenerator = (value) => value != null && typeof value == 'object' && typeof value.next == 'function';
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Retries an async function up to N times with optional delays.
|
|
329
|
+
*
|
|
330
|
+
* Delay can be fixed ('1s'), generator-like (expBackoff()), or none.
|
|
331
|
+
*
|
|
332
|
+
* @param {number} count - Positive integer number of attempts.
|
|
333
|
+
* @param {string|number|ExpBackoffGenerator|Function} delayOrCallback - Delay, delay generator, or callback if no separate callback is supplied.
|
|
334
|
+
* @param {Function} [callback] - Callback to retry when a delay is supplied.
|
|
335
|
+
* @returns {Promise<any>} Successful result.
|
|
336
|
+
* @throws {Error} If arguments are invalid, or the last callback error if all attempts fail.
|
|
337
|
+
* @example
|
|
338
|
+
* await retry(3, '1s', () => SH`curl http://unreliable`.run());
|
|
339
|
+
* await retry(3, expBackoff(), () => flakyOp());
|
|
340
|
+
*/
|
|
341
|
+
const retry = async (count, delayOrCallback, callback) => {
|
|
342
|
+
if (!Number.isInteger(count) || count < 1) {
|
|
343
|
+
throw new Error(`Invalid retry count: "${count}". Expected a positive integer.`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let retryCallback;
|
|
310
347
|
let delayStatic = 0;
|
|
311
348
|
let delayGen;
|
|
312
|
-
|
|
313
|
-
if (typeof
|
|
314
|
-
|
|
349
|
+
|
|
350
|
+
if (typeof delayOrCallback == 'function') {
|
|
351
|
+
retryCallback = delayOrCallback;
|
|
315
352
|
}
|
|
316
353
|
else {
|
|
317
|
-
if (typeof
|
|
318
|
-
|
|
354
|
+
if (typeof callback != 'function') {
|
|
355
|
+
throw new Error(`Invalid retry callback: expected a function.`);
|
|
356
|
+
}
|
|
357
|
+
retryCallback = callback;
|
|
358
|
+
|
|
359
|
+
if (isDelayGenerator(delayOrCallback)) {
|
|
360
|
+
delayGen = delayOrCallback;
|
|
361
|
+
}
|
|
362
|
+
else if (delayOrCallback != null && typeof delayOrCallback == 'object') {
|
|
363
|
+
throw new Error(`Invalid retry delay generator: expected an object with next().`);
|
|
319
364
|
}
|
|
320
365
|
else {
|
|
321
|
-
delayStatic = parseDuration(
|
|
366
|
+
delayStatic = parseDuration(delayOrCallback);
|
|
322
367
|
}
|
|
323
|
-
assert(b);
|
|
324
|
-
callback = b;
|
|
325
368
|
}
|
|
326
|
-
|
|
327
|
-
let attempt =
|
|
328
|
-
while (count-- > 0) {
|
|
329
|
-
attempt++;
|
|
369
|
+
|
|
370
|
+
for (let attempt = 1; attempt <= count; attempt++) {
|
|
330
371
|
try {
|
|
331
|
-
return await
|
|
372
|
+
return await retryCallback();
|
|
332
373
|
}
|
|
333
374
|
catch (err) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (
|
|
339
|
-
lastErr = err;
|
|
340
|
-
if (count == 0)
|
|
341
|
-
break;
|
|
342
|
-
if (delay)
|
|
375
|
+
if (attempt == count) {
|
|
376
|
+
throw err;
|
|
377
|
+
}
|
|
378
|
+
const delay = delayGen ? delayGen.next().value : delayStatic;
|
|
379
|
+
if (delay) {
|
|
343
380
|
await sleep(delay);
|
|
381
|
+
}
|
|
344
382
|
}
|
|
345
383
|
}
|
|
346
|
-
throw lastErr;
|
|
347
384
|
}
|
|
385
|
+
|
|
348
386
|
/**
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
* @
|
|
353
|
-
*
|
|
354
|
-
* await sleep('
|
|
355
|
-
*/
|
|
387
|
+
* Sleeps for a specified duration.
|
|
388
|
+
*
|
|
389
|
+
* @param {string|number} duration - Duration as '5s', '100ms', or ms number.
|
|
390
|
+
* @returns {Promise<void>}
|
|
391
|
+
* @example
|
|
392
|
+
* await sleep('2s');
|
|
393
|
+
*/
|
|
356
394
|
const sleep = (duration) => {
|
|
357
395
|
return new Promise((resolve) => {
|
|
358
396
|
setTimeout(resolve, parseDuration(duration));
|
|
359
397
|
});
|
|
360
398
|
}
|
|
399
|
+
|
|
361
400
|
/**
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
|
|
401
|
+
* Changes the current working directory.
|
|
402
|
+
*
|
|
403
|
+
* @param {string} dir - Path to new directory.
|
|
404
|
+
* @example
|
|
405
|
+
* cd('/tmp');
|
|
406
|
+
*/
|
|
365
407
|
const cd = (dir) => {
|
|
366
408
|
// @ts-ignore
|
|
367
409
|
process.chdir(dir);
|
|
368
410
|
}
|
|
411
|
+
|
|
369
412
|
/**
|
|
370
|
-
*
|
|
413
|
+
* Generator for exponential backoff delays with jitter.
|
|
371
414
|
*
|
|
372
415
|
* @generator
|
|
373
|
-
* @param {string} [max='60s'] -
|
|
374
|
-
* @param {string} [rand='100ms'] -
|
|
375
|
-
* @yields {number}
|
|
416
|
+
* @param {string} [max='60s'] - Max backoff duration.
|
|
417
|
+
* @param {string} [rand='100ms'] - Max jitter.
|
|
418
|
+
* @yields {number} Next backoff ms.
|
|
419
|
+
* @example
|
|
420
|
+
* const backoff = expBackoff();
|
|
421
|
+
* await sleep(backoff.next().value);
|
|
376
422
|
*/
|
|
377
423
|
function* expBackoff(max = '60s', rand = '100ms') {
|
|
378
424
|
const maxMs = parseDuration(max);
|
|
@@ -383,52 +429,63 @@ function* expBackoff(max = '60s', rand = '100ms') {
|
|
|
383
429
|
yield Math.min(2 ** n++, maxMs) + ms;
|
|
384
430
|
}
|
|
385
431
|
}
|
|
432
|
+
|
|
386
433
|
/**
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
* - If args is not passed, process.argv.slice(2) will be the default
|
|
397
|
-
* - Each key corresponds to an argument that starts with `--` or `-`,
|
|
398
|
-
* - The value is either the next argument or `true` if no value is provided,
|
|
399
|
-
* - The `_` property contains an array of unbound arguments.
|
|
400
|
-
*/
|
|
434
|
+
* Matches negative number tokens that should be parsed as values, not options.
|
|
435
|
+
*
|
|
436
|
+
* Supports decimal and exponent forms such as `-1`, `-0.5`, `-.5`, and `-1e3`.
|
|
437
|
+
*
|
|
438
|
+
* @param {string} arg - CLI argument token.
|
|
439
|
+
* @returns {boolean} True when the token is a negative numeric value.
|
|
440
|
+
*/
|
|
441
|
+
const isNegativeNumberArg = (arg) => /^-(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(arg);
|
|
442
|
+
|
|
401
443
|
/**
|
|
402
|
-
*
|
|
444
|
+
* Checks whether a token should be parsed as an option.
|
|
403
445
|
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
* - Bare values collected under `_.`
|
|
407
|
-
* - Duplicate keys throw an error; keys without a following value become true.
|
|
446
|
+
* Negative numeric values are intentionally excluded so calls like
|
|
447
|
+
* `parseArgs(['--n', '-1'])` parse `-1` as the value for `--n`.
|
|
408
448
|
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
|
|
449
|
+
* @param {string} arg - CLI argument token.
|
|
450
|
+
* @returns {boolean} True when the token should be parsed as an option.
|
|
451
|
+
*/
|
|
452
|
+
const isOptionArg = (arg) => arg.startsWith('-') && !isNegativeNumberArg(arg);
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Parses CLI arguments into an object.
|
|
456
|
+
*
|
|
457
|
+
* Supports --key value, -k value (no shorts grouped).
|
|
458
|
+
* Bares go to _[]. Duplicates error. No = syntax.
|
|
459
|
+
* Negative number tokens such as -1, -0.5, and -1e3 are parsed as values or
|
|
460
|
+
* positionals, not options.
|
|
412
461
|
*
|
|
413
|
-
*
|
|
462
|
+
* Defaults to process.argv.slice(2).
|
|
463
|
+
*
|
|
464
|
+
* @param {string[]} [args=process.argv.slice(2)] - Args array.
|
|
414
465
|
* @returns {ArgsObject}
|
|
466
|
+
* @throws {Error} On invalid/dupe args.
|
|
467
|
+
* @example
|
|
468
|
+
* parseArgs(['--port', '8080', 'file.txt']); // { port: '8080', _: ['file.txt'] }
|
|
469
|
+
* parseArgs(['--n', '-1']); // { n: '-1', _: [] }
|
|
415
470
|
*/
|
|
416
471
|
const parseArgs = (args) => {
|
|
417
472
|
if (!args) args = process.argv.slice(2);
|
|
418
473
|
const result = { _: [] };
|
|
419
474
|
const seenKeys = new Set();
|
|
420
475
|
for (let i = 0; i < args.length; i++) {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
476
|
+
const arg = args[i];
|
|
477
|
+
|
|
478
|
+
if (isOptionArg(arg)) {
|
|
479
|
+
if (arg.startsWith('-') && !arg.startsWith('--') && arg.length > 2) {
|
|
480
|
+
throw new Error(`Invalid argument: ${arg}. Use '--' for long options.`);
|
|
424
481
|
}
|
|
425
|
-
const key =
|
|
482
|
+
const key = arg.startsWith('--') ? arg.substring(2) : arg.substring(1);
|
|
426
483
|
if (seenKeys.has(key)) {
|
|
427
|
-
throw new Error(`Duplicate argument: ${
|
|
484
|
+
throw new Error(`Duplicate argument: ${arg}`);
|
|
428
485
|
}
|
|
429
486
|
seenKeys.add(key);
|
|
430
487
|
let value;
|
|
431
|
-
if (
|
|
488
|
+
if (i + 1 < args.length && !isOptionArg(args[i + 1])) {
|
|
432
489
|
value = args[i + 1];
|
|
433
490
|
i++; // Skip next element as it is a value
|
|
434
491
|
} else {
|
|
@@ -436,7 +493,7 @@ const parseArgs = (args) => {
|
|
|
436
493
|
}
|
|
437
494
|
result[key] = value;
|
|
438
495
|
} else {
|
|
439
|
-
result._.push(
|
|
496
|
+
result._.push(arg);
|
|
440
497
|
}
|
|
441
498
|
}
|
|
442
499
|
// @ts-ignore
|