@j-o-r/sh 0.0.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -63
- package/lib/sh.d.ts +112 -39
- package/lib/sh.js +354 -106
- package/package.json +4 -3
- package/lib/ProcessOutput.d.ts +0 -44
- package/lib/ProcessOutput.js +0 -113
- package/lib/ProcessPromise.d.ts +0 -147
- package/lib/ProcessPromise.js +0 -354
- package/lib/utils.d.ts +0 -20
- package/lib/utils.js +0 -422
- package/release/j-o-r-sh-0.0.3.tgz +0 -0
package/lib/sh.js
CHANGED
|
@@ -1,3 +1,262 @@
|
|
|
1
|
+
import { spawnSync, spawn, exec } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Kills a process and all child processes of a given process ID in Linux/Posix.
|
|
5
|
+
* @param {number} processPid - The process ID.
|
|
6
|
+
* @param {string} signal - Signal to send.
|
|
7
|
+
* @retruns {Promise<number[]>} array with killed pid numbers
|
|
8
|
+
*/
|
|
9
|
+
const killProcesses = (processPid, signal) => {
|
|
10
|
+
const killed = [];
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
// Command to get child PIDs of the given process
|
|
13
|
+
const cmd = `pgrep -P ${processPid}`;
|
|
14
|
+
exec(cmd, (error, stdout, stderr) => {
|
|
15
|
+
if (error) {
|
|
16
|
+
reject(error);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (stderr) {
|
|
20
|
+
reject(new Error(stderr));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const pids = stdout.split(/\r?\n/).filter(pid => pid);
|
|
24
|
+
// Kill each child process
|
|
25
|
+
try {
|
|
26
|
+
for (const pid of pids) {
|
|
27
|
+
process.kill(parseInt(pid), signal);
|
|
28
|
+
killed.push(parseInt(pid));
|
|
29
|
+
}
|
|
30
|
+
} catch (err) {
|
|
31
|
+
reject(err);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// Kill the parent process after all child processes have been killed
|
|
35
|
+
try {
|
|
36
|
+
process.kill(processPid, signal);
|
|
37
|
+
killed.push(processPid);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
reject(err);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
resolve(killed);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
class SHExecute {
|
|
48
|
+
#proc;
|
|
49
|
+
#command = '';
|
|
50
|
+
#options = {};
|
|
51
|
+
#stdout = '';
|
|
52
|
+
#stderr = '';
|
|
53
|
+
constructor(command, options = {}) {
|
|
54
|
+
this.#command = command;
|
|
55
|
+
this.#options = options;
|
|
56
|
+
this.#proc = null;
|
|
57
|
+
this.#stdout = '';
|
|
58
|
+
this.#stderr = '';
|
|
59
|
+
}
|
|
60
|
+
runSync () {
|
|
61
|
+
let { cwd, shell, env, stdio } = this.#options;
|
|
62
|
+
return spawnSync(this.#options.prefix, [this.#command], {
|
|
63
|
+
cwd,
|
|
64
|
+
shell,
|
|
65
|
+
stdio,
|
|
66
|
+
windowsHide: true,
|
|
67
|
+
env,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* @param {string} [payload] - data to write
|
|
72
|
+
* @retuns {Promise<string>}
|
|
73
|
+
*/
|
|
74
|
+
run(payload) {
|
|
75
|
+
if (payload && typeof payload !== 'string') {
|
|
76
|
+
throw new Error('Argument is not a string');
|
|
77
|
+
}
|
|
78
|
+
let { cwd, shell, env, stdio } = this.#options;
|
|
79
|
+
if (payload) stdio = ['pipe', 'pipe', 'pipe'];
|
|
80
|
+
this.#proc = spawn(this.#options.prefix, [this.#command], {
|
|
81
|
+
cwd,
|
|
82
|
+
shell,
|
|
83
|
+
stdio,
|
|
84
|
+
windowsHide: true,
|
|
85
|
+
env,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
this.#proc.stdout?.on('data', (data) => {
|
|
89
|
+
this.#stdout += data;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
this.#proc.stderr?.on('data', (data) => {
|
|
93
|
+
this.#stderr += data;
|
|
94
|
+
});
|
|
95
|
+
if (payload) {
|
|
96
|
+
this.#proc.stdin.write(payload);
|
|
97
|
+
this.#proc.stdin.end();
|
|
98
|
+
}
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
this.#proc.on('close', (code) => {
|
|
101
|
+
if (code === 0) {
|
|
102
|
+
resolve(this.#stdout.trim());
|
|
103
|
+
} else {
|
|
104
|
+
reject(new Error(`${code}: ${this.#command} "${this.#stderr.trim()}"`));
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
this.#proc.on('error', (err) => {
|
|
109
|
+
reject(err);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* @returns {Promise<number[]>}
|
|
115
|
+
*/
|
|
116
|
+
async kill(signal = 'SIGTERM') {
|
|
117
|
+
if (!this.#proc) throw new Error('Trying to kill a process without creating one.');
|
|
118
|
+
if (!this.#proc.pid) throw new Error('The process pid is undefined.');
|
|
119
|
+
|
|
120
|
+
return killProcesses(this.#proc.pid, signal);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @typedef {Object} SpawnSyncResponse
|
|
126
|
+
* @property {number} status - The exit code of the child process. A value of `0` indicates success.
|
|
127
|
+
* @property {Buffer|null} signal - The signal used to terminate the process, if any.
|
|
128
|
+
* @property {Array<string|null>} output - An array containing the standard output and standard error of the child process.
|
|
129
|
+
* @property {number} pid - The process ID of the child process.
|
|
130
|
+
* @property {Buffer|null} stdout - The standard output of the child process.
|
|
131
|
+
* @property {Buffer|null} stderr - The standard error of the child process.
|
|
132
|
+
*/
|
|
133
|
+
/**
|
|
134
|
+
* Default options for the execution environment.
|
|
135
|
+
*
|
|
136
|
+
* @typedef {Object} SHOptions
|
|
137
|
+
* @property {string} [cwd] - The current working directory.
|
|
138
|
+
* @property {NodeJS.ProcessEnv} [env] - The environment variables.
|
|
139
|
+
* @property {string} [shell] - The shell to use for execution.
|
|
140
|
+
* @property {string} [prefix] - The prefix commands to ensure a safe execution environment.
|
|
141
|
+
* @property {StdioOption|StdioOptions} [stdio] - The stdio configuration.
|
|
142
|
+
*/
|
|
143
|
+
/**
|
|
144
|
+
* @typedef {('pipe' | 'ignore' | 'inherit' | number)} StdioOption
|
|
145
|
+
* @description Defines the stdio configuration for each of the standard streams.
|
|
146
|
+
*
|
|
147
|
+
* - 'pipe' creates a pipe between the child process and the parent process.
|
|
148
|
+
* The parent end of the pipe is exposed as a property on the `ChildProcess` object.
|
|
149
|
+
* - 'ignore' indicates that the child process's corresponding stdio file descriptor will be ignored.
|
|
150
|
+
* - 'inherit' passes the corresponding stdio stream to/from the child process.
|
|
151
|
+
* - Stream object to be used for the stdio stream.
|
|
152
|
+
* - Positive integer representing a file descriptor to be used for the stdio stream.
|
|
153
|
+
*/
|
|
154
|
+
/**
|
|
155
|
+
* @typedef {Array<StdioOption>|StdioOption} StdioOptions
|
|
156
|
+
* @description
|
|
157
|
+
* Configures the stdio streams for the child process. This can be an array or a single StdioOption.
|
|
158
|
+
*
|
|
159
|
+
* Array Form: Specify the configuration for [stdin, stdout, stderr].
|
|
160
|
+
* - If array length is more than 3, additional positions correspond to extra streams.
|
|
161
|
+
* Single Value: This value will be applied to stdin, stdout, and stderr.
|
|
162
|
+
*
|
|
163
|
+
* Examples:
|
|
164
|
+
* - ['pipe', 'pipe', 'ignore']: Pipe stdin and stdout, ignore stderr.
|
|
165
|
+
* - 'inherit': Inherit all stdio streams from the parent.
|
|
166
|
+
*/
|
|
167
|
+
/**
|
|
168
|
+
* 'Code Safe' has own prop
|
|
169
|
+
*
|
|
170
|
+
* @param {any} o - object to examine
|
|
171
|
+
* @param {string} p - property to look for
|
|
172
|
+
* @returns {boolean}
|
|
173
|
+
*/
|
|
174
|
+
const hasProp = (o, p) => {
|
|
175
|
+
if (typeof o === 'undefined') {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
return Object.prototype.hasOwnProperty.call(o, p);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Merge property values while maintaining the fixed set of props in the original object
|
|
183
|
+
* @param {object} predefined - original object
|
|
184
|
+
* @param {object} options - object with new values
|
|
185
|
+
* @retuns {object}
|
|
186
|
+
*/
|
|
187
|
+
const mergeOptions = (predefined, options) => {
|
|
188
|
+
// Extract the keys from the predefined object
|
|
189
|
+
const keys = Object.keys(predefined);
|
|
190
|
+
|
|
191
|
+
// Use reduce to accumulate only the predefined properties from sourceObj
|
|
192
|
+
const mergedObj = keys.reduce((acc, key) => {
|
|
193
|
+
if (hasProp(options, key)) {
|
|
194
|
+
acc[key] = options[key];
|
|
195
|
+
} else {
|
|
196
|
+
acc[key] = predefined[key];
|
|
197
|
+
}
|
|
198
|
+
return acc;
|
|
199
|
+
}, {});
|
|
200
|
+
|
|
201
|
+
return mergedObj;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
/** @type {SHOptions} */
|
|
206
|
+
const defaultOptions = {
|
|
207
|
+
cwd: process.cwd(),
|
|
208
|
+
env: process.env,
|
|
209
|
+
shell: 'bash',
|
|
210
|
+
prefix: 'set -euo pipefail;/usr/bin/env',
|
|
211
|
+
stdio: ['inherit', 'pipe', 'pipe']
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class SHDispatch {
|
|
217
|
+
#cmd = '';
|
|
218
|
+
#options = {};
|
|
219
|
+
#proc;
|
|
220
|
+
/**
|
|
221
|
+
* @param {string} cmd - cmd to execute
|
|
222
|
+
*/
|
|
223
|
+
constructor(cmd) {
|
|
224
|
+
this.#cmd = cmd;
|
|
225
|
+
this.#options = defaultOptions;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* @param {SHOptions} options
|
|
229
|
+
* @returns {SHDispatch}
|
|
230
|
+
*/
|
|
231
|
+
options(options) {
|
|
232
|
+
this.#options = mergeOptions(defaultOptions, options);
|
|
233
|
+
return this;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* @param {string} [payload]
|
|
237
|
+
* @returns {Promise<string>}
|
|
238
|
+
*/
|
|
239
|
+
run(payload) {
|
|
240
|
+
this.#proc = new SHExecute(this.#cmd, this.#options);
|
|
241
|
+
return this.#proc.run(payload);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Works for screen takeovers like editors
|
|
246
|
+
* @returns {SpawnSyncResponse}
|
|
247
|
+
*/
|
|
248
|
+
runSync() {
|
|
249
|
+
// @ts-ignore
|
|
250
|
+
return new SHExecute(this.#cmd, this.#options).runSync();
|
|
251
|
+
}
|
|
252
|
+
async kill() {
|
|
253
|
+
try {
|
|
254
|
+
await this.#proc.kill();
|
|
255
|
+
} catch (_e) {}
|
|
256
|
+
this.#proc = undefined;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
1
260
|
// Copyright 2021 Google LLC
|
|
2
261
|
//
|
|
3
262
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -19,64 +278,85 @@
|
|
|
19
278
|
// The core functionality of this code is highly beneficial. However, certain parts of the original code
|
|
20
279
|
// were overwriting the global namespace with core libraries and variables. This was causing conflicts with
|
|
21
280
|
// other packages (for instance, fetch) and introducing unexpected elements into my code base.
|
|
281
|
+
// The main $/SH method is all there is left, with barebone Promises and readable code.
|
|
22
282
|
// Changes Made:
|
|
23
283
|
// - The code has been or is being reformatted to comply with ES2020 standards.
|
|
24
|
-
// - Some methods were added and existing ones were modified to enhance usability.
|
|
284
|
+
// - Some methods were added and existing ones were modified or deleted to enhance usability.
|
|
285
|
+
// - Most methods were deleted,
|
|
25
286
|
// - The namespace has been changed from '$' to 'SH'.
|
|
26
287
|
// Modified by: jorrit.duin+sh[AT]gmail.com
|
|
27
288
|
|
|
28
|
-
import assert from 'node:assert';
|
|
29
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
30
|
-
import { log, parseDuration, quote, quotePowerShell, } from './utils.js';
|
|
31
|
-
import ProcessPromise from './ProcessPromise.js';
|
|
32
|
-
const storage = new AsyncLocalStorage();
|
|
33
289
|
|
|
34
|
-
const defaults = {
|
|
35
|
-
processCwd: '',
|
|
36
|
-
verbose: false,
|
|
37
|
-
env: {},
|
|
38
|
-
shell: 'bash',
|
|
39
|
-
prefix: '',
|
|
40
|
-
};
|
|
41
|
-
defaults.prefix = 'set -euo pipefail;/usr/bin/env';
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Escape CLI arguments
|
|
45
|
-
* @param {string[]} arg
|
|
46
|
-
* @retruns {string}
|
|
47
|
-
*/
|
|
48
|
-
const sanitizeArg = (arg) => {
|
|
49
|
-
const s = `${arg}`;
|
|
50
|
-
if (process.platform == 'win32') {
|
|
51
|
-
return quotePowerShell(s)
|
|
52
|
-
}
|
|
53
|
-
return quote(s);
|
|
54
|
-
}
|
|
55
290
|
|
|
56
|
-
const getStore = () => {
|
|
57
|
-
return storage.getStore() || defaults;
|
|
58
|
-
}
|
|
59
291
|
/**
|
|
60
|
-
* Creates a new
|
|
292
|
+
* Creates a new SHDispatch object that represents a command to be executed.
|
|
61
293
|
*
|
|
62
294
|
* @typedef {Function} Shell
|
|
63
295
|
* @property {function(Array, ...*): ProcessPromise} execute - The function to execute the command.
|
|
64
|
-
* @property {boolean} verbose - A property to control verbosity.
|
|
65
296
|
*
|
|
66
297
|
* @param {Array} pieces - An array of string literals from a template literal.
|
|
67
298
|
* @param {...*} args - The values to be interpolated into the string literals.
|
|
68
|
-
* @returns {
|
|
299
|
+
* @returns {SHDispatch} Trigger for the command.
|
|
69
300
|
* @throws {Error} Throws an error if any of the string literals in `pieces` is undefined.
|
|
70
301
|
*
|
|
71
302
|
* @example
|
|
72
|
-
* const command = await SH`echo 'Hello, world!'
|
|
303
|
+
* const command = await SH`echo 'Hello, world!'`.run();
|
|
73
304
|
*/
|
|
74
|
-
|
|
75
|
-
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* escape paramater commands
|
|
308
|
+
* @param {string} arg
|
|
309
|
+
* @returns {string}
|
|
310
|
+
*/
|
|
311
|
+
const quote = (arg) => {
|
|
312
|
+
if (/^[a-z0-9/_.\-@:=]+$/i.test(arg) || arg === '') {
|
|
313
|
+
return arg;
|
|
314
|
+
}
|
|
315
|
+
return (`'` +
|
|
316
|
+
arg
|
|
317
|
+
.replace(/\\/g, '\\\\')
|
|
318
|
+
.replace(/'/g, "\\'")
|
|
319
|
+
.replace(/\f/g, '\\f')
|
|
320
|
+
.replace(/\n/g, '\\n')
|
|
321
|
+
.replace(/\r/g, '\\r')
|
|
322
|
+
.replace(/\t/g, '\\t')
|
|
323
|
+
.replace(/\v/g, '\\v')
|
|
324
|
+
.replace(/\0/g, '\\0') +
|
|
325
|
+
`'`);
|
|
326
|
+
};
|
|
327
|
+
/**
|
|
328
|
+
* Escape CLI arguments
|
|
329
|
+
* @param {string[]} arg
|
|
330
|
+
* @retruns {string}
|
|
331
|
+
*/
|
|
332
|
+
const sanitizeArg = (arg) => {
|
|
333
|
+
const s = `${arg}`;
|
|
334
|
+
return quote(s);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* 4ms, 5s || 5
|
|
339
|
+
* @param {number|string} d
|
|
340
|
+
* @returns {number}
|
|
341
|
+
*/
|
|
342
|
+
const parseDuration = (d) => {
|
|
343
|
+
if (typeof d == 'number') {
|
|
344
|
+
if (isNaN(d) || d < 0)
|
|
345
|
+
throw new Error(`Invalid duration: "${d}".`);
|
|
346
|
+
return d;
|
|
347
|
+
}
|
|
348
|
+
else if (/\d+s/.test(d)) {
|
|
349
|
+
return +d.slice(0, -1) * 1000;
|
|
350
|
+
}
|
|
351
|
+
else if (/\d+ms/.test(d)) {
|
|
352
|
+
return +d.slice(0, -2);
|
|
353
|
+
}
|
|
354
|
+
throw new Error(`Unknown duration: "${d}".`);
|
|
355
|
+
};
|
|
356
|
+
/** @type {Shell & { (pieces: TemplateStringsArray, ...args: *): SHDispatch }} */
|
|
76
357
|
const SH = new Proxy(function(pieces, ...args) {
|
|
77
|
-
const from = new Error().stack.split(/^\s*at\s/m)[2].trim();
|
|
78
358
|
if (pieces.some((p) => p == undefined)) {
|
|
79
|
-
throw new Error(`Malformed command
|
|
359
|
+
throw new Error(`Malformed command ${pieces}`);
|
|
80
360
|
}
|
|
81
361
|
let cmd = pieces[0], i = 0;
|
|
82
362
|
while (i < args.length) {
|
|
@@ -89,29 +369,8 @@ const SH = new Proxy(function(pieces, ...args) {
|
|
|
89
369
|
}
|
|
90
370
|
cmd += s + pieces[++i];
|
|
91
371
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const promise = new ProcessPromise((...args) => ([resolve, reject] = args));
|
|
95
|
-
// re-add the environment
|
|
96
|
-
defaults.processCwd = process.cwd();
|
|
97
|
-
defaults.env = process.env,
|
|
98
|
-
promise._bind(cmd, from, resolve, reject, getStore());
|
|
99
|
-
// Postpone run to allow promise configuration.
|
|
100
|
-
setImmediate(() => promise.isHalted || promise.run());
|
|
101
|
-
return promise;
|
|
102
|
-
}, {
|
|
103
|
-
// this will get and set from:
|
|
104
|
-
// defaults OR storage (@see within());
|
|
105
|
-
set(_, key, value) {
|
|
106
|
-
const target = key in Function.prototype ? _ : getStore();
|
|
107
|
-
Reflect.set(target, key, value);
|
|
108
|
-
return true;
|
|
109
|
-
},
|
|
110
|
-
get(_, key) {
|
|
111
|
-
const target = key in Function.prototype ? _ : getStore();
|
|
112
|
-
return Reflect.get(target, key);
|
|
113
|
-
},
|
|
114
|
-
});
|
|
372
|
+
return new SHDispatch(cmd);
|
|
373
|
+
}, {});
|
|
115
374
|
|
|
116
375
|
/**
|
|
117
376
|
* Create a async context in an sync block
|
|
@@ -119,32 +378,40 @@ const SH = new Proxy(function(pieces, ...args) {
|
|
|
119
378
|
* @example
|
|
120
379
|
* const p = within(async () => {
|
|
121
380
|
* const res = await Promise.all([
|
|
122
|
-
* SH`sleep 1; echo 1
|
|
123
|
-
* SH`sleep 2; echo 2
|
|
381
|
+
* SH`sleep 1; echo 1`.run(),
|
|
382
|
+
* SH`sleep 2; echo 2`.run(),
|
|
124
383
|
* sleep(2),
|
|
125
|
-
* SH`sleep 3; echo 3
|
|
384
|
+
* SH`sleep 3; echo 3`.run()
|
|
126
385
|
* ]);
|
|
127
386
|
*/
|
|
128
387
|
const within = (callback) => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
388
|
+
(async () => {
|
|
389
|
+
return await callback()
|
|
390
|
+
})();
|
|
391
|
+
};
|
|
132
392
|
/**
|
|
133
|
-
* This function reads the standard input (stdin)
|
|
134
|
-
* It is used to get piped content into a script.
|
|
393
|
+
* This function reads the standard input (stdin) from the current process.
|
|
135
394
|
* @example
|
|
136
395
|
* const content = await stdin();
|
|
137
396
|
*/
|
|
138
|
-
const
|
|
397
|
+
const readIn = async () => {
|
|
139
398
|
let buf = '';
|
|
140
399
|
process.stdin.setEncoding('utf8');
|
|
141
400
|
for await (const chunk of process.stdin) {
|
|
142
401
|
buf += chunk;
|
|
143
402
|
}
|
|
144
403
|
return buf;
|
|
145
|
-
}
|
|
404
|
+
};
|
|
405
|
+
|
|
146
406
|
/**
|
|
147
|
-
*
|
|
407
|
+
* Retries a given asynchronous function a specified number of times with optional delays between attempts.
|
|
408
|
+
*
|
|
409
|
+
* @param {number} count - The number of retry attempts.
|
|
410
|
+
* @param {string|expBackoff|Function} a - Either a delay duration as a string, a delay generator object, or the callback function.
|
|
411
|
+
* @param {Function} [b] - The callback function to retry, required if `a` is not a function.
|
|
412
|
+
* @returns {Promise<*>} - The result of the callback function if it succeeds within the retry attempts.
|
|
413
|
+
* @throws {Error} - The last error encountered if all retry attempts fail.
|
|
414
|
+
*
|
|
148
415
|
* @example
|
|
149
416
|
* // Retry a command 3 times
|
|
150
417
|
* const p = await retry(3, () => SH`curl -s https://flipwrsi`);
|
|
@@ -156,12 +423,11 @@ const stdin = async () => {
|
|
|
156
423
|
* const p = await retry(3, expBackoff(), () => SH`curl -s https://flipwrsi`);
|
|
157
424
|
*/
|
|
158
425
|
const retry = async (count, a, b) => {
|
|
159
|
-
const total = count;
|
|
426
|
+
// const total = count;
|
|
160
427
|
let callback;
|
|
161
428
|
let delayStatic = 0;
|
|
162
429
|
let delayGen;
|
|
163
430
|
// @ts-ignore
|
|
164
|
-
const verbose = SH.verbose;
|
|
165
431
|
if (typeof a == 'function') {
|
|
166
432
|
callback = a;
|
|
167
433
|
}
|
|
@@ -176,9 +442,7 @@ const retry = async (count, a, b) => {
|
|
|
176
442
|
callback = b;
|
|
177
443
|
}
|
|
178
444
|
let lastErr;
|
|
179
|
-
let attempt = 0;
|
|
180
445
|
while (count-- > 0) {
|
|
181
|
-
attempt++;
|
|
182
446
|
try {
|
|
183
447
|
return await callback();
|
|
184
448
|
}
|
|
@@ -187,13 +451,6 @@ const retry = async (count, a, b) => {
|
|
|
187
451
|
if (delayStatic > 0)
|
|
188
452
|
delay = delayStatic;
|
|
189
453
|
if (delayGen) delay = delayGen.next().value;
|
|
190
|
-
log({
|
|
191
|
-
verbose,
|
|
192
|
-
kind: 'retry',
|
|
193
|
-
error: ' FAIL ' +
|
|
194
|
-
` Attempt: ${attempt}${total == Infinity ? '' : `/${total}`}` +
|
|
195
|
-
(delay > 0 ? `; next in ${delay}ms` : ''),
|
|
196
|
-
});
|
|
197
454
|
lastErr = err;
|
|
198
455
|
if (count == 0)
|
|
199
456
|
break;
|
|
@@ -202,34 +459,36 @@ const retry = async (count, a, b) => {
|
|
|
202
459
|
}
|
|
203
460
|
}
|
|
204
461
|
throw lastErr;
|
|
205
|
-
}
|
|
462
|
+
};
|
|
206
463
|
/**
|
|
207
464
|
* This function pauses or "sleeps" code execution for a specified duration.
|
|
208
465
|
* @param {string|number} duration - The duration to pause execution for, e.g., '100ms' or '3s'.
|
|
209
466
|
*
|
|
210
467
|
* @example
|
|
211
468
|
*
|
|
212
|
-
* const res = await
|
|
213
|
-
* SH`sleep 2; echo 2`, // Sleep for 2 seconds
|
|
214
|
-
* sleep(2), // Sleep for 2 seconds
|
|
215
|
-
* SH`sleep 3; echo 3` // Sleep for 3 seconds
|
|
216
|
-
* ]);
|
|
469
|
+
* const res = await sleep('5s');
|
|
217
470
|
*/
|
|
218
471
|
const sleep = (duration) => {
|
|
219
472
|
return new Promise((resolve) => {
|
|
220
473
|
setTimeout(resolve, parseDuration(duration));
|
|
221
474
|
});
|
|
222
|
-
}
|
|
475
|
+
};
|
|
223
476
|
/**
|
|
224
477
|
* Change working directory
|
|
225
478
|
* @param {string} dir
|
|
226
479
|
*/
|
|
227
480
|
const cd = (dir) => {
|
|
228
481
|
// @ts-ignore
|
|
229
|
-
const verbose = SH.verbose;
|
|
230
|
-
log({ kind: 'cd', dir, verbose });
|
|
231
482
|
process.chdir(dir);
|
|
232
|
-
}
|
|
483
|
+
};
|
|
484
|
+
/**
|
|
485
|
+
* Generates an exponential backoff time with a random jitter.
|
|
486
|
+
*
|
|
487
|
+
* @generator
|
|
488
|
+
* @param {string} [max='60s'] - The maximum backoff time in a human-readable format (e.g., '60s' for 60 seconds).
|
|
489
|
+
* @param {string} [rand='100ms'] - The maximum random jitter time in a human-readable format (e.g., '100ms' for 100 milliseconds).
|
|
490
|
+
* @yields {number} The backoff time in milliseconds.
|
|
491
|
+
*/
|
|
233
492
|
function* expBackoff(max = '60s', rand = '100ms') {
|
|
234
493
|
const maxMs = parseDuration(max);
|
|
235
494
|
const randMs = parseDuration(rand);
|
|
@@ -239,16 +498,5 @@ function* expBackoff(max = '60s', rand = '100ms') {
|
|
|
239
498
|
yield Math.min(2 ** n++, maxMs) + ms;
|
|
240
499
|
}
|
|
241
500
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// process.chdir(SH['processCwd']);
|
|
245
|
-
// }
|
|
246
|
-
export {
|
|
247
|
-
SH,
|
|
248
|
-
cd,
|
|
249
|
-
sleep,
|
|
250
|
-
retry,
|
|
251
|
-
stdin,
|
|
252
|
-
within,
|
|
253
|
-
expBackoff,
|
|
254
|
-
}
|
|
501
|
+
|
|
502
|
+
export { SH, cd, expBackoff, readIn, retry, sleep, within };
|
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": "0.0
|
|
5
|
+
"version": "1.0.0",
|
|
6
6
|
"description": "Execute shell commands on Linux-based systems from javascript",
|
|
7
7
|
"main": "lib/sh.js",
|
|
8
8
|
"engines": {
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"test": "test/sh.js",
|
|
13
|
+
"compile": "npx rollup src/sh.js --generatedCode es2015 --file lib/sh.js --format es --name SH && npm run types",
|
|
13
14
|
"publish": "npm run release && npm publish --access public",
|
|
14
15
|
"release": "npm pack --pack-destination=release",
|
|
15
|
-
"
|
|
16
|
-
"types": "
|
|
16
|
+
"types": "tsc lib/*.js --module nodenext --moduleResolution nodenext --declaration --allowJs --emitDeclarationOnly --outDir lib/",
|
|
17
|
+
"clear:types": "rm lib/*.d.ts"
|
|
17
18
|
},
|
|
18
19
|
"repository": {
|
|
19
20
|
"type": "git",
|
package/lib/ProcessOutput.d.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
export default ProcessOutput;
|
|
3
|
-
/**
|
|
4
|
-
* The ProcessPromise returns a ProcessOutput even when it fails, by rejecting it.
|
|
5
|
-
* The extension of the Error class is implemented to ensure compatibility when an Error is expected upon rejection.
|
|
6
|
-
*/
|
|
7
|
-
declare class ProcessOutput extends Error {
|
|
8
|
-
/**
|
|
9
|
-
* @param {number} code - exit code
|
|
10
|
-
* @param {string} signal - SIGTERM ...
|
|
11
|
-
* @param {string} stdout - std reponse string
|
|
12
|
-
* @param {string} stderr - error reponse string
|
|
13
|
-
* @param {string} combined - stderr + stdout
|
|
14
|
-
* @param {string} message - Error message
|
|
15
|
-
*/
|
|
16
|
-
constructor(code: number, signal: string, stdout?: string, stderr?: string, combined?: string, message?: string);
|
|
17
|
-
/**
|
|
18
|
-
* This string represents the standard output (stdout) from the child process.
|
|
19
|
-
* @returns {string}
|
|
20
|
-
*/
|
|
21
|
-
get stdout(): string;
|
|
22
|
-
/**
|
|
23
|
-
* This string represents the error output (stderr) from the child process.
|
|
24
|
-
* @returns {string}
|
|
25
|
-
*/
|
|
26
|
-
get stderr(): string;
|
|
27
|
-
/**
|
|
28
|
-
* This represents the exit code returned by the external process.
|
|
29
|
-
* @returns {number} The exit
|
|
30
|
-
*/
|
|
31
|
-
get exitCode(): number;
|
|
32
|
-
/**
|
|
33
|
-
* This represents the exit signal, for example, "SIGTERM", received from the child process.
|
|
34
|
-
* @returns {string} The exit signal from the child
|
|
35
|
-
*/
|
|
36
|
-
get signal(): string;
|
|
37
|
-
/**
|
|
38
|
-
* This method is used for debugging purposes. It displays the current state of the object
|
|
39
|
-
* when passed to the console.log function.
|
|
40
|
-
*/
|
|
41
|
-
[inspect.custom](): string;
|
|
42
|
-
#private;
|
|
43
|
-
}
|
|
44
|
-
import { inspect } from 'node:util';
|