@j-o-r/sh 1.1.25 → 1.1.26

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
@@ -103,22 +103,41 @@ const parseDuration = (d) => {
103
103
  }
104
104
  throw new Error(`Unknown duration: "${d}".`);
105
105
  }
106
+
107
+
108
+ /** @type {import('./SHDispatch.js').SHOptions} */
109
+ const defaultOptions = {
110
+ cwd: process.cwd(),
111
+ env: process.env,
112
+ shell: 'bash',
113
+ stdio: ['inherit', 'pipe', 'pipe'],
114
+ timeout: 0, // when 0 there is no timeout
115
+ };
116
+
117
+ const setHandler = {
118
+ set(target, prop, value) {
119
+ defaultOptions[prop] = value; // Allow setting on target
120
+ // console.log(`Set ${String(prop)} = ${value}`);
121
+ // console.log(defaultOptions);
122
+ return true;
123
+ }
124
+ }
125
+
106
126
  /**
107
127
  * A template-tag that returns an SHDispatch.
108
128
  * @typedef {(pieces: TemplateStringsArray, ...args: unknown[]) => SHDispatch} SHTag
109
129
  */
110
130
  /**
111
- * @type {Shell & SHTag}
131
+ * SH template tag
132
+ *
133
+ * Interpolation rules:
134
+ * - 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 '\''.
135
+ * - Non-array values: currently coerced with String(value) and inserted as-is (NOT shell-escaped). Be careful when interpolating untrusted input.
136
+ *
137
+ * @type {Shell & SHTag & defaultOptions}
138
+ * Returns an SHDispatch that can be configured via .options() and executed with .run() / .runSync().
139
+ * @returns {SHDispatch}
112
140
  */
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
- */
122
141
  const SH = new Proxy(function(pieces, ...args) {
123
142
  if (pieces.some((p) => p == undefined)) {
124
143
  throw new Error(`Malformed command ${pieces}`);
@@ -145,8 +164,9 @@ const SH = new Proxy(function(pieces, ...args) {
145
164
  }
146
165
  cmd += s + pieces[++i];
147
166
  }
148
- return new SHDispatch(cmd);
149
- }, {});
167
+ return new SHDispatch(cmd, defaultOptions);
168
+ }, setHandler);
169
+
150
170
 
151
171
  /**
152
172
  * Create a async/sync context in new execution callstack
@@ -220,11 +240,12 @@ const userIn = (prompt) => {
220
240
 
221
241
  const cleanup = () => {
222
242
  process.stdin.removeListener('data', onData);
243
+ // @ts-ignore
223
244
  rl.removeAllListeners('line');
224
245
  clearTimeout(timer);
225
246
  rl.close();
226
247
  };
227
-
248
+ // @ts-ignore
228
249
  rl.on('line', (line) => {
229
250
  buffer += line + '\n';
230
251
  clearTimeout(timer);
package/lib/SHDispatch.js CHANGED
@@ -48,7 +48,7 @@ import SHExec from './SHExecute.js';
48
48
  /**
49
49
  * Merge property values while maintaining the fixed set of props from the predefined object
50
50
  * @param {SHOptions} predefined - options
51
- * @param {import('child_process').SpawnOptions | import('child_process').SpawnSyncOptions} options
51
+ * @param {SHOptions} options
52
52
  * @returns {SHOptions}
53
53
  */
