@j-o-r/sh 1.0.2 → 1.0.4

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 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 };