@j-o-r/sh 1.1.21 → 1.1.23

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 CHANGED
@@ -81,10 +81,8 @@ const jsType = (fn) => {
81
81
  * @returns {boolean}
82
82
  */
83
83
  const hasProp = (o, p) => {
84
- if (typeof o === 'undefined') {
85
- return false;
86
- }
87
- return Object.prototype.hasOwnProperty.call(o, p);
84
+ if (o == null) return false;
85
+ return Object.prototype.hasOwnProperty.call(o, p);
88
86
  };
89
87
  /**
90
88
  * 4ms, 5s || 5
@@ -112,6 +110,15 @@ const parseDuration = (d) => {
112
110
  /**
113
111
  * @type {Shell & SHTag}
114
112
  */
113
+ /**
114
+ * SH template tag
115
+ *
116
+ * Interpolation rules:
117
+ * - Arrays: each element is String(x).trim(), with newlines/carriage returns/tabs escaped; if an element contains shell metacharacters or spaces, it is wrapped in single-quotes and internal single-quotes are escaped as '\''.
118
+ * - Non-array values: currently coerced with String(value) and inserted as-is (NOT shell-escaped). Be careful when interpolating untrusted input.
119
+ *
120
+ * Returns an SHDispatch that can be configured via .options() and executed with .run() / .runSync().
121
+ */
115
122
  const SH = new Proxy(function(pieces, ...args) {
116
123
  if (pieces.some((p) => p == undefined)) {
117
124
  throw new Error(`Malformed command ${pieces}`);
@@ -165,6 +172,10 @@ const within = async (callback) => {
165
172
  * @example
166
173
  * const content = await readIn();
167
174
  */
175
+ /**
176
+ * Read entire stdin as UTF-8. If stdin is a TTY, resolves to an empty string.
177
+ * @returns {Promise<string>}
178
+ */
168
179
  const readIn = async () => {
169
180
  if (process.stdin.isTTY) return ''; // nothing was piped
170
181
  let buf = '';
@@ -345,6 +356,21 @@ function* expBackoff(max = '60s', rand = '100ms') {
345
356
  * - The value is either the next argument or `true` if no value is provided,
346
357
  * - The `_` property contains an array of unbound arguments.
347
358
  */
359
+ /**
360
+ * Parse command-line args into an object.
361
+ *
362
+ * Supported:
363
+ * - --key value, -k value (no grouped short flags)
364
+ * - Bare values collected under `_.`
365
+ * - Duplicate keys throw an error; keys without a following value become true.
366
+ *
367
+ * Not supported:
368
+ * - --key=value syntax
369
+ * - Grouped short flags like -abc
370
+ *
371
+ * @param {string[]} [args]
372
+ * @returns {ArgsObject}
373
+ */
348
374
  const parseArgs = (args) => {
349
375
  if (!args) args = process.argv.slice(2);
350
376
  const result = { _: [] };
package/lib/SHDispatch.js CHANGED
@@ -1,25 +1,25 @@
1
1
  import SHExec from './SHExecute.js';
2
2
  /**
3
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.
4
+ * @property {number|null} status - Exit code of the child process (null if terminated by signal).
5
+ * @property {string|null} signal - Name of the terminating signal, if any.
6
+ * @property {(string|Buffer|null)[]} output - [stdin, stdout, stderr] per Node's SpawnSyncReturns.
7
+ * @property {number} pid - PID of the spawned process.
8
+ * @property {string|Buffer|null} stdout - Stdout collected (type depends on encoding option).
9
+ * @property {string|Buffer|null} stderr - Stderr collected (type depends on encoding option).
10
10
  */
11
- /**
11
+ /**
12
12
  * @typedef {Object} SHOptions
13
13
  * @property {string} [cwd] - Current working directory of the child process.
14
14
  * @property {Object} [env] - Environment key-value pairs.
15
- * @property {Array|string} [argv0] - Explicitly set the value of `argv[0]` sent to the child process.
15
+ * @property {string} [argv0] - Explicitly set the value of `argv[0]` sent to the child process.
16
16
  * @property {boolean} [detached=false] - If true, the child will be a process group leader.
17
17
  * @property {number} [uid] - Sets the user identity of the process.
18
18
  * @property {number} [gid] - Sets the group identity of the process.
19
19
  * @property {StdioOptions|StdioOption} [stdio='pipe'] - Child's stdio configuration.
20
- * @property {boolean|string} [shell="bash"] - If true, runs command inside a shell.
21
- * @property {number} [timeout=0] - In milliseconds, specifies when to terminate the child process.
22
- * @property {string|Buffer|URL} [input] - The input to write to stdin.
20
+ * @property {boolean|string} [shell='bash'] - If string or true, runs the command via that shell ('bash' if true).
21
+ * @property {number} [timeout=0] - Milliseconds before sending SIGTERM (0 = no timeout).
22
+ * @property {string|Buffer|Uint8Array} [input] - Optional stdin payload for spawnSync. Use .run(payload) for async.
23
23
  */
24
24
  /**
25
25
  * @typedef {('pipe' | 'ignore' | 'inherit' | number)} StdioOption
@@ -64,6 +64,21 @@ const mergeOptions = (predefined, options) => {
64
64
  }
65
65
 
66
66
  /** @type {SHOptions} */
67
+ /**
68
+ * SHOptions — effective defaults and semantics used by SHDispatch/SHExecute
69
+ *
70
+ * Defaults:
71
+ * - cwd: process.cwd()
72
+ * - env: process.env
73
+ * - shell: 'bash' (string). If a string, that shell is used. If true, 'bash' is used. If false/undefined, no shell is used.
74
+ * - stdio: ['inherit', 'pipe', 'pipe'] — inherit stdin, capture stdout/stderr
75
+ * - timeout: 0 — no timeout
76
+ * - maxBuffer?: number — optional (bytes per stream). Passed through to SHExecute.
77
+ *
78
+ * Notes:
79
+ * - The prefix (see options(prefix)) is only applied when a shell is used; it is ignored in no-shell mode.
80
+ * - Each call to options() resets to the defaults and merges the provided options; it does not accumulate from prior calls.
81
+ */
67
82
  const defaultOptions = {
68
83
  cwd: process.cwd(),
69
84
  env: process.env,
@@ -76,7 +91,7 @@ const defaultOptions = {
76
91
 
77
92
  class SHDispatch {
78
93
  // #prefix = 'set -euo pipefail;/usr/bin/env'
79
- #prefix = 'set -euo pipefail;/usr/bin/env -S'
94
+ #prefix = 'set -euo pipefail'
80
95
  #cmd = '';
81
96
  #options = {};
82
97
  /**
package/lib/SHExecute.js CHANGED
@@ -1,168 +1,236 @@
1
- import { spawnSync, spawn, exec } from 'node:child_process';
1
+ import { spawnSync, spawn} from 'node:child_process';
2
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|number} 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
- }
3
+ // Local helpers
4
+ const isFinitePosInt = (n) => Number.isFinite(n) && n >= 0;
5
+ // Parse duration: accepts numbers (ms) or strings like "250", "250ms", or "2s". null/undefined yields 0; negatives or invalid forms throw.
6
+ const parseDuration = (d) => {
7
+ if (typeof d === 'number') return isFinitePosInt(d) ? d : (() => { throw new Error('Invalid duration'); })();
8
+ if (typeof d === 'string') {
9
+ const m = d.match(/^(\d+)(ms|s)?$/);
10
+ if (!m) throw new Error('Invalid duration string');
11
+ const n = Number(m[1]);
12
+ return m[2] === 's' ? n * 1000 : n;
13
+ }
14
+ if (d == null) return 0;
15
+ throw new Error('Invalid duration type');
16
+ };
17
+
18
+ // Kill a process tree using pgrep without going through a shell
19
+ const childrenOf = (pid) => new Promise((resolve, reject) => {
20
+ const p = spawn('pgrep', ['-P', String(pid)], { stdio: ['ignore', 'pipe', 'ignore'] });
21
+ const out = [];
22
+ p.stdout.on('data', (b) => out.push(b));
23
+ p.on('close', (code) => {
24
+ if (code === 0) {
25
+ const ids = Buffer.concat(out).toString('utf8').trim().split(/\s+/).map(Number).filter(Boolean);
26
+ resolve(ids);
27
+ } else if (code === 1) {
28
+ resolve([]); // no children
29
+ } else {
30
+ reject(new Error(`pgrep -P ${pid} failed with code ${code}`));
31
+ }
32
+ });
33
+ p.on('error', reject);
34
+ });
46
35
 
36
+ /**
37
+ * SHExecute
38
+ * Low-level process runner used by SHDispatch.
39
+ *
40
+ * Features:
41
+ * - Optional shell execution: if options.shell is a string (e.g., 'bash') or true, runs `${prefix}; ${command}` via that shell; otherwise runs without a shell using `/usr/bin/env -S`.
42
+ * - Timeout: options.timeout may be a number (ms) or string like '1500ms' or '2s'. On timeout, run() rejects with a descriptive error.
43
+ * - Buffering: Captures stdout/stderr up to maxBuffer bytes per stream (default 40 MiB). Appends "[stdout truncated]" / "[stderr truncated]" markers if exceeded.
44
+ * - Payload: Passing a payload writes it to stdin and forces stdin to be a pipe.
45
+ * - Detached mode: If options.detached is true, run() resolves to '' after ~1s and unrefs the process.
46
+ * - Kill support: kill(signal) attempts to terminate the process (and some children) and causes run() to reject with "Process killed (forced).".
47
+ */
47
48
  class SHExecute {
48
- #forcedKill = false;
49
- /**
50
- * @type {import('child_process').ChildProcess}
51
- */
52
- #proc;
53
- #prefix = '';
54
- #command = '';
55
- /** @type {import('child_process').SpawnOptions | import('child_process').SpawnSyncOptions} */
56
- #options = {};
57
- #stdout = '';
58
- #stderr = '';
59
- /**
60
- * @param {string} command - linux command to be executed
61
- * @param {string} prefix - command prefix (bash, sh etc.)
62
- * @param {import('child_process').SpawnOptions | import('child_process').SpawnSyncOptions} options
63
- */
64
- constructor(command, prefix, options = {}) {
65
- this.#prefix = prefix;
66
- this.#command = command;
67
- this.#options = options;
68
- this.#proc = null;
69
- this.#stdout = '';
70
- this.#stderr = '';
71
- }
72
- /**
73
- * @param {string} [payload] - data to write
74
- * @retuns {Promise<object>}
75
- */
76
- runSync(payload) {
77
- if (payload && typeof payload !== 'string') {
78
- throw new Error('Argument is not a string');
79
- }
80
- /** @type {import('node:child_process').SpawnSyncOptions} */
81
- const options = this.#options;
82
- // pipe need to be set on stdin when posting a payload
83
- // @ts-ignore
84
- if (payload) options['stdio'][0] = 'pipe';
85
- const input = payload || undefined;
86
- if (input) {
87
- options.input = input
88
- }
89
- return spawnSync(this.#prefix, [this.#command], options);
90
- }
91
- /**
92
- * @param {string} [payload] - data to write
93
- * @retuns {Promise<string>}
94
- */
95
- run(payload) {
96
- if (payload && typeof payload !== 'string') {
97
- throw new Error('Argument is not a string');
98
- }
99
- /** @type {import('node:child_process').SpawnOptions} */
100
- const options = this.#options;
101
- // pipe need to be set on stdin when posting a payload
102
- // @ts-ignore
103
- if (payload) options.stdio[0] = 'pipe';
104
- this.#proc = spawn(this.#prefix, [this.#command], options);
105
- this.#proc.stdout?.on('data', (data) => {
106
- this.#stdout += data;
107
- });
108
-
109
- this.#proc.stderr?.on('data', (data) => {
110
- this.#stderr += data;
111
- });
112
- if (payload) {
113
- this.#proc.stdin.end(payload);
114
- }
115
- return new Promise((resolve, reject) => {
116
- // https://nodejs.org/docs/latest/api/child_process.html#optionsdetached
117
- if (options.detached) {
118
- setTimeout(() => {
119
- resolve('');
120
- this.#proc.unref();
121
- }, 1000);
122
- }
123
- this.#proc.on('close', (code) => {
124
- if (this.#forcedKill) {
125
- // Resolve without content
126
- resolve();
127
- return;
128
- }
129
- // Detached does not closes with an exitcode
130
- if (code === 0 || code === null || typeof (code) === 'undefined') {
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
- * Kill this process and possible child processes
144
- * @param {number | string} signal - kill signal
145
- * @returns {Promise<number[]>}
146
- */
147
- async kill(signal = 'SIGTERM') {
148
- if (!this.#proc) throw new Error('Trying to kill a process without creating one.');
149
- if (!this.#proc.pid) throw new Error('The process pid is undefined.');
150
- this.#forcedKill = true;
151
- let res = [];
152
- try {
153
- // Try to kill pid 'childs'
154
- res = await killProcesses(this.#proc.pid, signal);
155
- } catch (_e) { }
156
- if (!res.includes(this.#proc.pid)) {
157
- // Kill self if I am not allready killed
158
- res.push(this.#proc.pid);
159
- // @ts-ignore
160
- this.#proc.kill(signal);
161
- }
162
- this.#proc = undefined;
163
- return res;
164
- }
49
+ #forcedKill = false;
50
+ /** @type {import('child_process').ChildProcess} */
51
+ #proc;
52
+ #prefix = '';
53
+ #command = '';
54
+ /** @type {import('child_process').SpawnOptions | import('child_process').SpawnSyncOptions} */
55
+ #options = {};
56
+ #stdoutChunks = [];
57
+ #stderrChunks = [];
58
+ #stdoutLen = 0;
59
+ #stderrLen = 0;
60
+ #maxBuffer = 40 * 1024 * 1024; // 40MB per stream
61
+ #truncated = { stdout: false, stderr: false };
62
+ #timedOut = false;
63
+
64
+ /**
65
+ * @param {string} command - linux command to be executed
66
+ * @param {string} prefix - command prefix (shell prelude, e.g. 'set -euo pipefail')
67
+ * @param {import('child_process').SpawnOptions & { maxBuffer?: number }} options
68
+ */
69
+ constructor(command, prefix, options = {}) {
70
+ this.#prefix = prefix;
71
+ this.#command = command;
72
+ const { maxBuffer, ...spawnOpts } = options ?? {};
73
+ this.#maxBuffer = Number.isFinite(maxBuffer) && maxBuffer > 0 ? maxBuffer : this.#maxBuffer;
74
+ this.#options = spawnOpts;
75
+ this.#proc = null;
76
+ this.#stdoutChunks = [];
77
+ this.#stderrChunks = [];
78
+ this.#stdoutLen = 0;
79
+ this.#stderrLen = 0;
80
+ this.#truncated = { stdout: false, stderr: false };
81
+ this.#timedOut = false;
82
+ }
83
+
84
+ /**
85
+ * @param {string} [payload] - data to write
86
+ * @returns {import('child_process').SpawnSyncReturns}
87
+ */
88
+ runSync(payload) {
89
+ this.#forcedKill = false;
90
+ if (payload && typeof payload !== 'string') {
91
+ throw new Error('Argument is not a string');
92
+ }
93
+ /** @type {import('node:child_process').SpawnSyncOptions} */
94
+ const options = { ...this.#options, shell: false };
95
+ if (payload) {
96
+ const stdio = Array.isArray(options.stdio) ? [...options.stdio] : ['pipe', 'pipe', 'pipe'];
97
+ stdio[0] = 'pipe';
98
+ // @ts-ignore
99
+ options.stdio = stdio;
100
+ options.input = payload;
101
+ }
102
+
103
+ const shellOpt = this.#options?.shell;
104
+ if (shellOpt) {
105
+ const sh = typeof shellOpt === 'string' ? shellOpt : 'bash';
106
+ const cmd = `${this.#prefix}; ${this.#command}`;
107
+ return spawnSync(sh, ['-c', cmd], options);
108
+ }
109
+ // no-shell mode
110
+ return spawnSync('/usr/bin/env', ['-S', this.#command], options);
111
+ }
112
+
113
+ /**
114
+ * @param {string} [payload] - data to write
115
+ * @returns {Promise<string>}
116
+ */
117
+ run(payload) {
118
+ this.#forcedKill = false;
119
+ if (payload && typeof payload !== 'string') {
120
+ throw new Error('Argument is not a string');
121
+ }
122
+ /** @type {import('node:child_process').SpawnOptions} */
123
+ const options = { ...this.#options, shell: false };
124
+ if (payload) {
125
+ const stdio = Array.isArray(options.stdio) ? [...options.stdio] : ['pipe', 'pipe', 'pipe'];
126
+ stdio[0] = 'pipe';
127
+ // @ts-ignore
128
+ options.stdio = stdio;
129
+ }
130
+
131
+ // reset buffers for each run
132
+ this.#stdoutChunks = [];
133
+ this.#stderrChunks = [];
134
+ this.#stdoutLen = 0;
135
+ this.#stderrLen = 0;
136
+ this.#truncated = { stdout: false, stderr: false };
137
+ this.#timedOut = false;
138
+
139
+ const shellOpt = this.#options?.shell;
140
+ if (shellOpt) {
141
+ const sh = typeof shellOpt === 'string' ? shellOpt : 'bash';
142
+ const cmd = `${this.#prefix}; ${this.#command}`;
143
+ this.#proc = spawn(sh, ['-c', cmd], options);
144
+ } else {
145
+ this.#proc = spawn('/usr/bin/env', ['-S', this.#command], options);
146
+ }
147
+
148
+ if (payload) this.#proc.stdin?.end(payload);
149
+
150
+ const ms = parseDuration(this.#options?.timeout ?? 0);
151
+ let timeoutId = null;
152
+ if (ms > 0) {
153
+ timeoutId = setTimeout(() => {
154
+ this.#timedOut = true;
155
+ this.kill('SIGTERM').catch(() => {});
156
+ }, ms);
157
+ }
158
+
159
+ this.#proc.stdout?.on('data', (chunk) => {
160
+ this.#stdoutLen += chunk.length;
161
+ if (this.#stdoutLen <= this.#maxBuffer) {
162
+ this.#stdoutChunks.push(chunk);
163
+ } else {
164
+ this.#truncated.stdout = true;
165
+ }
166
+ });
167
+
168
+ this.#proc.stderr?.on('data', (chunk) => {
169
+ this.#stderrLen += chunk.length;
170
+ if (this.#stderrLen <= this.#maxBuffer) {
171
+ this.#stderrChunks.push(chunk);
172
+ } else {
173
+ this.#truncated.stderr = true;
174
+ }
175
+ });
176
+
177
+ return new Promise((resolve, reject) => {
178
+ if (options.detached) {
179
+ setTimeout(() => {
180
+ resolve('');
181
+ this.#proc.unref();
182
+ }, 1000);
183
+ }
184
+
185
+ this.#proc.on('close', (code, signal) => {
186
+ if (timeoutId) clearTimeout(timeoutId);
187
+ if (this.#forcedKill) {
188
+ reject(new Error('Process killed (forced).'));
189
+ return;
190
+ }
191
+ if (this.#timedOut) {
192
+ reject(new Error(`Process timed out after ${ms}ms.`));
193
+ return;
194
+ }
195
+ if (code === 0) {
196
+ const stdoutBuf = this.#stdoutChunks.length ? Buffer.concat(this.#stdoutChunks) : Buffer.alloc(0);
197
+ let stdout = stdoutBuf.toString('utf8').trim();
198
+ if (this.#truncated.stdout) stdout += '\n[stdout truncated]\n';
199
+ resolve(stdout);
200
+ } else {
201
+ const stderrBuf = this.#stderrChunks.length ? Buffer.concat(this.#stderrChunks) : Buffer.alloc(0);
202
+ let stderr = stderrBuf.toString('utf8').trim();
203
+ if (this.#truncated.stderr) stderr += '\n[stderr truncated]\n';
204
+ reject(new Error(`Exit ${code ?? 'null'}${signal ? `, signal ${signal}` : ''}: ${this.#command}${stderr ? ` :: ${stderr}` : ''}`));
205
+ }
206
+ });
207
+
208
+ this.#proc.on('error', (err) => reject(err));
209
+ });
210
+ }
211
+
212
+ /**
213
+ * Kill this process and possible child processes
214
+ * @param {number | string} signal - kill signal
215
+ * @returns {Promise<number[]>}
216
+ */
217
+ async kill(signal = 'SIGTERM') {
218
+ if (!this.#proc) throw new Error('Trying to kill a process without creating one.');
219
+ if (!this.#proc.pid) throw new Error('The process pid is undefined.');
220
+ this.#forcedKill = true;
221
+ const pid = this.#proc.pid;
222
+ let killed = [];
223
+ try {
224
+ const kids = await childrenOf(pid).catch(() => []);
225
+ for (const k of kids) {
226
+ try { process.kill(k, signal); killed.push(k); } catch (e) { if (!e || e.code !== 'ESRCH') throw e; }
227
+ }
228
+ try { process.kill(pid, signal); killed.push(pid); } catch (e) { if (!e || e.code !== 'ESRCH') throw e; }
229
+ } finally {
230
+ this.#proc = undefined;
231
+ }
232
+ return killed;
233
+ }
165
234
  }
166
235
 
167
236
  export default SHExecute;
168
-
package/lib/Test.js CHANGED
@@ -64,7 +64,7 @@ class Test {
64
64
  */
65
65
  #errors = [];
66
66
  /** verbosed **/
67
- #quite = false;
67
+ #quiet = false;
68
68
  /** Timeout in ms to settle async code blocks called from sync methods */
69
69
  #TO = SETTLE_ASYNC;
70
70
  /**
@@ -72,7 +72,7 @@ class Test {
72
72
  */
73
73
  constructor(quiet = false) {
74
74
  if (quiet) {
75
- this.#quite = true;
75
+ this.#quiet = true;
76
76
  }
77
77
  // Track for unresolved promises
78
78
  this.#promiseTracker = new AsyncTracker();
@@ -149,7 +149,7 @@ class Test {
149
149
  this.#reports[i] = { description: cb.description, duration, executed };
150
150
  let error;
151
151
  try {
152
- if (!this.#quite) process.stdout.write(`${i}. ${cb.description} `);
152
+ if (!this.#quiet) process.stdout.write(`${i}. ${cb.description} `);
153
153
  await Promise.resolve(cb.callback());
154
154
  executed = true;
155
155
  if (type === 'Function') {
@@ -167,7 +167,7 @@ class Test {
167
167
  error = e;
168
168
  }
169
169
  duration = getDuration(start);
170
- if (!this.#quite && !error) process.stdout.write(`(duration: ${duration} ms)\n`);
170
+ if (!this.#quiet && !error) process.stdout.write(`(duration: ${duration} ms)\n`);
171
171
  this.#reports[i].duration = duration;
172
172
  this.#reports[i].executed = executed;
173
173
  if (error) {
@@ -198,14 +198,14 @@ class Test {
198
198
 
199
199
  }
200
200
  const errors = this.#errors.length;
201
- if (!this.#quite) {
201
+ if (!this.#quiet) {
202
202
  console.log('--------------------------------------------------');
203
203
  console.log(`Total: ${tests} tests, executed: ${executed} in ${duration} ms - errors: ${errors}`);
204
204
  if (tests !== executed) {
205
- if (!this.#quite) console.log('** Not all tests have been executed **');
205
+ if (!this.#quiet) console.log('** Not all tests have been executed **');
206
206
  return { tests, executed, duration, errors };
207
207
  }
208
- // Report on unresolved promises when NOT quite
208
+ // Report on unresolved promises when NOT quiet
209
209
  // This must be on a next tick because the current Promise (where this code is in) is not resolved yet
210
210
  nextTick(() => {
211
211
  const count = this.#promiseTracker.report(true);
@@ -235,9 +235,9 @@ class Test {
235
235
  // A global error is an error catched outside the callstack of the test
236
236
  const ERR = outside ? 'GLOBAL_ERROR' : 'ERROR'
237
237
  if (this.#currentTest > -1) {
238
- if (!this.#quite) process.stdout.write(`\n`);
238
+ if (!this.#quiet) process.stdout.write(`\n`);
239
239
  // Register this error
240
- // Always print out errors despite #quite
240
+ // Always print out errors despite #quiet
241
241
  const description = this.#reports[this.#currentTest].description;
242
242
  const executed = this.#reports[this.#currentTest].executed;
243
243
  if (executed) {
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": "1.1.21",
5
+ "version": "1.1.23",
6
6
  "description": "Execute shell commands on Linux-based systems from javascript",
7
7
  "main": "lib/SH.js",
8
8
  "types": "types/SH.d.ts",
package/types/SH.d.ts CHANGED
@@ -11,14 +11,7 @@ export type Shell = Function;
11
11
  * A template-tag that returns an SHDispatch.
12
12
  */
13
13
  export type SHTag = (pieces: TemplateStringsArray, ...args: unknown[]) => SHDispatch;
14
- /**
15
- * A template-tag that returns an SHDispatch.
16
- * @typedef {(pieces: TemplateStringsArray, ...args: unknown[]) => SHDispatch} SHTag
17
- */
18
- /**
19
- * @type {Shell & SHTag}
20
- */
21
- export const SH: Shell & SHTag;
14
+ export function SH(pieces: any, ...args: any[]): SHDispatch;
22
15
  /**
23
16
  * Change working directory
24
17
  * @param {string} dir
@@ -59,6 +52,10 @@ export function retry(count: number, a: string | number | typeof expBackoff | Fu
59
52
  * @example
60
53
  * const content = await readIn();
61
54
  */
55
+ /**
56
+ * Read entire stdin as UTF-8. If stdin is a TTY, resolves to an empty string.
57
+ * @returns {Promise<string>}
58
+ */
62
59
  export function readIn(): Promise<string>;
63
60
  /**
64
61
  * Get user input from the command line (stdin)
@@ -113,6 +110,21 @@ export function expBackoff(max?: string, rand?: string): Generator<number, void,
113
110
  * - The value is either the next argument or `true` if no value is provided,
114
111
  * - The `_` property contains an array of unbound arguments.
115
112
  */
113
+ /**
114
+ * Parse command-line args into an object.
115
+ *
116
+ * Supported:
117
+ * - --key value, -k value (no grouped short flags)
118
+ * - Bare values collected under `_.`
119
+ * - Duplicate keys throw an error; keys without a following value become true.
120
+ *
121
+ * Not supported:
122
+ * - --key=value syntax
123
+ * - Grouped short flags like -abc
124
+ *
125
+ * @param {string[]} [args]
126
+ * @returns {ArgsObject}
127
+ */
116
128
  export function parseArgs(args?: string[]): ArgsObject;
117
129
  /**
118
130
  * @typedef {Object.<string, string>} ArgsObject
@@ -1,29 +1,29 @@
1
1
  export default SHDispatch;
2
2
  export type SpawnSyncResponse = {
3
3
  /**
4
- * - The exit code of the child process. A value of `0` indicates success.
4
+ * - Exit code of the child process (null if terminated by signal).
5
5
  */
6
- status: number;
6
+ status: number | null;
7
7
  /**
8
- * - The signal used to terminate the process, if any.
8
+ * - Name of the terminating signal, if any.
9
9
  */
10
- signal: Buffer | null;
10
+ signal: string | null;
11
11
  /**
12
- * - An array containing the standard output and standard error of the child process.
12
+ * - [stdin, stdout, stderr] per Node's SpawnSyncReturns.
13
13
  */
14
- output: Array<string | null>;
14
+ output: (string | Buffer | null)[];
15
15
  /**
16
- * - The process ID of the child process.
16
+ * - PID of the spawned process.
17
17
  */
18
18
  pid: number;
19
19
  /**
20
- * - The standard output of the child process.
20
+ * - Stdout collected (type depends on encoding option).
21
21
  */
22
- stdout: Buffer | null;
22
+ stdout: string | Buffer | null;
23
23
  /**
24
- * - The standard error of the child process.
24
+ * - Stderr collected (type depends on encoding option).
25
25
  */
26
- stderr: Buffer | null;
26
+ stderr: string | Buffer | null;
27
27
  };
28
28
  export type SHOptions = {
29
29
  /**
@@ -37,7 +37,7 @@ export type SHOptions = {
37
37
  /**
38
38
  * - Explicitly set the value of `argv[0]` sent to the child process.
39
39
  */
40
- argv0?: string | any[] | undefined;
40
+ argv0?: string | undefined;
41
41
  /**
42
42
  * - If true, the child will be a process group leader.
43
43
  */
@@ -55,17 +55,17 @@ export type SHOptions = {
55
55
  */
56
56
  stdio?: number | "pipe" | "ignore" | "inherit" | StdioOption[] | undefined;
57
57
  /**
58
- * - If true, runs command inside a shell.
58
+ * - If string or true, runs the command via that shell ('bash' if true).
59
59
  */
60
60
  shell?: string | boolean | undefined;
61
61
  /**
62
- * - In milliseconds, specifies when to terminate the child process.
62
+ * - Milliseconds before sending SIGTERM (0 = no timeout).
63
63
  */
64
64
  timeout?: number | undefined;
65
65
  /**
66
- * - The input to write to stdin.
66
+ * - Optional stdin payload for spawnSync. Use .run(payload) for async.
67
67
  */
68
- input?: string | Buffer<ArrayBufferLike> | URL | undefined;
68
+ input?: string | Uint8Array<ArrayBufferLike> | Buffer<ArrayBufferLike> | undefined;
69
69
  };
70
70
  export type StdioOption = ("pipe" | "ignore" | "inherit" | number);
71
71
  export type StdioOptions = Array<StdioOption> | StdioOption;
@@ -1,26 +1,40 @@
1
1
  export default SHExecute;
2
+ /**
3
+ * SHExecute
4
+ * Low-level process runner used by SHDispatch.
5
+ *
6
+ * Features:
7
+ * - Optional shell execution: if options.shell is a string (e.g., 'bash') or true, runs `${prefix}; ${command}` via that shell; otherwise runs without a shell using `/usr/bin/env -S`.
8
+ * - Timeout: options.timeout may be a number (ms) or string like '1500ms' or '2s'. On timeout, run() rejects with a descriptive error.
9
+ * - Buffering: Captures stdout/stderr up to maxBuffer bytes per stream (default 40 MiB). Appends "[stdout truncated]" / "[stderr truncated]" markers if exceeded.
10
+ * - Payload: Passing a payload writes it to stdin and forces stdin to be a pipe.
11
+ * - Detached mode: If options.detached is true, run() resolves to '' after ~1s and unrefs the process.
12
+ * - Kill support: kill(signal) attempts to terminate the process (and some children) and causes run() to reject with "Process killed (forced).".
13
+ */
2
14
  declare class SHExecute {
3
15
  /**
4
- * @param {string} command - linux command to be executed
5
- * @param {string} prefix - command prefix (bash, sh etc.)
6
- * @param {import('child_process').SpawnOptions | import('child_process').SpawnSyncOptions} options
7
- */
8
- constructor(command: string, prefix: string, options?: import("child_process").SpawnOptions | import("child_process").SpawnSyncOptions);
16
+ * @param {string} command - linux command to be executed
17
+ * @param {string} prefix - command prefix (shell prelude, e.g. 'set -euo pipefail')
18
+ * @param {import('child_process').SpawnOptions & { maxBuffer?: number }} options
19
+ */
20
+ constructor(command: string, prefix: string, options?: import("child_process").SpawnOptions & {
21
+ maxBuffer?: number;
22
+ });
9
23
  /**
10
- * @param {string} [payload] - data to write
11
- * @retuns {Promise<object>}
12
- */
13
- runSync(payload?: string): import("child_process").SpawnSyncReturns<string | Buffer<ArrayBufferLike>>;
24
+ * @param {string} [payload] - data to write
25
+ * @returns {import('child_process').SpawnSyncReturns}
26
+ */
27
+ runSync(payload?: string): import("child_process").SpawnSyncReturns<any>;
14
28
  /**
15
- * @param {string} [payload] - data to write
16
- * @retuns {Promise<string>}
17
- */
18
- run(payload?: string): Promise<any>;
29
+ * @param {string} [payload] - data to write
30
+ * @returns {Promise<string>}
31
+ */
32
+ run(payload?: string): Promise<string>;
19
33
  /**
20
- * Kill this process and possible child processes
21
- * @param {number | string} signal - kill signal
22
- * @returns {Promise<number[]>}
23
- */
34
+ * Kill this process and possible child processes
35
+ * @param {number | string} signal - kill signal
36
+ * @returns {Promise<number[]>}
37
+ */
24
38
  kill(signal?: number | string): Promise<number[]>;
25
39
  #private;
26
40
  }