@j-o-r/sh 1.1.28 → 1.1.31

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/README.md CHANGED
@@ -1,195 +1,189 @@
1
- # @j-o-r/sh
1
+ # @j-o-r/sh   [![npm](https://img.shields.io/npm/v/@j-o-r/sh?logo=npm)](https://www.npmjs.com/package/@j-o-r/sh) [![License](https://img.shields.io/npm/license/@j-o-r/sh)](https://codeberg.org/duin/sh/src/branch/main/LICENSE) [![Codeberg](https://img.shields.io/badge/Codeberg-main-blue?logo=codeberg)](https://codeberg.org/duin/sh)
2
2
 
3
- Execute shell commands from JavaScript.
3
+ Execute shell commands from JavaScript on Linux.
4
4
 
5
5
  ## Introduction
6
6
 
7
- `@j-o-r/sh` is a Node.js module that simplifies the execution of shell commands within JavaScript applications. It provides a range of utilities to handle shell scripts and manage their output efficiently.
7
+ `@j-o-r/sh` is a lightweight Node.js module for running shell commands with a clean, zx-inspired API. It fixes namespace pollution, adds **rolling timeouts** (reset on stdout/stderr), **no-shell mode** (`/usr/bin/env -S`), process tree kills, buffer limits (1MB default), and a built-in **Test** framework + **AsyncTracker** for leaks.
8
8
 
