@j-o-r/sh 1.0.2 → 1.0.3
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 +254 -0
- package/lib/SHDispatch.js +151 -0
- package/lib/SHExecute.js +154 -0
- package/package.json +5 -5
- package/{lib/sh.d.ts → types/SH.d.ts} +19 -100
- package/types/SHDispatch.d.ts +82 -0
- package/types/SHExecute.d.ts +24 -0
- package/lib/sh.js +0 -538
package/lib/SH.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// Copyright 2021 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
//
|
|
16
|
+
// Original Source: zx
|
|
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
|
|
21
|
+
// other packages (for instance, fetch) and introducing unexpected elements into my code base.
|
|
22
|
+
// The main $/SH method is all there is left, with barebone Promises and readable code.
|
|
23
|
+
// Changes Made:
|
|
24
|
+
// - The code has been or is being reformatted to comply with ES2020 standards.
|
|
25
|
+
// - Some methods were added and existing ones were modified or deleted to enhance usability.
|
|
26
|
+
// - Most methods were deleted,
|
|
27
|
+
// - The namespace has been changed from '$' to 'SH'.
|
|
28
|
+
// Modified by: jorrit.duin+sh[AT]gmail.com
|
|
29
|
+
import assert from 'node:assert';
|
|
30
|
+
import SHDispatch from './SHDispatch.js';
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Creates a new SHDispatch object that represents a command to be executed.
|
|
35
|
+
*
|
|
36
|
+
* @typedef {Function} Shell
|
|
37
|
+
* @property {function(Array, ...*): ProcessPromise} execute - The function to execute the command.
|
|
38
|
+
*
|
|
39
|
+
* @param {Array} pieces - An array of string literals from a template literal.
|
|
40
|
+
* @param {...*} args - The values to be interpolated into the string literals.
|
|
41
|
+
* @returns {SHDispatch} Trigger for the command.
|
|
42
|
+
* @throws {Error} Throws an error if any of the string literals in `pieces` is undefined.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* const command = await SH`echo 'Hello, world!'`.run();
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* escape paramater commands
|
|
50
|
+
* @param {string} arg
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
const quote = (arg) => {
|
|
54
|
+
if (/^[a-z0-9/_.\-@:=]+$/i.test(arg) || arg === '') {
|
|
55
|
+
return arg;
|
|
56
|
+
}
|
|
57
|
+
return (`'` +
|
|
58
|
+
arg
|
|
59
|
+
.replace(/\\/g, '\\\\')
|
|
60
|
+
.replace(/'/g, "\\'")
|
|
61
|
+
.replace(/\f/g, '\\f')
|
|
62
|
+
.replace(/\n/g, '\\n')
|
|
63
|
+
.replace(/\r/g, '\\r')
|
|
64
|
+
.replace(/\t/g, '\\t')
|
|
65
|
+
.replace(/\v/g, '\\v')
|
|
66
|
+
.replace(/\0/g, '\\0') +
|
|
67
|
+
`'`);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Escape CLI arguments
|
|
71
|
+
* @param {string[]} arg
|
|
72
|
+
* @retruns {string}
|
|
73
|
+
*/
|
|
74
|
+
const sanitizeArg = (arg) => {
|
|
75
|
+
const s = `${arg}`;
|
|
76
|
+
return quote(s);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 4ms, 5s || 5
|
|
81
|
+
* @param {number|string} d
|
|
82
|
+
* @returns {number}
|
|
83
|
+
*/
|
|
84
|
+
const parseDuration = (d) => {
|
|
85
|
+
if (typeof d == 'number') {
|
|
86
|
+
if (isNaN(d) || d < 0)
|
|
87
|
+
throw new Error(`Invalid duration: "${d}".`);
|
|
88
|
+
return d;
|
|
89
|
+
}
|
|
90
|
+
else if (/\d+s/.test(d)) {
|
|
91
|
+
return +d.slice(0, -1) * 1000;
|
|
92
|
+
}
|
|
93
|
+
else if (/\d+ms/.test(d)) {
|
|
94
|
+
return +d.slice(0, -2);
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`Unknown duration: "${d}".`);
|
|
97
|
+
}
|
|
98
|
+
/** @type {Shell & { (pieces: TemplateStringsArray, ...args: *): SHDispatch }} */
|
|
99
|
+
const SH = new Proxy(function(pieces, ...args) {
|
|
100
|
+
if (pieces.some((p) => p == undefined)) {
|
|
101
|
+
throw new Error(`Malformed command ${pieces}`);
|
|
102
|
+
}
|
|
103
|
+
let cmd = pieces[0], i = 0;
|
|
104
|
+
while (i < args.length) {
|
|
105
|
+
let s;
|
|
106
|
+
if (Array.isArray(args[i])) {
|
|
107
|
+
s = args[i].map((x) => sanitizeArg(x)).join(' ');
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
s = sanitizeArg(args[i]);
|
|
111
|
+
}
|
|
112
|
+
cmd += s + pieces[++i];
|
|
113
|
+
}
|
|
114
|
+
return new SHDispatch(cmd);
|
|
115
|
+
}, {});
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create a async context in an sync block
|
|
119
|
+
* @param {function} callback - async function
|
|
120
|
+
* @example
|
|
121
|
+
* const p = within(async () => {
|
|
122
|
+
* const res = await Promise.all([
|
|
123
|
+
* SH`sleep 1; echo 1`.run(),
|
|
124
|
+
* SH`sleep 2; echo 2`.run(),
|
|
125
|
+
* sleep(2),
|
|
126
|
+
* SH`sleep 3; echo 3`.run()
|
|
127
|
+
* ]);
|
|
128
|
+
*/
|
|
129
|
+
const within = (callback) => {
|
|
130
|
+
(async () => {
|
|
131
|
+
return await callback()
|
|
132
|
+
})()
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* This function reads the standard input (stdin) from the current process.
|
|
136
|
+
* @example
|
|
137
|
+
* const content = await stdin();
|
|
138
|
+
*/
|
|
139
|
+
const readIn = async () => {
|
|
140
|
+
let buf = '';
|
|
141
|
+
process.stdin.setEncoding('utf8');
|
|
142
|
+
for await (const chunk of process.stdin) {
|
|
143
|
+
buf += chunk;
|
|
144
|
+
}
|
|
145
|
+
return buf;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Retries a given asynchronous function a specified number of times with optional delays between attempts.
|
|
150
|
+
*
|
|
151
|
+
* @param {number} count - The number of retry attempts.
|
|
152
|
+
* @param {string|expBackoff|Function} a - Either a delay duration as a string, a delay generator object, or the callback function.
|
|
153
|
+
* @param {Function} [b] - The callback function to retry, required if `a` is not a function.
|
|
154
|
+
* @returns {Promise<*>} - The result of the callback function if it succeeds within the retry attempts.
|
|
155
|
+
* @throws {Error} - The last error encountered if all retry attempts fail.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* // Retry a command 3 times
|
|
159
|
+
* const p = await retry(3, () => SH`curl -s https://flipwrsi`);
|
|
160
|
+
*
|
|
161
|
+
* // Retry a command 3 times with an interval of 1 second between each try
|
|
162
|
+
* const p = await retry(3, '1s', () => SH`curl -s https://flipwrsi`);
|
|
163
|
+
*
|
|
164
|
+
* // Retry a command 3 times with irregular intervals using exponential backoff
|
|
165
|
+
* const p = await retry(3, expBackoff(), () => SH`curl -s https://flipwrsi`);
|
|
166
|
+
*/
|
|
167
|
+
const retry = async (count, a, b) => {
|
|
168
|
+
// const total = count;
|
|
169
|
+
let callback;
|
|
170
|
+
let delayStatic = 0;
|
|
171
|
+
let delayGen;
|
|
172
|
+
// @ts-ignore
|
|
173
|
+
if (typeof a == 'function') {
|
|
174
|
+
callback = a;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
if (typeof a == 'object') {
|
|
178
|
+
delayGen = a;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
delayStatic = parseDuration(a);
|
|
182
|
+
}
|
|
183
|
+
assert(b);
|
|
184
|
+
callback = b;
|
|
185
|
+
}
|
|
186
|
+
let lastErr;
|
|
187
|
+
let attempt = 0;
|
|
188
|
+
while (count-- > 0) {
|
|
189
|
+
attempt++;
|
|
190
|
+
try {
|
|
191
|
+
return await callback();
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
let delay = 0;
|
|
195
|
+
if (delayStatic > 0)
|
|
196
|
+
delay = delayStatic;
|
|
197
|
+
// @ts-ignore
|
|
198
|
+
if (delayGen) delay = delayGen.next().value;
|
|
199
|
+
lastErr = err;
|
|
200
|
+
if (count == 0)
|
|
201
|
+
break;
|
|
202
|
+
if (delay)
|
|
203
|
+
await sleep(delay);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
throw lastErr;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* This function pauses or "sleeps" code execution for a specified duration.
|
|
210
|
+
* @param {string|number} duration - The duration to pause execution for, e.g., '100ms' or '3s'.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
*
|
|
214
|
+
* const res = await sleep('5s');
|
|
215
|
+
*/
|
|
216
|
+
const sleep = (duration) => {
|
|
217
|
+
return new Promise((resolve) => {
|
|
218
|
+
setTimeout(resolve, parseDuration(duration));
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Change working directory
|
|
223
|
+
* @param {string} dir
|
|
224
|
+
*/
|
|
225
|
+
const cd = (dir) => {
|
|
226
|
+
// @ts-ignore
|
|
227
|
+
process.chdir(dir);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Generates an exponential backoff time with a random jitter.
|
|
231
|
+
*
|
|
232
|
+
* @generator
|
|
233
|
+
* @param {string} [max='60s'] - The maximum backoff time in a human-readable format (e.g., '60s' for 60 seconds).
|
|
234
|
+
* @param {string} [rand='100ms'] - The maximum random jitter time in a human-readable format (e.g., '100ms' for 100 milliseconds).
|
|
235
|
+
* @yields {number} The backoff time in milliseconds.
|
|
236
|
+
*/
|
|
237
|
+
function* expBackoff(max = '60s', rand = '100ms') {
|
|
238
|
+
const maxMs = parseDuration(max);
|
|
239
|
+
const randMs = parseDuration(rand);
|
|
240
|
+
let n = 1;
|
|
241
|
+
while (true) {
|
|
242
|
+
const ms = Math.floor(Math.random() * randMs);
|
|
243
|
+
yield Math.min(2 ** n++, maxMs) + ms;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
export {
|
|
247
|
+
SH,
|
|
248
|
+
cd,
|
|
249
|
+
sleep,
|
|
250
|
+
retry,
|
|
251
|
+
readIn,
|
|
252
|
+
within,
|
|
253
|
+
expBackoff,
|
|
254
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import SHExec from './SHExecute.js';
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {Object} SpawnSyncResponse
|
|
4
|
+
* @property {number} status - The exit code of the child process. A value of `0` indicates success.
|
|
5
|
+
* @property {Buffer|null} signal - The signal used to terminate the process, if any.
|
|
6
|
+
* @property {Array<string|null>} output - An array containing the standard output and standard error of the child process.
|
|
7
|
+
* @property {number} pid - The process ID of the child process.
|
|
8
|
+
* @property {Buffer|null} stdout - The standard output of the child process.
|
|
9
|
+
* @property {Buffer|null} stderr - The standard error of the child process.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Default options for the execution environment.
|
|
13
|
+
*
|
|
14
|
+
* @typedef {Object} SHOptions
|
|
15
|
+
* @property {string} [cwd] - The current working directory.
|
|
16
|
+
* @property {NodeJS.ProcessEnv} [env] - The environment variables.
|
|
17
|
+
* @property {string} [shell] - The shell to use for execution.
|
|
18
|
+
* @property {string} [prefix] - The prefix commands to ensure a safe execution environment. e.g: prefix: 'set -euo pipefail;/usr/bin/env',
|
|
19
|
+
* @property {StdioOptions|StdioOption} [stdio] - The stdio configuration.
|
|
20
|
+
* @property {number} [timeout] - default 20000: a timeout error is triggerd when a process execution time is exceeded, 0 is no timeout
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {('pipe' | 'ignore' | 'inherit' | number)} StdioOption
|
|
24
|
+
* @description Defines the stdio configuration for each of the standard streams.
|
|
25
|
+
*
|
|
26
|
+
* - 'pipe' creates a pipe between the child process and the parent process.
|
|
27
|
+
* The parent end of the pipe is exposed as a property on the `ChildProcess` object.
|
|
28
|
+
* - 'ignore' indicates that the child process's corresponding stdio file descriptor will be ignored.
|
|
29
|
+
* - 'inherit' passes the corresponding stdio stream to/from the child process.
|
|
30
|
+
* - Stream object to be used for the stdio stream.
|
|
31
|
+
* - Positive integer representing a file descriptor to be used for the stdio stream.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {Array<StdioOption>|StdioOption} StdioOptions
|
|
35
|
+
* @description
|
|
36
|
+
* Configures the stdio streams for the child process. This can be an array or a single StdioOption.
|
|
37
|
+
*
|
|
38
|
+
* Array Form: Specify the configuration for [stdin, stdout, stderr].
|
|
39
|
+
* - If array length is more than 3, additional positions correspond to extra streams.
|
|
40
|
+
* Single Value: This value will be applied to stdin, stdout, and stderr.
|
|
41
|
+
*
|
|
42
|
+
* Examples:
|
|
43
|
+
* - ['pipe', 'pipe', 'ignore']: Pipe stdin and stdout, ignore stderr.
|
|
44
|
+
* - 'inherit': Inherit all stdio streams from the parent.
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* 'Code Safe' has own prop
|
|
48
|
+
*
|
|
49
|
+
* @param {any} o - object to examine
|
|
50
|
+
* @param {string} p - property to look for
|
|
51
|
+
* @returns {boolean}
|
|
52
|
+
*/
|
|
53
|
+
const hasProp = (o, p) => {
|
|
54
|
+
if (typeof o === 'undefined') {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return Object.prototype.hasOwnProperty.call(o, p);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Merge property values while maintaining the fixed set of props in the original object
|
|
62
|
+
* @param {SHOptions} predefined - original object
|
|
63
|
+
* @param {SHOptions} options - object with new values
|
|
64
|
+
* @returns {SHOptions}
|
|
65
|
+
*/
|
|
66
|
+
const mergeOptions = (predefined, options) => {
|
|
67
|
+
// Extract the keys from the predefined object
|
|
68
|
+
const keys = Object.keys(predefined);
|
|
69
|
+
|
|
70
|
+
// Use reduce to accumulate only the predefined properties from sourceObj
|
|
71
|
+
const mergedObj = keys.reduce((acc, key) => {
|
|
72
|
+
if (hasProp(options, key)) {
|
|
73
|
+
acc[key] = options[key];
|
|
74
|
+
} else {
|
|
75
|
+
acc[key] = predefined[key];
|
|
76
|
+
}
|
|
77
|
+
return acc;
|
|
78
|
+
}, {});
|
|
79
|
+
|
|
80
|
+
return mergedObj;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
/** @type {SHOptions} */
|
|
85
|
+
const defaultOptions = {
|
|
86
|
+
cwd: process.cwd(),
|
|
87
|
+
env: process.env,
|
|
88
|
+
shell: 'bash',
|
|
89
|
+
prefix: '/usr/bin/env',
|
|
90
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
91
|
+
timeout: 10000 // when 0 there is no timeout
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class SHDispatch {
|
|
97
|
+
#cmd = '';
|
|
98
|
+
#options = {};
|
|
99
|
+
/**
|
|
100
|
+
* @type {SHExec}
|
|
101
|
+
*/
|
|
102
|
+
#proc;
|
|
103
|
+
/**
|
|
104
|
+
* @param {string} cmd - cmd to execute
|
|
105
|
+
*/
|
|
106
|
+
constructor(cmd) {
|
|
107
|
+
this.#cmd = cmd;
|
|
108
|
+
this.#options = defaultOptions
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* @param {SHOptions} options
|
|
112
|
+
* @returns {SHDispatch}
|
|
113
|
+
*/
|
|
114
|
+
options(options) {
|
|
115
|
+
if (options.stdio && typeof options.stdio === 'string') {
|
|
116
|
+
// convert stdio to array
|
|
117
|
+
// This sets the default io values
|
|
118
|
+
// but can be overwritten when having a payload
|
|
119
|
+
const io = options.stdio;
|
|
120
|
+
options.stdio = Array(3).fill(io);
|
|
121
|
+
}
|
|
122
|
+
this.#options = mergeOptions(defaultOptions, options)
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} [payload]
|
|
127
|
+
* @returns {Promise<string>}
|
|
128
|
+
*/
|
|
129
|
+
run(payload) {
|
|
130
|
+
this.#proc = new SHExec(this.#cmd, this.#options);
|
|
131
|
+
return this.#proc.run(payload);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Works for screen takeovers like editors
|
|
136
|
+
* @param {string} [payload]
|
|
137
|
+
* @returns {SpawnSyncResponse}
|
|
138
|
+
*/
|
|
139
|
+
runSync(payload) {
|
|
140
|
+
// @ts-ignore
|
|
141
|
+
return new SHExec(this.#cmd, this.#options).runSync(payload);
|
|
142
|
+
}
|
|
143
|
+
async kill() {
|
|
144
|
+
try {
|
|
145
|
+
await this.#proc.kill();
|
|
146
|
+
} catch (_e) { }
|
|
147
|
+
this.#proc = undefined;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export default SHDispatch
|
package/lib/SHExecute.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
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
|
+
/**
|
|
49
|
+
* @type {import('child_process').ChildProcess}
|
|
50
|
+
*/
|
|
51
|
+
#proc;
|
|
52
|
+
#command = '';
|
|
53
|
+
#options = {};
|
|
54
|
+
#stdout = '';
|
|
55
|
+
#stderr = '';
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} command - linux command to be executed
|
|
58
|
+
* @param {import('./SHDispatch').SHOptions} [options] - ChildProcess options
|
|
59
|
+
*/
|
|
60
|
+
constructor(command, options = {}) {
|
|
61
|
+
this.#command = command;
|
|
62
|
+
this.#options = options;
|
|
63
|
+
this.#proc = null;
|
|
64
|
+
this.#stdout = '';
|
|
65
|
+
this.#stderr = '';
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* @param {string} [payload] - data to write
|
|
69
|
+
* @retuns {Promise<object>}
|
|
70
|
+
*/
|
|
71
|
+
runSync(payload) {
|
|
72
|
+
if (payload && typeof payload !== 'string') {
|
|
73
|
+
throw new Error('Argument is not a string');
|
|
74
|
+
}
|
|
75
|
+
let { cwd, shell, env, stdio } = this.#options;
|
|
76
|
+
// pipe need to be set on stdin when posting a payload
|
|
77
|
+
if (payload) stdio[0] = 'pipe';
|
|
78
|
+
const input = payload || undefined;
|
|
79
|
+
return spawnSync(this.#options.prefix, [this.#command], {
|
|
80
|
+
cwd,
|
|
81
|
+
shell,
|
|
82
|
+
stdio,
|
|
83
|
+
windowsHide: true,
|
|
84
|
+
env,
|
|
85
|
+
input
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* @param {string} [payload] - data to write
|
|
90
|
+
* @retuns {Promise<string>}
|
|
91
|
+
*/
|
|
92
|
+
run(payload) {
|
|
93
|
+
let to = 0;
|
|
94
|
+
if (payload && typeof payload !== 'string') {
|
|
95
|
+
throw new Error('Argument is not a string');
|
|
96
|
+
}
|
|
97
|
+
if (this.#options.timeout) {
|
|
98
|
+
to = this.#options.timeout;
|
|
99
|
+
}
|
|
100
|
+
let { cwd, shell, env, stdio } = this.#options;
|
|
101
|
+
// pipe need to be set on stdin when posting a payload
|
|
102
|
+
if (payload) stdio[0] = 'pipe';
|
|
103
|
+
this.#proc = spawn(this.#options.prefix, [this.#command], {
|
|
104
|
+
cwd,
|
|
105
|
+
shell,
|
|
106
|
+
stdio,
|
|
107
|
+
windowsHide: true,
|
|
108
|
+
env,
|
|
109
|
+
});
|
|
110
|
+
this.#proc.stdout?.on('data', (data) => {
|
|
111
|
+
this.#stdout += data;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
this.#proc.stderr?.on('data', (data) => {
|
|
115
|
+
this.#stderr += data;
|
|
116
|
+
});
|
|
117
|
+
if (payload) {
|
|
118
|
+
this.#proc.stdin.end(payload);
|
|
119
|
+
}
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
let timeout;
|
|
122
|
+
if (to > 0) {
|
|
123
|
+
timeout = setTimeout(async () => {
|
|
124
|
+
this.#proc.kill();
|
|
125
|
+
reject(new Error(`Process timed out: ${this.#command}`));
|
|
126
|
+
}, to); // options.timeout
|
|
127
|
+
}
|
|
128
|
+
this.#proc.on('close', (code) => {
|
|
129
|
+
if (timeout) clearTimeout(timeout);
|
|
130
|
+
if (code === 0) {
|
|
131
|
+
resolve(this.#stdout.trim());
|
|
132
|
+
} else {
|
|
133
|
+
reject(new Error(`${code}: ${this.#command} "${this.#stderr.trim()}"`));
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
this.#proc.on('error', (err) => {
|
|
138
|
+
reject(err);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* @returns {Promise<number[]>}
|
|
144
|
+
*/
|
|
145
|
+
async kill(signal = 'SIGTERM') {
|
|
146
|
+
if (!this.#proc) throw new Error('Trying to kill a process without creating one.');
|
|
147
|
+
if (!this.#proc.pid) throw new Error('The process pid is undefined.');
|
|
148
|
+
|
|
149
|
+
return killProcesses(this.#proc.pid, signal);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export default SHExecute;
|
|
154
|
+
|
package/package.json
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
"name": "@j-o-r/sh",
|
|
3
3
|
"author": "Jorrit Duin <j-o-r@duin.work>",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.3",
|
|
6
6
|
"description": "Execute shell commands on Linux-based systems from javascript",
|
|
7
|
-
"main": "lib/
|
|
7
|
+
"main": "lib/SH.js",
|
|
8
|
+
"types": "types/SH.d.ts",
|
|
8
9
|
"engines": {
|
|
9
10
|
"node": ">=20.0.0"
|
|
10
11
|
},
|
|
11
12
|
"scripts": {
|
|
12
13
|
"test": "test/sh.js",
|
|
13
|
-
"compile": "npx rollup src/sh.js --generatedCode es2015 --file lib/sh.js --format es --name SH && npm run types",
|
|
14
14
|
"publish": "npm run release && npm publish --access public",
|
|
15
15
|
"release": "npm pack --pack-destination=release",
|
|
16
|
-
"types": "tsc lib/*.js --module nodenext --moduleResolution nodenext --declaration --allowJs --emitDeclarationOnly --outDir
|
|
17
|
-
"clear:types": "rm
|
|
16
|
+
"types": "tsc lib/*.js --module nodenext --moduleResolution nodenext --declaration --allowJs --emitDeclarationOnly --outDir types/",
|
|
17
|
+
"clear:types": "rm types/*.d.ts"
|
|
18
18
|
},
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
@@ -1,60 +1,3 @@
|
|
|
1
|
-
export type SpawnSyncResponse = {
|
|
2
|
-
/**
|
|
3
|
-
* - The exit code of the child process. A value of `0` indicates success.
|
|
4
|
-
*/
|
|
5
|
-
status: number;
|
|
6
|
-
/**
|
|
7
|
-
* - The signal used to terminate the process, if any.
|
|
8
|
-
*/
|
|
9
|
-
signal: Buffer | null;
|
|
10
|
-
/**
|
|
11
|
-
* - An array containing the standard output and standard error of the child process.
|
|
12
|
-
*/
|
|
13
|
-
output: Array<string | null>;
|
|
14
|
-
/**
|
|
15
|
-
* - The process ID of the child process.
|
|
16
|
-
*/
|
|
17
|
-
pid: number;
|
|
18
|
-
/**
|
|
19
|
-
* - The standard output of the child process.
|
|
20
|
-
*/
|
|
21
|
-
stdout: Buffer | null;
|
|
22
|
-
/**
|
|
23
|
-
* - The standard error of the child process.
|
|
24
|
-
*/
|
|
25
|
-
stderr: Buffer | null;
|
|
26
|
-
};
|
|
27
|
-
/**
|
|
28
|
-
* Default options for the execution environment.
|
|
29
|
-
*/
|
|
30
|
-
export type SHOptions = {
|
|
31
|
-
/**
|
|
32
|
-
* - The current working directory.
|
|
33
|
-
*/
|
|
34
|
-
cwd?: string;
|
|
35
|
-
/**
|
|
36
|
-
* - The environment variables.
|
|
37
|
-
*/
|
|
38
|
-
env?: NodeJS.ProcessEnv;
|
|
39
|
-
/**
|
|
40
|
-
* - The shell to use for execution.
|
|
41
|
-
*/
|
|
42
|
-
shell?: string;
|
|
43
|
-
/**
|
|
44
|
-
* - The prefix commands to ensure a safe execution environment.
|
|
45
|
-
*/
|
|
46
|
-
prefix?: string;
|
|
47
|
-
/**
|
|
48
|
-
* - The stdio configuration.
|
|
49
|
-
*/
|
|
50
|
-
stdio?: StdioOptions | StdioOption;
|
|
51
|
-
/**
|
|
52
|
-
* - default 20000: a timeout error is triggerd when a process execution time is exceeded, 0 is no timeout
|
|
53
|
-
*/
|
|
54
|
-
timeout?: number;
|
|
55
|
-
};
|
|
56
|
-
export type StdioOption = ('pipe' | 'ignore' | 'inherit' | number);
|
|
57
|
-
export type StdioOptions = Array<StdioOption> | StdioOption;
|
|
58
1
|
/**
|
|
59
2
|
* Creates a new SHDispatch object that represents a command to be executed.
|
|
60
3
|
*/
|
|
@@ -69,20 +12,14 @@ export const SH: Shell & {
|
|
|
69
12
|
*/
|
|
70
13
|
export function cd(dir: string): void;
|
|
71
14
|
/**
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
* @param {string} [max='60s'] - The maximum backoff time in a human-readable format (e.g., '60s' for 60 seconds).
|
|
76
|
-
* @param {string} [rand='100ms'] - The maximum random jitter time in a human-readable format (e.g., '100ms' for 100 milliseconds).
|
|
77
|
-
* @yields {number} The backoff time in milliseconds.
|
|
78
|
-
*/
|
|
79
|
-
export function expBackoff(max?: string, rand?: string): Generator<number, void, unknown>;
|
|
80
|
-
/**
|
|
81
|
-
* This function reads the standard input (stdin) from the current process.
|
|
15
|
+
* This function pauses or "sleeps" code execution for a specified duration.
|
|
16
|
+
* @param {string|number} duration - The duration to pause execution for, e.g., '100ms' or '3s'.
|
|
17
|
+
*
|
|
82
18
|
* @example
|
|
83
|
-
*
|
|
19
|
+
*
|
|
20
|
+
* const res = await sleep('5s');
|
|
84
21
|
*/
|
|
85
|
-
export function
|
|
22
|
+
export function sleep(duration: string | number): Promise<any>;
|
|
86
23
|
/**
|
|
87
24
|
* Retries a given asynchronous function a specified number of times with optional delays between attempts.
|
|
88
25
|
*
|
|
@@ -104,14 +41,11 @@ export function readIn(): Promise<string>;
|
|
|
104
41
|
*/
|
|
105
42
|
export function retry(count: number, a: string | typeof expBackoff | Function, b?: Function): Promise<any>;
|
|
106
43
|
/**
|
|
107
|
-
* This function
|
|
108
|
-
* @param {string|number} duration - The duration to pause execution for, e.g., '100ms' or '3s'.
|
|
109
|
-
*
|
|
44
|
+
* This function reads the standard input (stdin) from the current process.
|
|
110
45
|
* @example
|
|
111
|
-
*
|
|
112
|
-
* const res = await sleep('5s');
|
|
46
|
+
* const content = await stdin();
|
|
113
47
|
*/
|
|
114
|
-
export function
|
|
48
|
+
export function readIn(): Promise<string>;
|
|
115
49
|
/**
|
|
116
50
|
* Create a async context in an sync block
|
|
117
51
|
* @param {function} callback - async function
|
|
@@ -125,28 +59,13 @@ export function sleep(duration: string | number): Promise<any>;
|
|
|
125
59
|
* ]);
|
|
126
60
|
*/
|
|
127
61
|
export function within(callback: Function): void;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* @param {string} [payload]
|
|
140
|
-
* @returns {Promise<string>}
|
|
141
|
-
*/
|
|
142
|
-
run(payload?: string): Promise<string>;
|
|
143
|
-
/**
|
|
144
|
-
* Works for screen takeovers like editors
|
|
145
|
-
* @param {string} [payload]
|
|
146
|
-
* @returns {SpawnSyncResponse}
|
|
147
|
-
*/
|
|
148
|
-
runSync(payload?: string): SpawnSyncResponse;
|
|
149
|
-
kill(): Promise<void>;
|
|
150
|
-
#private;
|
|
151
|
-
}
|
|
152
|
-
export {};
|
|
62
|
+
/**
|
|
63
|
+
* Generates an exponential backoff time with a random jitter.
|
|
64
|
+
*
|
|
65
|
+
* @generator
|
|
66
|
+
* @param {string} [max='60s'] - The maximum backoff time in a human-readable format (e.g., '60s' for 60 seconds).
|
|
67
|
+
* @param {string} [rand='100ms'] - The maximum random jitter time in a human-readable format (e.g., '100ms' for 100 milliseconds).
|
|
68
|
+
* @yields {number} The backoff time in milliseconds.
|
|
69
|
+
*/
|
|
70
|
+
export function expBackoff(max?: string, rand?: string): Generator<number, void, unknown>;
|
|
71
|
+
import SHDispatch from './SHDispatch.js';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export default SHDispatch;
|
|
2
|
+
export type SpawnSyncResponse = {
|
|
3
|
+
/**
|
|
4
|
+
* - The exit code of the child process. A value of `0` indicates success.
|
|
5
|
+
*/
|
|
6
|
+
status: number;
|
|
7
|
+
/**
|
|
8
|
+
* - The signal used to terminate the process, if any.
|
|
9
|
+
*/
|
|
10
|
+
signal: Buffer | null;
|
|
11
|
+
/**
|
|
12
|
+
* - An array containing the standard output and standard error of the child process.
|
|
13
|
+
*/
|
|
14
|
+
output: Array<string | null>;
|
|
15
|
+
/**
|
|
16
|
+
* - The process ID of the child process.
|
|
17
|
+
*/
|
|
18
|
+
pid: number;
|
|
19
|
+
/**
|
|
20
|
+
* - The standard output of the child process.
|
|
21
|
+
*/
|
|
22
|
+
stdout: Buffer | null;
|
|
23
|
+
/**
|
|
24
|
+
* - The standard error of the child process.
|
|
25
|
+
*/
|
|
26
|
+
stderr: Buffer | null;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Default options for the execution environment.
|
|
30
|
+
*/
|
|
31
|
+
export type SHOptions = {
|
|
32
|
+
/**
|
|
33
|
+
* - The current working directory.
|
|
34
|
+
*/
|
|
35
|
+
cwd?: string;
|
|
36
|
+
/**
|
|
37
|
+
* - The environment variables.
|
|
38
|
+
*/
|
|
39
|
+
env?: NodeJS.ProcessEnv;
|
|
40
|
+
/**
|
|
41
|
+
* - The shell to use for execution.
|
|
42
|
+
*/
|
|
43
|
+
shell?: string;
|
|
44
|
+
/**
|
|
45
|
+
* - The prefix commands to ensure a safe execution environment. e.g: prefix: 'set -euo pipefail;/usr/bin/env',
|
|
46
|
+
*/
|
|
47
|
+
prefix?: string;
|
|
48
|
+
/**
|
|
49
|
+
* - The stdio configuration.
|
|
50
|
+
*/
|
|
51
|
+
stdio?: StdioOptions | StdioOption;
|
|
52
|
+
/**
|
|
53
|
+
* - default 20000: a timeout error is triggerd when a process execution time is exceeded, 0 is no timeout
|
|
54
|
+
*/
|
|
55
|
+
timeout?: number;
|
|
56
|
+
};
|
|
57
|
+
export type StdioOption = ('pipe' | 'ignore' | 'inherit' | number);
|
|
58
|
+
export type StdioOptions = Array<StdioOption> | StdioOption;
|
|
59
|
+
declare class SHDispatch {
|
|
60
|
+
/**
|
|
61
|
+
* @param {string} cmd - cmd to execute
|
|
62
|
+
*/
|
|
63
|
+
constructor(cmd: string);
|
|
64
|
+
/**
|
|
65
|
+
* @param {SHOptions} options
|
|
66
|
+
* @returns {SHDispatch}
|
|
67
|
+
*/
|
|
68
|
+
options(options: SHOptions): SHDispatch;
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} [payload]
|
|
71
|
+
* @returns {Promise<string>}
|
|
72
|
+
*/
|
|
73
|
+
run(payload?: string): Promise<string>;
|
|
74
|
+
/**
|
|
75
|
+
* Works for screen takeovers like editors
|
|
76
|
+
* @param {string} [payload]
|
|
77
|
+
* @returns {SpawnSyncResponse}
|
|
78
|
+
*/
|
|
79
|
+
runSync(payload?: string): SpawnSyncResponse;
|
|
80
|
+
kill(): Promise<void>;
|
|
81
|
+
#private;
|
|
82
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
export default SHExecute;
|
|
3
|
+
declare class SHExecute {
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} command - linux command to be executed
|
|
6
|
+
* @param {import('./SHDispatch').SHOptions} [options] - ChildProcess options
|
|
7
|
+
*/
|
|
8
|
+
constructor(command: string, options?: any);
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} [payload] - data to write
|
|
11
|
+
* @retuns {Promise<object>}
|
|
12
|
+
*/
|
|
13
|
+
runSync(payload?: string): import("child_process").SpawnSyncReturns<Buffer>;
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} [payload] - data to write
|
|
16
|
+
* @retuns {Promise<string>}
|
|
17
|
+
*/
|
|
18
|
+
run(payload?: string): Promise<any>;
|
|
19
|
+
/**
|
|
20
|
+
* @returns {Promise<number[]>}
|
|
21
|
+
*/
|
|
22
|
+
kill(signal?: string): Promise<number[]>;
|
|
23
|
+
#private;
|
|
24
|
+
}
|
package/lib/sh.js
DELETED
|
@@ -1,538 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert';
|
|
2
|
-
import { spawnSync, spawn, exec } from 'node:child_process';
|
|
3
|
-
|
|
4
|
-
// timeout when a process becomes inresponsive
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Kills a process and all child processes of a given process ID in Linux/Posix.
|
|
8
|
-
* @param {number} processPid - The process ID.
|
|
9
|
-
* @param {string} signal - Signal to send.
|
|
10
|
-
* @retruns {Promise<number[]>} array with killed pid numbers
|
|
11
|
-
*/
|
|
12
|
-
const killProcesses = (processPid, signal) => {
|
|
13
|
-
const killed = [];
|
|
14
|
-
return new Promise((resolve, reject) => {
|
|
15
|
-
// Command to get child PIDs of the given process
|
|
16
|
-
const cmd = `pgrep -P ${processPid}`;
|
|
17
|
-
exec(cmd, (error, stdout, stderr) => {
|
|
18
|
-
if (error) {
|
|
19
|
-
reject(error);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
if (stderr) {
|
|
23
|
-
reject(new Error(stderr));
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
const pids = stdout.split(/\r?\n/).filter(pid => pid);
|
|
27
|
-
// Kill each child process
|
|
28
|
-
try {
|
|
29
|
-
for (const pid of pids) {
|
|
30
|
-
process.kill(parseInt(pid), signal);
|
|
31
|
-
killed.push(parseInt(pid));
|
|
32
|
-
}
|
|
33
|
-
} catch (err) {
|
|
34
|
-
reject(err);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
// Kill the parent process after all child processes have been killed
|
|
38
|
-
try {
|
|
39
|
-
process.kill(processPid, signal);
|
|
40
|
-
killed.push(processPid);
|
|
41
|
-
} catch (err) {
|
|
42
|
-
reject(err);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
resolve(killed);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
class SHExecute {
|
|
51
|
-
#proc;
|
|
52
|
-
#command = '';
|
|
53
|
-
#options = {};
|
|
54
|
-
#stdout = '';
|
|
55
|
-
#stderr = '';
|
|
56
|
-
constructor(command, options = {}) {
|
|
57
|
-
this.#command = command;
|
|
58
|
-
this.#options = options;
|
|
59
|
-
this.#proc = null;
|
|
60
|
-
this.#stdout = '';
|
|
61
|
-
this.#stderr = '';
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* @param {string} [payload] - data to write
|
|
65
|
-
* @retuns {Promise<object>}
|
|
66
|
-
*/
|
|
67
|
-
runSync(payload) {
|
|
68
|
-
if (payload && typeof payload !== 'string') {
|
|
69
|
-
throw new Error('Argument is not a string');
|
|
70
|
-
}
|
|
71
|
-
let { cwd, shell, env, stdio } = this.#options;
|
|
72
|
-
// pipe need to be set on stdin when posting a payload
|
|
73
|
-
if (payload) stdio[0] = 'pipe';
|
|
74
|
-
const input = payload || undefined;
|
|
75
|
-
return spawnSync(this.#options.prefix, [this.#command], {
|
|
76
|
-
cwd,
|
|
77
|
-
shell,
|
|
78
|
-
stdio,
|
|
79
|
-
windowsHide: true,
|
|
80
|
-
env,
|
|
81
|
-
input
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* @param {string} [payload] - data to write
|
|
86
|
-
* @retuns {Promise<string>}
|
|
87
|
-
*/
|
|
88
|
-
run(payload) {
|
|
89
|
-
let to = 0;
|
|
90
|
-
if (payload && typeof payload !== 'string') {
|
|
91
|
-
throw new Error('Argument is not a string');
|
|
92
|
-
}
|
|
93
|
-
if (this.#options.timeout) {
|
|
94
|
-
to = this.#options.timeout;
|
|
95
|
-
}
|
|
96
|
-
let { cwd, shell, env, stdio } = this.#options;
|
|
97
|
-
// pipe need to be set on stdin when posting a payload
|
|
98
|
-
if (payload) stdio[0] = 'pipe';
|
|
99
|
-
this.#proc = spawn(this.#options.prefix, [this.#command], {
|
|
100
|
-
cwd,
|
|
101
|
-
shell,
|
|
102
|
-
stdio,
|
|
103
|
-
windowsHide: true,
|
|
104
|
-
env,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
this.#proc.stdout?.on('data', (data) => {
|
|
108
|
-
this.#stdout += data;
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
this.#proc.stderr?.on('data', (data) => {
|
|
112
|
-
this.#stderr += data;
|
|
113
|
-
});
|
|
114
|
-
if (payload) {
|
|
115
|
-
this.#proc.stdin.end(payload);
|
|
116
|
-
}
|
|
117
|
-
return new Promise((resolve, reject) => {
|
|
118
|
-
let timeout;
|
|
119
|
-
if (to > 0) {
|
|
120
|
-
timeout = setTimeout(async () => {
|
|
121
|
-
await this.#proc.kill();
|
|
122
|
-
reject(new Error(`Process timed out: ${this.#command}`));
|
|
123
|
-
}, to); // options.timeout
|
|
124
|
-
}
|
|
125
|
-
this.#proc.on('close', (code) => {
|
|
126
|
-
if (timeout) clearTimeout(timeout);
|
|
127
|
-
if (code === 0) {
|
|
128
|
-
resolve(this.#stdout.trim());
|
|
129
|
-
} else {
|
|
130
|
-
reject(new Error(`${code}: ${this.#command} "${this.#stderr.trim()}"`));
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
this.#proc.on('error', (err) => {
|
|
135
|
-
reject(err);
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* @returns {Promise<number[]>}
|
|
141
|
-
*/
|
|
142
|
-
async kill(signal = 'SIGTERM') {
|
|
143
|
-
if (!this.#proc) throw new Error('Trying to kill a process without creating one.');
|
|
144
|
-
if (!this.#proc.pid) throw new Error('The process pid is undefined.');
|
|
145
|
-
|
|
146
|
-
return killProcesses(this.#proc.pid, signal);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* @typedef {Object} SpawnSyncResponse
|
|
152
|
-
* @property {number} status - The exit code of the child process. A value of `0` indicates success.
|
|
153
|
-
* @property {Buffer|null} signal - The signal used to terminate the process, if any.
|
|
154
|
-
* @property {Array<string|null>} output - An array containing the standard output and standard error of the child process.
|
|
155
|
-
* @property {number} pid - The process ID of the child process.
|
|
156
|
-
* @property {Buffer|null} stdout - The standard output of the child process.
|
|
157
|
-
* @property {Buffer|null} stderr - The standard error of the child process.
|
|
158
|
-
*/
|
|
159
|
-
/**
|
|
160
|
-
* Default options for the execution environment.
|
|
161
|
-
*
|
|
162
|
-
* @typedef {Object} SHOptions
|
|
163
|
-
* @property {string} [cwd] - The current working directory.
|
|
164
|
-
* @property {NodeJS.ProcessEnv} [env] - The environment variables.
|
|
165
|
-
* @property {string} [shell] - The shell to use for execution.
|
|
166
|
-
* @property {string} [prefix] - The prefix commands to ensure a safe execution environment.
|
|
167
|
-
* @property {StdioOptions|StdioOption} [stdio] - The stdio configuration.
|
|
168
|
-
* @property {number} [timeout] - default 20000: a timeout error is triggerd when a process execution time is exceeded, 0 is no timeout
|
|
169
|
-
*/
|
|
170
|
-
/**
|
|
171
|
-
* @typedef {('pipe' | 'ignore' | 'inherit' | number)} StdioOption
|
|
172
|
-
* @description Defines the stdio configuration for each of the standard streams.
|
|
173
|
-
*
|
|
174
|
-
* - 'pipe' creates a pipe between the child process and the parent process.
|
|
175
|
-
* The parent end of the pipe is exposed as a property on the `ChildProcess` object.
|
|
176
|
-
* - 'ignore' indicates that the child process's corresponding stdio file descriptor will be ignored.
|
|
177
|
-
* - 'inherit' passes the corresponding stdio stream to/from the child process.
|
|
178
|
-
* - Stream object to be used for the stdio stream.
|
|
179
|
-
* - Positive integer representing a file descriptor to be used for the stdio stream.
|
|
180
|
-
*/
|
|
181
|
-
/**
|
|
182
|
-
* @typedef {Array<StdioOption>|StdioOption} StdioOptions
|
|
183
|
-
* @description
|
|
184
|
-
* Configures the stdio streams for the child process. This can be an array or a single StdioOption.
|
|
185
|
-
*
|
|
186
|
-
* Array Form: Specify the configuration for [stdin, stdout, stderr].
|
|
187
|
-
* - If array length is more than 3, additional positions correspond to extra streams.
|
|
188
|
-
* Single Value: This value will be applied to stdin, stdout, and stderr.
|
|
189
|
-
*
|
|
190
|
-
* Examples:
|
|
191
|
-
* - ['pipe', 'pipe', 'ignore']: Pipe stdin and stdout, ignore stderr.
|
|
192
|
-
* - 'inherit': Inherit all stdio streams from the parent.
|
|
193
|
-
*/
|
|
194
|
-
/**
|
|
195
|
-
* 'Code Safe' has own prop
|
|
196
|
-
*
|
|
197
|
-
* @param {any} o - object to examine
|
|
198
|
-
* @param {string} p - property to look for
|
|
199
|
-
* @returns {boolean}
|
|
200
|
-
*/
|
|
201
|
-
const hasProp = (o, p) => {
|
|
202
|
-
if (typeof o === 'undefined') {
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
|
-
return Object.prototype.hasOwnProperty.call(o, p);
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Merge property values while maintaining the fixed set of props in the original object
|
|
210
|
-
* @param {SHOptions} predefined - original object
|
|
211
|
-
* @param {SHOptions} options - object with new values
|
|
212
|
-
* @returns {SHOptions}
|
|
213
|
-
*/
|
|
214
|
-
const mergeOptions = (predefined, options) => {
|
|
215
|
-
// Extract the keys from the predefined object
|
|
216
|
-
const keys = Object.keys(predefined);
|
|
217
|
-
|
|
218
|
-
// Use reduce to accumulate only the predefined properties from sourceObj
|
|
219
|
-
const mergedObj = keys.reduce((acc, key) => {
|
|
220
|
-
if (hasProp(options, key)) {
|
|
221
|
-
acc[key] = options[key];
|
|
222
|
-
} else {
|
|
223
|
-
acc[key] = predefined[key];
|
|
224
|
-
}
|
|
225
|
-
return acc;
|
|
226
|
-
}, {});
|
|
227
|
-
|
|
228
|
-
return mergedObj;
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
/** @type {SHOptions} */
|
|
233
|
-
const defaultOptions = {
|
|
234
|
-
cwd: process.cwd(),
|
|
235
|
-
env: process.env,
|
|
236
|
-
shell: 'bash',
|
|
237
|
-
prefix: 'set -euo pipefail;/usr/bin/env',
|
|
238
|
-
stdio: ['inherit', 'pipe', 'pipe'],
|
|
239
|
-
timeout: 10000 // when 0 there is no timeout
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
class SHDispatch {
|
|
245
|
-
#cmd = '';
|
|
246
|
-
#options = {};
|
|
247
|
-
#proc;
|
|
248
|
-
/**
|
|
249
|
-
* @param {string} cmd - cmd to execute
|
|
250
|
-
*/
|
|
251
|
-
constructor(cmd) {
|
|
252
|
-
this.#cmd = cmd;
|
|
253
|
-
this.#options = defaultOptions;
|
|
254
|
-
}
|
|
255
|
-
/**
|
|
256
|
-
* @param {SHOptions} options
|
|
257
|
-
* @returns {SHDispatch}
|
|
258
|
-
*/
|
|
259
|
-
options(options) {
|
|
260
|
-
if (options.stdio && typeof options.stdio === 'string') {
|
|
261
|
-
// convert stdio to array
|
|
262
|
-
// This sets the default io values
|
|
263
|
-
// but can be overwritten when having a payload
|
|
264
|
-
const io = options.stdio;
|
|
265
|
-
options.stdio = Array(3).fill(io);
|
|
266
|
-
}
|
|
267
|
-
this.#options = mergeOptions(defaultOptions, options);
|
|
268
|
-
return this;
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* @param {string} [payload]
|
|
272
|
-
* @returns {Promise<string>}
|
|
273
|
-
*/
|
|
274
|
-
run(payload) {
|
|
275
|
-
this.#proc = new SHExecute(this.#cmd, this.#options);
|
|
276
|
-
return this.#proc.run(payload);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Works for screen takeovers like editors
|
|
281
|
-
* @param {string} [payload]
|
|
282
|
-
* @returns {SpawnSyncResponse}
|
|
283
|
-
*/
|
|
284
|
-
runSync(payload) {
|
|
285
|
-
// @ts-ignore
|
|
286
|
-
return new SHExecute(this.#cmd, this.#options).runSync(payload);
|
|
287
|
-
}
|
|
288
|
-
async kill() {
|
|
289
|
-
try {
|
|
290
|
-
await this.#proc.kill();
|
|
291
|
-
} catch (_e) {}
|
|
292
|
-
this.#proc = undefined;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Copyright 2021 Google LLC
|
|
297
|
-
//
|
|
298
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
299
|
-
// you may not use this file except in compliance with the License.
|
|
300
|
-
// You may obtain a copy of the License at
|
|
301
|
-
//
|
|
302
|
-
// https://www.apache.org/licenses/LICENSE-2.0
|
|
303
|
-
//
|
|
304
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
305
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
306
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
307
|
-
// See the License for the specific language governing permissions and
|
|
308
|
-
// limitations under the License.
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
// Original Source: zx
|
|
312
|
-
// Link to Original Source: https://github.com/google/zx
|
|
313
|
-
// Reason for Using This Code:
|
|
314
|
-
// The core functionality of this code is highly beneficial. However, certain parts of the original code
|
|
315
|
-
// were overwriting the global namespace with core libraries and variables. This was causing conflicts with
|
|
316
|
-
// other packages (for instance, fetch) and introducing unexpected elements into my code base.
|
|
317
|
-
// The main $/SH method is all there is left, with barebone Promises and readable code.
|
|
318
|
-
// Changes Made:
|
|
319
|
-
// - The code has been or is being reformatted to comply with ES2020 standards.
|
|
320
|
-
// - Some methods were added and existing ones were modified or deleted to enhance usability.
|
|
321
|
-
// - Most methods were deleted,
|
|
322
|
-
// - The namespace has been changed from '$' to 'SH'.
|
|
323
|
-
// Modified by: jorrit.duin+sh[AT]gmail.com
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Creates a new SHDispatch object that represents a command to be executed.
|
|
328
|
-
*
|
|
329
|
-
* @typedef {Function} Shell
|
|
330
|
-
* @property {function(Array, ...*): ProcessPromise} execute - The function to execute the command.
|
|
331
|
-
*
|
|
332
|
-
* @param {Array} pieces - An array of string literals from a template literal.
|
|
333
|
-
* @param {...*} args - The values to be interpolated into the string literals.
|
|
334
|
-
* @returns {SHDispatch} Trigger for the command.
|
|
335
|
-
* @throws {Error} Throws an error if any of the string literals in `pieces` is undefined.
|
|
336
|
-
*
|
|
337
|
-
* @example
|
|
338
|
-
* const command = await SH`echo 'Hello, world!'`.run();
|
|
339
|
-
*/
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* escape paramater commands
|
|
343
|
-
* @param {string} arg
|
|
344
|
-
* @returns {string}
|
|
345
|
-
*/
|
|
346
|
-
const quote = (arg) => {
|
|
347
|
-
if (/^[a-z0-9/_.\-@:=]+$/i.test(arg) || arg === '') {
|
|
348
|
-
return arg;
|
|
349
|
-
}
|
|
350
|
-
return (`'` +
|
|
351
|
-
arg
|
|
352
|
-
.replace(/\\/g, '\\\\')
|
|
353
|
-
.replace(/'/g, "\\'")
|
|
354
|
-
.replace(/\f/g, '\\f')
|
|
355
|
-
.replace(/\n/g, '\\n')
|
|
356
|
-
.replace(/\r/g, '\\r')
|
|
357
|
-
.replace(/\t/g, '\\t')
|
|
358
|
-
.replace(/\v/g, '\\v')
|
|
359
|
-
.replace(/\0/g, '\\0') +
|
|
360
|
-
`'`);
|
|
361
|
-
};
|
|
362
|
-
/**
|
|
363
|
-
* Escape CLI arguments
|
|
364
|
-
* @param {string[]} arg
|
|
365
|
-
* @retruns {string}
|
|
366
|
-
*/
|
|
367
|
-
const sanitizeArg = (arg) => {
|
|
368
|
-
const s = `${arg}`;
|
|
369
|
-
return quote(s);
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* 4ms, 5s || 5
|
|
374
|
-
* @param {number|string} d
|
|
375
|
-
* @returns {number}
|
|
376
|
-
*/
|
|
377
|
-
const parseDuration = (d) => {
|
|
378
|
-
if (typeof d == 'number') {
|
|
379
|
-
if (isNaN(d) || d < 0)
|
|
380
|
-
throw new Error(`Invalid duration: "${d}".`);
|
|
381
|
-
return d;
|
|
382
|
-
}
|
|
383
|
-
else if (/\d+s/.test(d)) {
|
|
384
|
-
return +d.slice(0, -1) * 1000;
|
|
385
|
-
}
|
|
386
|
-
else if (/\d+ms/.test(d)) {
|
|
387
|
-
return +d.slice(0, -2);
|
|
388
|
-
}
|
|
389
|
-
throw new Error(`Unknown duration: "${d}".`);
|
|
390
|
-
};
|
|
391
|
-
/** @type {Shell & { (pieces: TemplateStringsArray, ...args: *): SHDispatch }} */
|
|
392
|
-
const SH = new Proxy(function(pieces, ...args) {
|
|
393
|
-
if (pieces.some((p) => p == undefined)) {
|
|
394
|
-
throw new Error(`Malformed command ${pieces}`);
|
|
395
|
-
}
|
|
396
|
-
let cmd = pieces[0], i = 0;
|
|
397
|
-
while (i < args.length) {
|
|
398
|
-
let s;
|
|
399
|
-
if (Array.isArray(args[i])) {
|
|
400
|
-
s = args[i].map((x) => sanitizeArg(x)).join(' ');
|
|
401
|
-
}
|
|
402
|
-
else {
|
|
403
|
-
s = sanitizeArg(args[i]);
|
|
404
|
-
}
|
|
405
|
-
cmd += s + pieces[++i];
|
|
406
|
-
}
|
|
407
|
-
return new SHDispatch(cmd);
|
|
408
|
-
}, {});
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Create a async context in an sync block
|
|
412
|
-
* @param {function} callback - async function
|
|
413
|
-
* @example
|
|
414
|
-
* const p = within(async () => {
|
|
415
|
-
* const res = await Promise.all([
|
|
416
|
-
* SH`sleep 1; echo 1`.run(),
|
|
417
|
-
* SH`sleep 2; echo 2`.run(),
|
|
418
|
-
* sleep(2),
|
|
419
|
-
* SH`sleep 3; echo 3`.run()
|
|
420
|
-
* ]);
|
|
421
|
-
*/
|
|
422
|
-
const within = (callback) => {
|
|
423
|
-
(async () => {
|
|
424
|
-
return await callback()
|
|
425
|
-
})();
|
|
426
|
-
};
|
|
427
|
-
/**
|
|
428
|
-
* This function reads the standard input (stdin) from the current process.
|
|
429
|
-
* @example
|
|
430
|
-
* const content = await stdin();
|
|
431
|
-
*/
|
|
432
|
-
const readIn = async () => {
|
|
433
|
-
let buf = '';
|
|
434
|
-
process.stdin.setEncoding('utf8');
|
|
435
|
-
for await (const chunk of process.stdin) {
|
|
436
|
-
buf += chunk;
|
|
437
|
-
}
|
|
438
|
-
return buf;
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Retries a given asynchronous function a specified number of times with optional delays between attempts.
|
|
443
|
-
*
|
|
444
|
-
* @param {number} count - The number of retry attempts.
|
|
445
|
-
* @param {string|expBackoff|Function} a - Either a delay duration as a string, a delay generator object, or the callback function.
|
|
446
|
-
* @param {Function} [b] - The callback function to retry, required if `a` is not a function.
|
|
447
|
-
* @returns {Promise<*>} - The result of the callback function if it succeeds within the retry attempts.
|
|
448
|
-
* @throws {Error} - The last error encountered if all retry attempts fail.
|
|
449
|
-
*
|
|
450
|
-
* @example
|
|
451
|
-
* // Retry a command 3 times
|
|
452
|
-
* const p = await retry(3, () => SH`curl -s https://flipwrsi`);
|
|
453
|
-
*
|
|
454
|
-
* // Retry a command 3 times with an interval of 1 second between each try
|
|
455
|
-
* const p = await retry(3, '1s', () => SH`curl -s https://flipwrsi`);
|
|
456
|
-
*
|
|
457
|
-
* // Retry a command 3 times with irregular intervals using exponential backoff
|
|
458
|
-
* const p = await retry(3, expBackoff(), () => SH`curl -s https://flipwrsi`);
|
|
459
|
-
*/
|
|
460
|
-
const retry = async (count, a, b) => {
|
|
461
|
-
// const total = count;
|
|
462
|
-
let callback;
|
|
463
|
-
let delayStatic = 0;
|
|
464
|
-
let delayGen;
|
|
465
|
-
// @ts-ignore
|
|
466
|
-
if (typeof a == 'function') {
|
|
467
|
-
callback = a;
|
|
468
|
-
}
|
|
469
|
-
else {
|
|
470
|
-
if (typeof a == 'object') {
|
|
471
|
-
delayGen = a;
|
|
472
|
-
}
|
|
473
|
-
else {
|
|
474
|
-
delayStatic = parseDuration(a);
|
|
475
|
-
}
|
|
476
|
-
assert(b);
|
|
477
|
-
callback = b;
|
|
478
|
-
}
|
|
479
|
-
let lastErr;
|
|
480
|
-
while (count-- > 0) {
|
|
481
|
-
try {
|
|
482
|
-
return await callback();
|
|
483
|
-
}
|
|
484
|
-
catch (err) {
|
|
485
|
-
let delay = 0;
|
|
486
|
-
if (delayStatic > 0)
|
|
487
|
-
delay = delayStatic;
|
|
488
|
-
// @ts-ignore
|
|
489
|
-
if (delayGen) delay = delayGen.next().value;
|
|
490
|
-
lastErr = err;
|
|
491
|
-
if (count == 0)
|
|
492
|
-
break;
|
|
493
|
-
if (delay)
|
|
494
|
-
await sleep(delay);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
throw lastErr;
|
|
498
|
-
};
|
|
499
|
-
/**
|
|
500
|
-
* This function pauses or "sleeps" code execution for a specified duration.
|
|
501
|
-
* @param {string|number} duration - The duration to pause execution for, e.g., '100ms' or '3s'.
|
|
502
|
-
*
|
|
503
|
-
* @example
|
|
504
|
-
*
|
|
505
|
-
* const res = await sleep('5s');
|
|
506
|
-
*/
|
|
507
|
-
const sleep = (duration) => {
|
|
508
|
-
return new Promise((resolve) => {
|
|
509
|
-
setTimeout(resolve, parseDuration(duration));
|
|
510
|
-
});
|
|
511
|
-
};
|
|
512
|
-
/**
|
|
513
|
-
* Change working directory
|
|
514
|
-
* @param {string} dir
|
|
515
|
-
*/
|
|
516
|
-
const cd = (dir) => {
|
|
517
|
-
// @ts-ignore
|
|
518
|
-
process.chdir(dir);
|
|
519
|
-
};
|
|
520
|
-
/**
|
|
521
|
-
* Generates an exponential backoff time with a random jitter.
|
|
522
|
-
*
|
|
523
|
-
* @generator
|
|
524
|
-
* @param {string} [max='60s'] - The maximum backoff time in a human-readable format (e.g., '60s' for 60 seconds).
|
|
525
|
-
* @param {string} [rand='100ms'] - The maximum random jitter time in a human-readable format (e.g., '100ms' for 100 milliseconds).
|
|
526
|
-
* @yields {number} The backoff time in milliseconds.
|
|
527
|
-
*/
|
|
528
|
-
function* expBackoff(max = '60s', rand = '100ms') {
|
|
529
|
-
const maxMs = parseDuration(max);
|
|
530
|
-
const randMs = parseDuration(rand);
|
|
531
|
-
let n = 1;
|
|
532
|
-
while (true) {
|
|
533
|
-
const ms = Math.floor(Math.random() * randMs);
|
|
534
|
-
yield Math.min(2 ** n++, maxMs) + ms;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
export { SH, cd, expBackoff, readIn, retry, sleep, within };
|