@j-o-r/sh 0.0.1

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.
@@ -0,0 +1,323 @@
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
+ // Changes Made:
23
+ // - 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.
25
+ // - The namespace has been changed from '$' to 'SH'.
26
+ // Modified by: jorrit.duin+sh[AT]gmail.com
27
+
28
+ import { spawn } from 'node:child_process';
29
+ import assert from 'node:assert';
30
+ import { log, errnoMessage, exitCodeInfo, noop, parseDuration, psTree } from './utils.js';
31
+ import ProcessOutput from './ProcessOutput.js';
32
+ /**
33
+ * @typedef {Function} resolver
34
+ * @param {ProcessOutput} value
35
+ */
36
+ /**
37
+ * @typedef {Function} rejecter
38
+ * @param {ProcessOutput} value
39
+ */
40
+ /**
41
+ * @typedef {Function} PromiseConstruct
42
+ * @param {resolver} resolve
43
+ * @param {rejecter} reject
44
+ */
45
+
46
+ class ProcessPromise extends Promise {
47
+ #command = '';
48
+ #from = '';
49
+ /** @type {resolver} */
50
+ #resolve = () => { };
51
+ /** @type {rejecter} */
52
+ #reject = () => { };
53
+ #snapshot = {};
54
+ #stdio = ['inherit', 'pipe', 'pipe'];
55
+ #nothrow = false;
56
+ #quiet = false;
57
+ #resolved = false;
58
+ #halted = false;
59
+ #piped = false;
60
+ #prerun = noop;
61
+ #postrun = noop;
62
+ /**
63
+ * @param {PromiseConstruct} p - A function that takes two arguments, resolve and reject.
64
+ */
65
+ constructor(p) {
66
+ super(p)
67
+ }
68
+ /**
69
+ * Set the environment
70
+ * and the
71
+ * @param {string} cmd - Command to execute
72
+ * @param {string} from - Position in the codfe where this is triggred from
73
+ * @param {function} resolve - Promise resolve method
74
+ * @param {function} reject - Reject method
75
+ * @param {object} options - Settings (options default)
76
+ */
77
+ _bind(cmd, from, resolve, reject, options) {
78
+ this.#command = cmd;
79
+ this.#from = from;
80
+ this.#resolve = resolve;
81
+ this.#reject = reject;
82
+ this.#snapshot = { ...options };
83
+ }
84
+ /**
85
+ * Run the promise
86
+ */
87
+ run() {
88
+ const ENV = this.#snapshot;
89
+ if (this.child) return this; // The _run() can be called from a few places.
90
+ this.#prerun(); // In case $1.pipe($2), the $2 returned, and on $2._run() invoke $1._run().
91
+ log({
92
+ kind: 'cmd',
93
+ cmd: this.#command,
94
+ verbose: ENV.verbose && !this.#quiet,
95
+ });
96
+ const cwd = ENV['processCwd'];
97
+ this.child = spawn(ENV.prefix + this.#command, {
98
+ cwd,
99
+ // cwd: $.cwd ?? $[processCwd],
100
+ shell: typeof ENV.shell === 'string' ? ENV.shell : true,
101
+ stdio: this.#stdio,
102
+ windowsHide: true,
103
+ env: ENV.env,
104
+ });
105
+ this.child.on('close', (code, signal) => {
106
+ let message = `exit code: ${code}`;
107
+ if (code != 0 || signal != null) {
108
+ message = `${stderr || '\n'} at ${this.#from}`;
109
+ message += `\n exit code: ${code}${exitCodeInfo(code) ? ' (' + exitCodeInfo(code) + ')' : ''}`;
110
+ if (signal != null) {
111
+ message += `\n signal: ${signal}`;
112
+ }
113
+ }
114
+ let output = new ProcessOutput(code, signal, stdout, stderr, combined, message);
115
+ if (code === 0 || this.#nothrow) {
116
+ this.#resolve(output);
117
+ }
118
+ else {
119
+ this.#reject(output);
120
+ }
121
+ this.#resolved = true;
122
+ });
123
+ this.child.on('error', (err) => {
124
+ const message = `${err.message}\n` +
125
+ ` errno: ${err.errno} (${errnoMessage(err.errno)})\n` +
126
+ ` code: ${err.code}\n` +
127
+ ` at ${this.#from}`;
128
+ this.#reject(new ProcessOutput(null, null, stdout, stderr, combined, message));
129
+ this.#resolved = true;
130
+ });
131
+ let stdout = '', stderr = '', combined = '';
132
+ /** @param {Blob} data */
133
+ const onStdout = (data) => {
134
+ log({ kind: 'stdout', data, verbose: ENV.verbose && !this.#quiet });
135
+ stdout += data;
136
+ combined += data;
137
+ };
138
+ /** @param {Blob} data */
139
+ const onStderr = (data) => {
140
+ log({ kind: 'stderr', data, verbose: ENV.verbose && !this.#quiet });
141
+ stderr += data;
142
+ combined += data;
143
+ };
144
+ if (!this.#piped)
145
+ this.child.stdout?.on('data', onStdout); // If process is piped, don't collect or print output.
146
+ this.child.stderr?.on('data', onStderr); // Stderr should be printed regardless of piping.
147
+ this.#postrun(); // In case $1.pipe($2), after both subprocesses are running, we can pipe $1.stdout to $2.stdin.
148
+ if (this._timeout && this._timeoutSignal) {
149
+ const t = setTimeout(() => this.kill(this._timeoutSignal), this._timeout);
150
+ this.finally(() => clearTimeout(t)).catch(noop);
151
+ }
152
+ return this;
153
+ }
154
+ /**
155
+ * stdin child stream
156
+ * @retruns {Writeable}
157
+ */
158
+ get stdin() {
159
+ this.stdio('pipe');
160
+ this.run();
161
+ assert(this.child);
162
+ if (this.child.stdin == null)
163
+ throw new Error('The stdin of subprocess is null.');
164
+ return this.child.stdin;
165
+ }
166
+ /**
167
+ * stdout child stream
168
+ * @retruns {Readable}
169
+ */
170
+ get stdout() {
171
+ this.run();
172
+ assert(this.child);
173
+ if (this.child.stdout == null)
174
+ throw new Error('The stdout of subprocess is null.');
175
+ return this.child.stdout;
176
+ }
177
+ /**
178
+ * stderr child stream
179
+ * @retruns {Readable}
180
+ */
181
+ get stderr() {
182
+ this.run();
183
+ assert(this.child);
184
+ if (this.child.stderr == null)
185
+ throw new Error('The stderr of subprocess is null.');
186
+ return this.child.stderr;
187
+ }
188
+ /**
189
+ * process exit code
190
+ * @returns {Promise<number>}
191
+ */
192
+ get exitCode() {
193
+ return this.then((p) => p.exitCode, (p) => p.exitCode);
194
+ }
195
+ then(onfulfilled, onrejected) {
196
+ if (this.isHalted && !this.child) {
197
+ throw new Error('The process is halted!');
198
+ }
199
+ return super.then(onfulfilled, onrejected);
200
+ }
201
+ catch(onrejected) {
202
+ return super.catch(onrejected);
203
+ }
204
+ /**
205
+ * Pipe the output to the input to the next Promise
206
+ * @example
207
+ * const res = await SH`ls -FLa`.pipe(SH`grep package.json`);
208
+ */
209
+ pipe(dest) {
210
+ if (typeof dest == 'string')
211
+ throw new Error('The pipe() method does not take strings. Forgot SH?');
212
+ if (this.#resolved) {
213
+ if (dest instanceof ProcessPromise)
214
+ dest.stdin.end(); // In case of piped stdin, we may want to close stdin of dest as well.
215
+ throw new Error("The pipe() method shouldn't be called after promise is already resolved!");
216
+ }
217
+ this.#piped = true;
218
+ if (dest instanceof ProcessPromise) {
219
+ dest.stdio('pipe');
220
+ dest._prerun = this.run.bind(this);
221
+ dest._postrun = () => {
222
+ if (!dest.child)
223
+ throw new Error('Access to stdin of pipe destination without creation a subprocess.');
224
+ this.stdout.pipe(dest.stdin);
225
+ };
226
+ return dest;
227
+ }
228
+ else {
229
+ this._postrun = () => this.stdout.pipe(dest);
230
+ return this;
231
+ }
232
+ }
233
+ /**
234
+ * Send a KILL signal to the child process
235
+ */
236
+ async kill(signal = 'SIGTERM') {
237
+ if (!this.child)
238
+ throw new Error('Trying to kill a process without creating one.');
239
+ if (!this.child.pid)
240
+ throw new Error('The process pid is undefined.');
241
+ let children = await psTree(this.child.pid);
242
+ for (const p of children) {
243
+ try {
244
+ process.kill(+p.PID, signal);
245
+ }
246
+ catch (e) { }
247
+ }
248
+ try {
249
+ process.kill(this.child.pid, signal);
250
+ }
251
+ catch (e) { }
252
+ }
253
+ stdio(stdin, stdout = 'pipe', stderr = 'pipe') {
254
+ this.#stdio = [stdin, stdout, stderr];
255
+ return this;
256
+ }
257
+ /**
258
+ * Do not throw
259
+ */
260
+ nothrow() {
261
+ this.#nothrow = true;
262
+ return this;
263
+ }
264
+ /**
265
+ * supress log output
266
+ * SH.verbose = false; does the same
267
+ */
268
+ quiet() {
269
+ this.#quiet = true;
270
+ return this;
271
+ }
272
+ /**
273
+ * Show log output in the console
274
+ */
275
+ verbose() {
276
+ this._quiet = false;
277
+ return this;
278
+ }
279
+ /**
280
+ * Set a timeout to kill a process
281
+ *
282
+ * @param {string} d - 10s, 1000ms
283
+ * @param {string} [signal] - default "SIGTERM" Signal to send to kill the proces
284
+ */
285
+ timeout(d, signal = 'SIGTERM') {
286
+ this._timeout = parseDuration(d);
287
+ this._timeoutSignal = signal;
288
+ return this;
289
+ }
290
+ /**
291
+ * stop execution for the next step
292
+ */
293
+ halt() {
294
+ this.#halted = true;
295
+ return this;
296
+ }
297
+ /**
298
+ * @private
299
+ * Set a prerun action, internal use only
300
+ * @param {function} f
301
+ */
302
+ set _prerun(f) {
303
+ // @ts-ignore
304
+ this.#prerun = f;
305
+ }
306
+ /**
307
+ * @private
308
+ * Set a postrun action, internal use only
309
+ * @param {function} f
310
+ */
311
+ set _postrun(f) {
312
+ // @ts-ignore
313
+ this.#postrun = f;
314
+ }
315
+ /**
316
+ * Is this promise halted?
317
+ * @returns {boolean}
318
+ */
319
+ get isHalted() {
320
+ return this.#halted;
321
+ }
322
+ }
323
+ export default ProcessPromise;
package/src/sh.js ADDED
@@ -0,0 +1,261 @@
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
+ // Changes Made:
23
+ // - 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.
25
+ // - The namespace has been changed from '$' to 'SH'.
26
+ // Modified by: jorrit.duin+sh[AT]gmail.com
27
+
28
+ import assert from 'node:assert';
29
+ import { AsyncLocalStorage } from 'node:async_hooks';
30
+ import which from 'which';
31
+ import { log, parseDuration, quote, quotePowerShell, } from './utils.js';
32
+ import ProcessPromise from './ProcessPromise.js';
33
+ // const processCwd = Symbol('processCwd');
34
+ const storage = new AsyncLocalStorage();
35
+
36
+ const defaults = {
37
+ processCwd: '',
38
+ verbose: false,
39
+ env: {},
40
+ shell: '',
41
+ prefix: '',
42
+ };
43
+ if (process.platform == 'win32') {
44
+ defaults.shell = which.sync('powershell.exe');
45
+ } else {
46
+ defaults.shell = which.sync('bash');
47
+ defaults.prefix = 'set -euo pipefail;';
48
+ }
49
+ /**
50
+ * Escape CLI arguments
51
+ * @retruns {string}
52
+ */
53
+ const sanitizeArg = (arg) => {
54
+ const s = `${arg}`;
55
+ if (process.platform == 'win32') {
56
+ return quotePowerShell(s)
57
+ }
58
+ return quote(s);
59
+ }
60
+
61
+ const getStore = () => {
62
+ return storage.getStore() || defaults;
63
+ }
64
+
65
+ /**
66
+ * Creates a new ProcessPromise object that represents a command to be executed.
67
+ *
68
+ * @typedef {Function} Shell
69
+ * @type {function}
70
+ * @param {Array} pieces - An array of string literals from a template literal.
71
+ * @param {...*} args - The values to be interpolated into the string literals.
72
+ * @returns {ProcessPromise} A ProcessPromise object that represents the command.
73
+ * @throws {Error} Throws an error if any of the string literals in `pieces` is undefined.
74
+ *
75
+ * @property {boolean} verbose
76
+ *
77
+ * @example
78
+ * const command = await SH`echo 'Hello, world!'`;
79
+ */
80
+ /** @type {Shell} */
81
+ const SH = new Proxy(function(pieces, ...args) {
82
+ const from = new Error().stack.split(/^\s*at\s/m)[2].trim();
83
+ if (pieces.some((p) => p == undefined)) {
84
+ throw new Error(`Malformed command at ${from}`);
85
+ }
86
+ let cmd = pieces[0], i = 0;
87
+ while (i < args.length) {
88
+ let s;
89
+ if (Array.isArray(args[i])) {
90
+ s = args[i].map((x) => sanitizeArg(x)).join(' ');
91
+ }
92
+ else {
93
+ s = sanitizeArg(args[i]);
94
+ }
95
+ cmd += s + pieces[++i];
96
+ }
97
+ let resolve, reject;
98
+
99
+ const promise = new ProcessPromise((...args) => ([resolve, reject] = args));
100
+ // re-add the environment
101
+ defaults.processCwd = process.cwd();
102
+ defaults.env = process.env,
103
+ promise._bind(cmd, from, resolve, reject, getStore());
104
+ // Postpone run to allow promise configuration.
105
+ setImmediate(() => promise.isHalted || promise.run());
106
+ return promise;
107
+ }, {
108
+ // this will get and set from:
109
+ // defaults OR storage (@see within());
110
+ set(_, key, value) {
111
+ const target = key in Function.prototype ? _ : getStore();
112
+ Reflect.set(target, key, value);
113
+ return true;
114
+ },
115
+ get(_, key) {
116
+ const target = key in Function.prototype ? _ : getStore();
117
+ return Reflect.get(target, key);
118
+ },
119
+ });
120
+
121
+ /**
122
+ * Create a async context in an sync block
123
+ * @param {function} callback - async function
124
+ * @example
125
+ * const p = within(async () => {
126
+ * const res = await Promise.all([
127
+ * SH`sleep 1; echo 1`,
128
+ * SH`sleep 2; echo 2`,
129
+ * sleep(2),
130
+ * SH`sleep 3; echo 3`
131
+ * ]);
132
+ */
133
+ const within = (callback) => {
134
+ // @ts-ignore
135
+ return storage.run({ ...getStore() }, callback);
136
+ }
137
+ /**
138
+ * This function reads the standard input (stdin) for the current process.
139
+ * It is used to get piped content into a script.
140
+ * @example
141
+ * const content = await stdin();
142
+ */
143
+ const stdin = async () => {
144
+ let buf = '';
145
+ process.stdin.setEncoding('utf8');
146
+ for await (const chunk of process.stdin) {
147
+ buf += chunk;
148
+ }
149
+ return buf;
150
+ }
151
+ /**
152
+ * This function retries a command a specified number of times.
153
+ * @example
154
+ * // Retry a command 3 times
155
+ * const p = await retry(3, () => SH`curl -s https://flipwrsi`);
156
+ *
157
+ * // Retry a command 3 times with an interval of 1 second between each try
158
+ * const p = await retry(3, '1s', () => SH`curl -s https://flipwrsi`);
159
+ *
160
+ * // Retry a command 3 times with irregular intervals using exponential backoff
161
+ * const p = await retry(3, expBackoff(), () => SH`curl -s https://flipwrsi`);
162
+ */
163
+ const retry = async (count, a, b) => {
164
+ const total = count;
165
+ let callback;
166
+ let delayStatic = 0;
167
+ let delayGen;
168
+ // @ts-ignore
169
+ const verbose = SH.verbose;
170
+ if (typeof a == 'function') {
171
+ callback = a;
172
+ }
173
+ else {
174
+ if (typeof a == 'object') {
175
+ delayGen = a;
176
+ }
177
+ else {
178
+ delayStatic = parseDuration(a);
179
+ }
180
+ // console.log(assert(b));
181
+ assert(b);
182
+ callback = b;
183
+ }
184
+ let lastErr;
185
+ let attempt = 0;
186
+ while (count-- > 0) {
187
+ attempt++;
188
+ try {
189
+ return await callback();
190
+ }
191
+ catch (err) {
192
+ let delay = 0;
193
+ if (delayStatic > 0)
194
+ delay = delayStatic;
195
+ if (delayGen) delay = delayGen.next().value;
196
+ log({
197
+ verbose,
198
+ kind: 'retry',
199
+ error: ' FAIL ' +
200
+ ` Attempt: ${attempt}${total == Infinity ? '' : `/${total}`}` +
201
+ (delay > 0 ? `; next in ${delay}ms` : ''),
202
+ });
203
+ lastErr = err;
204
+ if (count == 0)
205
+ break;
206
+ if (delay)
207
+ await sleep(delay);
208
+ }
209
+ }
210
+ throw lastErr;
211
+ }
212
+ /**
213
+ * This function pauses or "sleeps" code execution for a specified duration.
214
+ * @param {string} duration - The duration to pause execution for, e.g., '100ms' or '3s'.
215
+ *
216
+ * @example
217
+ *
218
+ * const res = await Promise.all([
219
+ * SH`sleep 2; echo 2`, // Sleep for 2 seconds
220
+ * sleep(2), // Sleep for 2 seconds
221
+ * SH`sleep 3; echo 3` // Sleep for 3 seconds
222
+ * ]);
223
+ */
224
+ const sleep = (duration) => {
225
+ return new Promise((resolve) => {
226
+ setTimeout(resolve, parseDuration(duration));
227
+ });
228
+ }
229
+ /**
230
+ * Change working directory
231
+ * @param {string} dir
232
+ */
233
+ const cd = (dir) => {
234
+ // @ts-ignore
235
+ const verbose = SH.verbose;
236
+ log({ kind: 'cd', dir, verbose });
237
+ process.chdir(dir);
238
+ }
239
+ function* expBackoff(max = '60s', rand = '100ms') {
240
+ const maxMs = parseDuration(max);
241
+ const randMs = parseDuration(rand);
242
+ let n = 1;
243
+ while (true) {
244
+ const ms = Math.floor(Math.random() * randMs);
245
+ yield Math.min(2 ** n++, maxMs) + ms;
246
+ }
247
+ }
248
+ // const syncCwd = () => {
249
+ // if (SH['processCwd'] != process.cwd())
250
+ // process.chdir(SH['processCwd']);
251
+ // }
252
+ export {
253
+ /** @type {Shell} */
254
+ SH,
255
+ cd,
256
+ sleep,
257
+ retry,
258
+ stdin,
259
+ within,
260
+ expBackoff,
261
+ }