9
- This project draws inspiration from the exceptional [zx library](https://github.com/google/zx). The core functionality of zx, particularly the shell execution method, has been extracted and forms the foundation of this project.
9
+ **Key Features**:
10
+ - Template tag `SH`cmd`` → `SHDispatch` for `.options().run()`.
11
+ - Global options: `SH.timeout = '5s'; SH.cwd = '/tmp';`.
12
+ - Sync/async `run()`/`runSync()`; stdin payload; detached mode.
13
+ - Utils: `sleep`, `retry`, `expBackoff`, `cd`, `parseArgs`, `userIn`, `readIn`.
14
+ - Testing: `new Test().add('name', () => assert(...)).run()`.
15
+ - Async leak detection: `AsyncTracker` via `async_hooks`.
16
+ - Raw-by-default interpolation with explicit `bashEscape` quoting for untrusted argument values; full JSDoc.
10
17
 
11
- ## Installation
18
+ No runtime deps. ESM-only (ES2020+).
12
19
 
13
- Install the module using npm:
20
+ ## Quick Install
14
21
 
15
- ```sh
16
- npm install @j-o-r/sh
22
+ ```bash
23
+ npm i @j-o-r/sh
17
24
  ```
18
25
 
19
26
  ## Usage
20
27
 
21
- ### Basic Usage
28
+ ### Basics
22
29
 
23
- To execute a shell command, use the `SH` function:
30
+ ```js
31
+ import { SH, cd, sleep } from '@j-o-r/sh';
24
32
 
25
- ```javascript
26
- import { SH, cd, within, sleep, retry, expBackoff } from '@j-o-r/sh';
27
-
28
- SH`your_shell_command`.run()
29
- .then(output => {
30
- console.log('Output:', output);
31
- })
32
- .catch(error => {
33
- console.error('Error:', error);
34
- });
33
+ cd('/tmp');
34
+ const out = await SH`ls -la`.run();
35
+ console.log(out); // Captured stdout (trimmed)
35
36
  ```
36
37
 
37
- ### Advanced Usage
38
+ ### Interpolation: raw by default
38
39
 
39
- ```javascript
40
- const res = await SH`ls -FLa | grep package.json | wc -l`.run();
41
- console.log(res);
40
+ Interpolated values are inserted into the shell command as raw shell source. This
41
+ is convenient for trusted shell fragments such as flags, pipes, redirects, and
42
+ compound expressions, but it is **not safe for untrusted input**.
42
43
 
43
- const ar = within(async () => {
44
- const res = await Promise.all([
45
- SH`sleep 1; echo 1`.run(),
46
- SH`sleep 2; echo 2`.run(),
47
- sleep(2),
48
- SH`sleep 3; echo 3`.run()
49
- ]);
50
- });
51
- ```
44
+ ```js
45
+ import { SH, bashEscape } from '@j-o-r/sh';
46
+
47
+ const flags = ['-l', '-a'];
48
+ await SH`ls ${flags}`.run(); // Executes: ls -l -a
52
49
 
53
- ```javascript
54
- const p = await retry(3, expBackoff(), () => SH`curl -s https://unreachable`.run());
50
+ const fragment = 'printf ok; printf done';
51
+ await SH`${fragment}`.run(); // Raw shell syntax is intentional
52
+
53
+ const userInput = 'name; rm -rf /';
54
+ await SH`printf '%s\n' ${bashEscape(userInput)}`.run();
55
+ // Prints the value literally instead of executing `rm`.
55
56
  ```
56
57
 
57
- The `SH` method accepts a template literal string enclosed in backticks as its argument. It returns an `SHDispatch` object.
58
+ `bashEscape(value)` returns one POSIX-shell-quoted argument. It preserves empty
59
+ strings, whitespace, quotes, semicolons, glob characters, tabs, and newlines.
60
+ For arrays of untrusted values, quote each element yourself:
58
61
 
59
- ### Additional Utilities
62
+ ```js
63
+ const args = ['two words', 'semi;colon', "quote's"];
64
+ await SH`printf '%s\n' ${args.map(bashEscape)}`.run();
65
+ ```
60
66
 
61
- The module also provides additional utilities for common tasks:
62
- - `parseArgs(process.args)`: Transform an array of strings into an object
63
- - `cd(dir)`: Change the working directory.
64
- - `sleep(duration)`: Pause execution for a specified duration.
65
- - `retry(count, interval, callback)`: Retry a command a specified number of times with an optional interval.
66
- - `readIn()`: Read from standard input.
67
- - `userIn(prompt)`: Prompt the user for input and return an object with `input` (Promise resolving to the user input) and `abort()` method to cancel.
68
- - `within(callback)`: Create an async context in a sync block.
69
- - `expBackoff(max, rand)`: Generate intervals for exponential backoff.
70
- - `jsType(any)`: Get the 'real' javascript variable type
71
- - `hasProp(object, property)`: Safely check if an object has its own property (handles null/undefined objects).
72
- - `assert.`: Node assert library
73
- - `new Test()`: A small sync/async minimal test framework
74
- - `AsyncTracker`: A class for tracking asynchronous operations using Node.js async hooks.
67
+ ### Chaining & Options
75
68
 
76
- ## SHDispatch
69
+ ```js
70
+ SH`curl -s ip.js.org`
71
+ .options({ timeout: '2s', shell: false }) // No-shell: /usr/bin/env -S
72
+ .run()
73
+ .catch(e => console.error(e.message)); // "Command failed with code 1: ..."
74
+ ```
77
75
 
78
- This class is returned by the `SH` function. Here's a summary of its methods and properties:
76
+ ### Parallel & Context
79
77
 
80
- ### Methods
78
+ ```js
79
+ import { within } from '@j-o-r/sh';
81
80
 
82
- - **options(options)**: Sets options for the command execution.
83
- - **run(payload?)**: Executes the command and returns a promise that resolves with the command's output.
84
- - **runSync(payload?)**: Executes the command synchronously and returns a `SpawnSyncResponse`.
85
- - **kill()**: Sends a kill signal to the child process.
81
+ const results = await within(async () => Promise.all([
82
+ SH`sleep 1; echo ok`.run(),
83
+ sleep('500ms'),
84
+ SH`uname`.run()
85
+ ]));
86
+ ```
86
87
 
87
- ### Examples
88
+ ### Retry & Backoff
88
89
 
89
- - Elementary usages, piped:
90
- ```javascript
91
- const res = await SH`ls -FLa | grep package.json | wc -l`.run();
92
- console.log(res);
93
- ```
90
+ ```js
91
+ import { retry, expBackoff } from '@j-o-r/sh';
94
92
 
95
- - Feed command with content:
96
- ```javascript
97
- const res = await SH`wc -l`.run(`one\ntwo\n`);
98
- console.log(res);
99
- ```
93
+ try {
94
+ const res = await retry(3, expBackoff('10s'), () =>
95
+ SH`curl -s unreachable`.run()
96
+ );
97
+ } catch (e) {
98
+ // Last error
99
+ }
100
+ ```
100
101
 
101
- - Create a command from a string
102
- ```javascript
103
- const command = "uname -r";
104
- const content = await SH`${command}`.run();
105
- console.log(content);
106
- ```
102
+ ### Interactive / Stdin
107
103
 
108
- - Async context with multiple commands and sleep:
109
- ```javascript
110
- within(async () => {
111
- const res = await Promise.all([
112
- SH`sleep 1; echo 1`.run(),
113
- SH`sleep 2; echo 2`.run(),
114
- sleep(2),
115
- SH`sleep 3; echo 3`.run()
116
- ]);
117
- console.log(res);
118
- });
119
- ```
104
+ ```js
105
+ import { userIn, readIn } from '@j-o-r/sh';
120
106
 
121
- - Retry with exponential backoff:
122
- ```javascript
123
- try {
124
- const p = await retry(3, expBackoff(), () => SH`curl -s https://unreachable`.run());
125
- } catch (e) {
126
- console.error('Retry failed:', e);
127
- }
128
- ```
107
+ // User prompt (abortable)
108
+ const { input, abort } = userIn('Password: ');
109
+ const pw = await input;
129
110
 
130
- - Prompt user for input:
131
- ```javascript
132
- const user = userIn('Enter your name: ');
133
- const name = await user.input;
134
- console.log('Hello,', name);
135
- ```
111
+ // Piped stdin
112
+ const out = await SH`grep secret`.run(await readIn());
113
+ ```
136
114
 