54
54
  const mergeOptions = (predefined, options) => {
@@ -63,7 +63,6 @@ const mergeOptions = (predefined, options) => {
63
63
  return mergedObj;
64
64
  }
65
65
 
66
- /** @type {SHOptions} */
67
66
  /**
68
67
  * SHOptions — effective defaults and semantics used by SHDispatch/SHExecute
69
68
  *
@@ -79,19 +78,12 @@ const mergeOptions = (predefined, options) => {
79
78
  * - The prefix (see options(prefix)) is only applied when a shell is used; it is ignored in no-shell mode.
80
79
  * - Each call to options() resets to the defaults and merges the provided options; it does not accumulate from prior calls.
81
80
  */
82
- const defaultOptions = {
83
- cwd: process.cwd(),
84
- env: process.env,
85
- shell: 'bash',
86
- stdio: ['inherit', 'pipe', 'pipe'],
87
- timeout: 0 // when 0 there is no timeout
88
- };
89
-
90
81
 
91
82
 
92
83
  class SHDispatch {
93
84
  // #prefix = 'set -euo pipefail;/usr/bin/env'
94
- #prefix = 'set -euo pipefail'
85
+ // #prefix = 'set -euo pipefail'
86
+ #prefix = '';
95
87
  #cmd = '';
96
88
  #options = {};
97
89
  /**
@@ -100,23 +92,29 @@ class SHDispatch {
100
92
  #proc;
101
93
  /**
102
94
  * @param {string} cmd - cmd to execute
95
+ * @param {SHOptions} options
96
+ * @param {string} [prefix] - command prefix e.g (default) '/usr/bin/env'
103
97
  */
104
- constructor(cmd) {
105
- if (!cmd || cmd === '') {
98
+ constructor(cmd, options, prefix) {
99
+ if (typeof cmd !== 'string' || cmd === '') {
106
100
  throw new Error('Undefined command');
107
101
  }
108
102
  this.#cmd = cmd;
109
- this.#options = defaultOptions
103
+ this.options(options, prefix);
110
104
  }
111
105
  /**
112
- * @param {import('child_process').SpawnOptions | import('child_process').SpawnSyncOptions} options
106
+ * @param {SHOptions} [options]
113
107
  * @param {string} [prefix] - command prefix e.g (default) '/usr/bin/env'
114
108
  * @returns {SHDispatch}
115
109
  */
116
110
  options(options, prefix) {
111
+ // this.#options = defaultOptions
117
112
  if (typeof prefix === 'string') {
118
113
  this.#prefix = prefix;
119
114
  }
115
+ if (!options) {
116
+ return this;
117
+ }
120
118
  if (options.stdio && typeof options.stdio === 'string') {
121
119
  // convert stdio to array
122
120
  // This sets the default io values
@@ -124,7 +122,7 @@ class SHDispatch {
124
122
  const io = options.stdio;
125
123
  options.stdio = Array(3).fill(io);
126
124
  }
127
- this.#options = mergeOptions(defaultOptions, options)
125
+ this.#options = mergeOptions(this.#options, options);
128
126
  return this;
129
127
  }
130
128
 
package/module.md CHANGED
@@ -3,7 +3,7 @@
3
3
  ## Overview
4
4
 
5
5
  **Name:** @j-o-r/sh
6
- **Version:** 1.1.23
6
+ **Version:** 1.1.26
7
7
  **Description:** Execute shell commands on Linux-based systems from javascript.
8
8
 
9
9
  This module simplifies the execution of shell commands within JavaScript applications, providing utilities to handle shell scripts and manage their output efficiently. It is inspired by the zx library and supports features like command execution, retries, user input, and more.
@@ -33,117 +33,196 @@ Requires Node.js >= 20.0.0.
33
33
 
34
34
  ### Basic Usage
35
35
 
36
- To execute a shell command, use the `SH` function:
37
-
38
36
  ```javascript
39
37
  import { SH } from '@j-o-r/sh';
40
38
 
41
- SH`your_shell_command`.run()
42
- .then(output => {
43
- console.log('Output:', output);
44
- })
45
- .catch(error => {
46
- console.error('Error:', error);
47
- });
39
+ const output = await SH`ls -la`.run();
40
+ console.log(output);
48
41
  ```
49
42
 
50
43
  ### Advanced Usage
51
44
 
52
45
  ```javascript
53
- import { SH, cd, within, sleep, retry, expBackoff } from '@j-o-r/sh';
46
+ import { SH, cd, within, sleep, retry, expBackoff, userIn } from '@j-o-r/sh';
47
+
48
+ cd('/tmp'); // Change directory
54
49
 
55
- const res = await SH`ls -FLa | grep package.json | wc -l`.run();
50
+ const res = await SH`echo "Hello World"`.run();
56
51
  console.log(res);
57
52
 
58
- const ar = within(async () => {
59
- const res = await Promise.all([
53
+ // Retry example
54
+ try {
55
+ const p = await retry(3, expBackoff(), () => SH`curl -s https://unreachable`.run());
56
+ console.log(p);
57
+ } catch (e) {
58
+ console.error('Retry failed:', e);
59
+ }
60
+
61
+ // User input
62
+ const user = userIn('Enter your name: ');
63
+ const name = await user.input;
64
+ console.log('Hello,', name);
65
+
66
+ // Within async context
67
+ within(async () => {
68
+ const results = await Promise.all([
60
69
  SH`sleep 1; echo 1`.run(),
61
70
  SH`sleep 2; echo 2`.run(),
62
71
  sleep(2),
63
72
  SH`sleep 3; echo 3`.run()
64
73
  ]);
74
+ console.log(results);
65
75
  });
66
-
67
- const p = await retry(3, expBackoff(), () => SH`curl -s https://unreachable`.run());
68
76
  ```
69
77
 
70
- ### Additional Examples
78
+ ### Test Framework Example
79
+
80
+ ```javascript
81
+ import { Test, assert, jsType } from '@j-o-r/sh';
82
+
83
+ const test = new Test();
84
+ test.add('Test type', () => {
85
+ assert.strictEqual(jsType('string'), 'String');
86
+ });
87
+ const report = await test.run();
88
+ if (report.errors > 0) {
89
+ process.exit(1);
90
+ }
91
+ ```
71
92
 
72
- - Prompt user for input:
73
- ```javascript
74
- const user = userIn('Enter your name: ');
75
- const name = await user.input;
76
- console.log('Hello,', name);
77
- ```
93
+ ### AsyncTracker Example
78
94
 
79
- - Create and run a test:
80
- ```javascript
81
- import { assert, jsType, Test } from '@j-o-r/sh';
95
+ ```javascript
96
+ import { AsyncTracker } from '@j-o-r/sh';
82
97
 
83
- const test = new Test();
84
- test.add('Test is test in sync', () => {
85
- assert.strictEqual(jsType(test), 'Test');
86
- });
87
- const report = await test.run();
88
- ```
98
+ const tracker = new AsyncTracker();
99
+ tracker.enable();
100
+ setTimeout(() => {
101
+ tracker.report(true); // Verbose report
102
+ }, 1000);
103
+ ```
89
104
 
90
105
  ## Full API Reference
91
106
 
92
107
  ### Main Exports
93
108
 
94
- - **SH**: A template tag function that returns an `SHDispatch` object for executing shell commands.
95
- - **cd(dir: string)**: Changes the working directory.
96
- - **sleep(duration: string | number)**: Pauses execution for the specified duration (e.g., '5s').
97
- - **retry(count: number, a: string | number | expBackoff | Function, b?: Function)**: Retries a function with optional intervals.
98
- - **readIn()**: Reads from stdin as a promise.
99
- - **userIn(prompt: string)**: Prompts for user input, returns object with `input` promise and `abort` method.
100
- - **within(callback: Function)**: Creates an async context.
101
- - **expBackoff(max?: string, rand?: string)**: Generator for exponential backoff intervals.
102
- - **parseArgs(args?: string[])**: Parses command-line arguments into an object.
103
- - **jsType(any)**: Returns the 'real' JavaScript type.
104
- - **hasProp(o: any, p: string)**: Safely checks if an object has a property.
105
- - **Test**: Class for a minimal test framework.
106
- - **assert**: Node.js assert library.
107
- - **AsyncTracker**: Class for tracking async operations.
109
+ - **SH**: `(pieces: TemplateStringsArray, ...args: unknown[]) => SHDispatch`
110
+ - Template tag for creating shell commands. Returns SHDispatch.
111
+ - Interpolation: Arrays elements trimmed/escaped; non-arrays String(value) as-is (no escape, careful with untrusted input).
112
+
113
+ - **cd(dir: string)**: `void`
114
+ - Changes the working directory.
115
+
116
+ - **sleep(duration: string | number)**: `Promise<any>`
117
+ - Pauses execution for duration (e.g., '5s', 5000 ms).
118
+
119
+ - **retry(count: number, a: string | number | typeof expBackoff | Function, b?: Function)**: `Promise<any>`
120
+ - Retries callback up to `count` times. `a` can be delay, backoff generator, or callback (then `b` is callback).
121
+
122
+ - **readIn()**: `Promise<string>`
123
+ - Reads stdin as UTF-8, empty if TTY.
124
+
125
+ - **userIn(prompt: string)**: `{input: Promise<string | void>, abort: () => void}`
126
+ - Prompts user for input.
127
+
128
+ - **within(callback: Function)**: `Promise<any>`
129
+ - Executes callback in a new execution context.
130
+
131
+ - **expBackoff(max?: string, rand?: string)**: `Generator<number>`
132
+ - Yields exponential backoff intervals with jitter (max default '60s', rand '100ms').
133
+
134
+ - **parseArgs(args?: string[])**: `ArgsObject`
135
+ - Parses args: --key value, -k value; duplicates error; no =value or grouped shorts; _ for unnamed.
136
+
137
+ - **jsType(any: any)**: `string`
138
+ - Returns real JS type name (e.g., 'Array' for []).
139
+
140
+ - **hasProp(o: any, p: string)**: `boolean`
141
+ - Safely checks if `o` has own property `p` (handles null/undefined).
142
+
143
+ - **assert**: `typeof import('node:assert')`
144
+ - Node.js assert module.
145
+
146
+ - **Test**: Class - Minimal sync/async test framework (see below).
147
+
148
+ - **AsyncTracker**: Class - Tracks async operations using Node async_hooks (see below).
108
149
 
109
150
  ### SHDispatch Class
110
151
 
111
- Returned by the `SH` template tag. Methods:
152
+ `new SHDispatch(cmd: string, options: SHOptions, prefix?: string)`
153
+
154
+ - **options(options?: SHOptions, prefix?: string)**: `SHDispatch`
155
+ - Configures execution options, merges with defaults (resets each call).
156
+
157
+ - **run(payload?: string)**: `Promise<string>`
158
+ - Executes command asynchronously, returns stdout. Rejects on error/timeout/kill.
112
159
 
113
- - **options(options: SpawnOptions | SpawnSyncOptions, prefix?: string)**: Sets execution options.
114
- - **run(payload?: string)**: Executes the command asynchronously, returns Promise<string>.
115
- - **runSync(payload?: string)**: Executes synchronously, returns SpawnSyncReturns.
116
- - **kill(signal?: string)**: Kills the process.
160
+ - **runSync(payload?: string)**: `import('child_process').SpawnSyncReturns<any>`
161
+ - Executes synchronously.
162
+
163
+ - **kill(signal?: string)**: `Promise<number[]>`
164
+ - Kills the process (and children if possible).
165
+
166
+ **Defaults/Semantics:**
167
+ - cwd: `process.cwd()`
168
+ - env: `process.env`
169
+ - shell: `'bash'` (use shell if string/true; no shell if false/undefined, uses /usr/bin/env -S)
170
+ - stdio: `['inherit', 'pipe', 'pipe']`
171
+ - timeout: `0` (no timeout)
172
+ - Prefix (e.g., 'set -euo pipefail') applied only with shell.
173
+ - Buffering: Up to 40MiB per stream, truncates with marker.
174
+ - Payload: Writes to stdin (forces pipe).
117
175
 
118
176
  ### Test Class
119
177
 
120
- For running tests:
178
+ `new Test(quiet?: boolean)` - quiet: no auto-report.
179
+
180
+ - **syncTimeout(timeout: number)**: `void` - Timeout for sync tests (default 50ms, for async in sync).
181
+
182
+ - **add(description: string, callback: Function | AsyncFunction)**: `Test`
183
+ - Adds sync or async test. Throws if conditions fail.
184
+
185
+ - **run(execute?: number[])**: `Promise<Report>`
186
+ - Runs all or specified tests (by index).
121
187
 
122
- - **constructor(quiet?: boolean)**: Creates a test suite.
123
- - **syncTimeout(timeout: number)**: Sets timeout for sync tests.
124
- - **add(description: string, callback: Function | AsyncFunction)**: Adds a test.
125
- - **run(execute?: number[])**: Runs tests, returns Promise<Report>.
126
- - **reset()**: Clears tests.
188
+ - **unresolved()**: `void` - Checks/handles unresolved (internal?).
189
+
190
+ - **reset()**: `void` - Clears tests.
191
+
192
+ **Types:**
193
+ - `AsyncFunction = () => Promise<any>`
194
+ - `testDefinition = {description: string, callback: Function | AsyncFunction}`
195
+ - `testReport = {description: string, duration: number, executed: boolean}`
196
+ - `Report = {tests: number, duration: number, errors: number, executed: number}`
127
197
 
128
198
  ### AsyncTracker Class
129
199
 
130
- Tracks async operations:
200
+ `new AsyncTracker()`
201
+
202
+ - **enable(type?: SystemTypes)**: `void` - Starts tracking all or specific type.
203
+
204
+ - **disable()**: `void` - Stops tracking.
205
+
206
+ - **reset()**: `void` - Clears tracked items.
131
207
 
132
- - **enable(type?: SystemTypes)**: Enables tracking.
133
- - **disable()**: Disables tracking.
134
- - **reset()**: Clears tracked items.
135
- - **report(verbose?: boolean)**: Reports unresolved async operations.
136
- - **getUnresolved(type?: SystemTypes)**: Gets unresolved items.
137
- - **getTypeDescription(type: SystemTypes)**: Gets type description.
138
- - **addCustomType(type: string, description: string)**: Adds custom type.
208
+ - **report(verbose?: boolean)**: `number` - Logs unresolved, returns count.
139
209
 
140
- ### Types
210
+ - **getUnresolved(type?: SystemTypes)**: `AsyncHookItem[]` - Gets unresolved items.
141
211
 
142
- - **ArgsObject**: Object for parsed args, with `_` for unnamed.
143
- - **SpawnSyncResponse**: Result of sync spawn.
144
- - **SHOptions**: Options for SH execution.
145
- - **AsyncHookItem**: Item in async tracking.
146
- - **testDefinition, testReport, Report**: Types for test framework.
212
+ - **getTypeDescription(type: SystemTypes)**: `string` - Description of type.
213
+
214
+ - **addCustomType(type: string, description: string)**: `void` - Adds custom type desc.
215
+
216
+ **Types:**
217
+ - `AsyncHookItem = {key: number, type: string, triggerAsyncId: number, stack: string, resource: SystemTypes}`
218
+ - `SystemTypes` = "PROMISE" | "TIMEOUT" | ... (full Node async resource types)
219
+
220
+ ### Other Types
221
+
222
+ - `ArgsObject = {[x: string]: string}` (with `_`: string[] for unnamed)
223
+ - `SpawnSyncResponse = {status: number|null, signal: string|null, output: (string|Buffer|null)[], pid: number, stdout: string|Buffer|null, stderr: string|Buffer|null}`
224
+ - `SHOptions` extends `SpawnOptions` & `{maxBuffer?: number, input?: string|Uint8Array|Buffer}`
225
+ - `StdioOption = "pipe" | "ignore" | "inherit" | number`
147
226
 
148
227
  ## Dependencies
149
228
 
@@ -151,4 +230,3 @@ Tracks async operations:
151
230
 
152
231
  **Dev Dependencies:**
153
232
  - @types/node: ^22.10.10
154
-
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.25",
5
+ "version": "1.1.26",
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,7 +11,22 @@ 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
- export function SH(pieces: any, ...args: any[]): SHDispatch;
14
+ /**
15
+ * A template-tag that returns an SHDispatch.
16
+ * @typedef {(pieces: TemplateStringsArray, ...args: unknown[]) => SHDispatch} SHTag
17
+ */
18
+ /**
19
+ * SH template tag
20
+ *
21
+ * Interpolation rules:
22
+ * - 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 '\''.
23
+ * - Non-array values: currently coerced with String(value) and inserted as-is (NOT shell-escaped). Be careful when interpolating untrusted input.
24
+ *
25
+ * @type {Shell & SHTag & defaultOptions}
26
+ * Returns an SHDispatch that can be configured via .options() and executed with .run() / .runSync().
27
+ * @returns {SHDispatch}
28
+ */
29
+ export const SH: Shell & SHTag & import("./SHDispatch.js").SHOptions;
15
30
  /**
16
31
  * Change working directory
17
32
  * @param {string} dir
@@ -69,17 +69,34 @@ export type SHOptions = {
69
69
  };
70
70
  export type StdioOption = ("pipe" | "ignore" | "inherit" | number);
71
71
  export type StdioOptions = Array<StdioOption> | StdioOption;
72
+ /**
73
+ * SHOptions — effective defaults and semantics used by SHDispatch/SHExecute
74
+ *
75
+ * Defaults:
76
+ * - cwd: process.cwd()
77
+ * - env: process.env
78
+ * - shell: 'bash' (string). If a string, that shell is used. If true, 'bash' is used. If false/undefined, no shell is used.
79
+ * - stdio: ['inherit', 'pipe', 'pipe'] — inherit stdin, capture stdout/stderr
80
+ * - timeout: 0 — no timeout
81
+ * - maxBuffer?: number — optional (bytes per stream). Passed through to SHExecute.
82
+ *
83
+ * Notes:
84
+ * - The prefix (see options(prefix)) is only applied when a shell is used; it is ignored in no-shell mode.
85
+ * - Each call to options() resets to the defaults and merges the provided options; it does not accumulate from prior calls.
86
+ */
72
87
  declare class SHDispatch {
73
88
  /**
74
89
  * @param {string} cmd - cmd to execute
90
+ * @param {SHOptions} options
91
+ * @param {string} [prefix] - command prefix e.g (default) '/usr/bin/env'
75
92
  */
76
- constructor(cmd: string);
93
+ constructor(cmd: string, options: SHOptions, prefix?: string);
77
94
  /**
78
- * @param {import('child_process').SpawnOptions | import('child_process').SpawnSyncOptions} options
95
+ * @param {SHOptions} [options]
79
96
  * @param {string} [prefix] - command prefix e.g (default) '/usr/bin/env'
80
97
  * @returns {SHDispatch}
81
98
  */
82
- options(options: import("child_process").SpawnOptions | import("child_process").SpawnSyncOptions, prefix?: string): SHDispatch;
99
+ options(options?: SHOptions, prefix?: string): SHDispatch;
83
100
  /**
84
101
  * @param {string} [payload]
85
102
  * @returns {Promise<string>}