137
- - Check if object has property:
138
- ```javascript
139
- const obj = { a: 1 };
140
- console.log(hasProp(obj, 'a')); // true
141
- console.log(hasProp(obj, 'b')); // false
142
- console.log(hasProp(null, 'a')); // false
143
- ```
115
+ ### Vim / TTY (Sync)
144
116
 
145
- - Method for copying data to the clipboard:
146
- ```javascript
147
- /**
148
- * Copy text to the clipboard
149
- * @param {string} text
150
- * @returns {Promise<string>}
151
- */
152
- const copyToClipboard = async (text) => {
153
- const prams = [
154
- '-selection',
155
- 'clipboard'
156
- ]
157
- return SH`xclip ${prams}`.options({stdio: 'inherit'}).run(text);
158
- }
159
- ```
117
+ ```js
118
+ SH`vim`.options({ stdio: 'inherit' }).runSync();
119
+ ```
160
120
 
161
- - Open the 'vim' editor
162
- ```javascript
163
- SH`vim`.options({stdio: 'inherit'}).runSync();
164
- ```
121
+ ## Testing
165
122
 
166
- - Create and run a test
167
- ```javascript
168
- import { assert, jsType, Test} from '@j-o-r/sh';
123
+ Full-featured tester with async support, error reporting, unresolved Promise detection.
169
124
 
170
- const test = new Test();
171
- test.add('Test is test in sync', () => {
172
- assert.strictEqual(jsType(test), 'Test');
173
- });
174
- test.add('Test an Array in sync', () => {
175
- assert.strictEqual(jsType([]), 'Array');
176
- });
177
- test.add('Test is test in async', async () => {
178
- assert.strictEqual(jsType(test), 'Test');
179
- });
180
- test.add('Test is test in async, returning a promise', async () => {
181
- return new Promise((resolve, _reject) => {
182
- assert.strictEqual(jsType(test), 'Test');
183
- resolve();
184
- });
125
+ ```js
126
+ import { Test, assert, jsType } from '@j-o-r/sh';
127
+
128
+ const t = new Test(true); // quiet: no console
129
+ t.add('sync assert', () => assert.strictEqual(1 + 1, 2));
130
+ t.add('async', async () => {
131
+ await sleep('100ms');
132
+ assert.strictEqual(jsType([]), 'Array');
185
133
  });
186
- const report = await test.run();
187
- if (report.errors > 0) {
188
- process.exit(1);
189
- }
190
- // await test.run([0,3]); // only run test 0 and 3
134
+ const report = await t.run();
135
+ console.log(report); // { tests: 2, executed: 2, duration: 150, errors: 0 }
136
+
137
+ t.unresolved(); // Logs leaks if any
138
+ ```
139
+
140
+ ## Advanced
141
+
142
+ - **SSH Example** (interactive; use keys/sshpass for automation):
143
+ ```js
144
+ // TTY/inherit for prompts
145
+ SH`ssh user@host`.options({ stdio: 'inherit' }).runSync();
146
+ // Or sshpass: SH`sshpass -p pw ssh user@host`.run()
147
+ ```
148
+ See `scenarios/` for full demos.
149
+
150
+ - **CLI Args**: `parseArgs()` → `{ port: '8080', _: ['file'] }`.
151
+ - **Global Defaults**: Set global options on the `SH` object to apply to all subsequent commands. These can be overridden per-command via `.options()`. Examples:
152
+ - `SH.timeout = '5s';` – Default timeout for all runs.
153
+ - `SH.cwd = '/tmp';` – Default working directory.
154
+ - `SH.shell = false;` – Disable shell mode (uses `/usr/bin/env -S`).
155
+ - `SH.maxBuffer = 1 * 1024 * 1024;` – Override default buffer limit (500kb) to 1MB per stream (stdout/stderr). Buffering captures output up to this limit; excess is truncated with markers.
156
+ - **Kill Tree**: `dispatch.kill('SIGKILL')` → children via `pgrep -P`.
157
+
158
+ ## API
159
+
160
+ Full JSDoc in `lib/*.js`. Key exports:
161
+
162
+ | Utility | Description |
163
+ |---------|-------------|
164
+ | `SH`cmd`` | Template → {@link SHDispatch} |
165
+ | `cd(dir)` | `process.chdir()` |
166
+ | `sleep('1s')` | Promise delay |
167
+ | `retry(3, '1s', fn)` | Retry w/ delay/gen |
168
+ | `userIn(prompt)` | `{ input: Promise, abort() }` |
169
+ | `Test` | Test runner |
170
+ | `AsyncTracker` | Async leak detector |
171
+ | `parseArgs(argv)` | CLI parser |
172
+ | `bashEscape(value)` | Quote one value as a POSIX shell argument for safe interpolation |
173
+
174
+ See [types/SH.d.ts](types/SH.d.ts) for TS defs.
175
+
176
+ ## Development
177
+
178
+ ```bash
179
+ npm run types # Generate types/
180
+ npm test # Run scenarios/sh.js
181
+ npm run release # Pack for publish
182
+ npm run publish # npm publish
191
183
  ```
192
184
 
185
+ Repo: [Codeberg](https://codeberg.org/duin/sh) | Issues: [Codeberg Issues](https://codeberg.org/duin/sh/issues)
186
+
193
187
  ## License
194
188
 
195
- This project is licensed under the Apache License, Version 2.0.
189
+ Apache-2.0 © [Jorrit Duin](mailto:jorrit.duin+sh@gmail.com)
package/TODO.md ADDED
@@ -0,0 +1,8 @@
1
+ # TODOs for @j-o-r/sh project
2
+
3
+ ## lib/SH.js review findings (2026-06-26)
4
+
5
+ ## In Progress
6
+
7
+ ## Done
8
+