@j-o-r/sh 1.1.28 → 1.1.29
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 +119 -154
- package/TODO.md +18 -0
- package/lib/AsyncTracker.js +126 -107
- package/lib/SH.js +158 -154
- package/lib/SHDispatch.js +105 -91
- package/lib/SHExecute.js +83 -41
- package/lib/Test.js +153 -123
- package/package.json +1 -1
- package/types/AsyncTracker.d.ts +65 -31
- package/types/SH.d.ts +157 -164
- package/types/SHDispatch.d.ts +55 -78
- package/types/SHExecute.d.ts +44 -23
- package/types/Test.d.ts +66 -30
package/README.md
CHANGED
|
@@ -1,195 +1,160 @@
|
|
|
1
|
-
# @j-o-r/sh
|
|
1
|
+
# @j-o-r/sh [](https://www.npmjs.com/package/@j-o-r/sh) [](https://codeberg.org/duin/sh/src/branch/main/LICENSE) [](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
|
|
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
|
-
|
|
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
|
+
- Safe interpolation; `bashEscape`; full JSDoc.
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
No runtime deps. ESM-only (ES2020+).
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
## Quick Install
|
|
14
21
|
|
|
15
|
-
```
|
|
16
|
-
npm
|
|
22
|
+
```bash
|
|
23
|
+
npm i @j-o-r/sh
|
|
17
24
|
```
|
|
18
25
|
|
|
19
26
|
## Usage
|
|
20
27
|
|
|
21
|
-
###
|
|
28
|
+
### Basics
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+
```js
|
|
31
|
+
import { SH, cd, sleep } from '@j-o-r/sh';
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
###
|
|
38
|
-
|
|
39
|
-
```javascript
|
|
40
|
-
const res = await SH`ls -FLa | grep package.json | wc -l`.run();
|
|
41
|
-
console.log(res);
|
|
38
|
+
### Chaining & Options
|
|
42
39
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
SH`sleep 3; echo 3`.run()
|
|
49
|
-
]);
|
|
50
|
-
});
|
|
40
|
+
```js
|
|
41
|
+
SH`curl -s ip.js.org`
|
|
42
|
+
.options({ timeout: '2s', shell: false }) // No-shell: /usr/bin/env -S
|
|
43
|
+
.run()
|
|
44
|
+
.catch(e => console.error(e.message)); // "Command failed with code 1: ..."
|
|
51
45
|
```
|
|
52
46
|
|
|
53
|
-
|
|
54
|
-
const p = await retry(3, expBackoff(), () => SH`curl -s https://unreachable`.run());
|
|
55
|
-
```
|
|
47
|
+
### Parallel & Context
|
|
56
48
|
|
|
57
|
-
|
|
49
|
+
```js
|
|
50
|
+
import { within } from '@j-o-r/sh';
|
|
58
51
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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.
|
|
52
|
+
const results = await within(async () => Promise.all([
|
|
53
|
+
SH`sleep 1; echo ok`.run(),
|
|
54
|
+
sleep('500ms'),
|
|
55
|
+
SH`uname`.run()
|
|
56
|
+
]));
|
|
57
|
+
```
|
|
75
58
|
|
|
76
|
-
|
|
59
|
+
### Retry & Backoff
|
|
77
60
|
|
|
78
|
-
|
|
61
|
+
```js
|
|
62
|
+
import { retry, expBackoff } from '@j-o-r/sh';
|
|
79
63
|
|
|
80
|
-
|
|
64
|
+
try {
|
|
65
|
+
const res = await retry(3, expBackoff('10s'), () =>
|
|
66
|
+
SH`curl -s unreachable`.run()
|
|
67
|
+
);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Last error
|
|
70
|
+
}
|
|
71
|
+
```
|
|
81
72
|
|
|
82
|
-
|
|
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.
|
|
73
|
+
### Interactive / Stdin
|
|
86
74
|
|
|
87
|
-
|
|
75
|
+
```js
|
|
76
|
+
import { userIn, readIn } from '@j-o-r/sh';
|
|
88
77
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
console.log(res);
|
|
93
|
-
```
|
|
78
|
+
// User prompt (abortable)
|
|
79
|
+
const { input, abort } = userIn('Password: ');
|
|
80
|
+
const pw = await input;
|
|
94
81
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
console.log(res);
|
|
99
|
-
```
|
|
82
|
+
// Piped stdin
|
|
83
|
+
const out = await SH`grep secret`.run(await readIn());
|
|
84
|
+
```
|
|
100
85
|
|
|
101
|
-
|
|
102
|
-
```javascript
|
|
103
|
-
const command = "uname -r";
|
|
104
|
-
const content = await SH`${command}`.run();
|
|
105
|
-
console.log(content);
|
|
106
|
-
```
|
|
86
|
+
### Vim / TTY (Sync)
|
|
107
87
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
```
|
|
88
|
+
```js
|
|
89
|
+
SH`vim`.options({ stdio: 'inherit' }).runSync();
|
|
90
|
+
```
|
|
120
91
|
|
|
121
|
-
|
|
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
|
-
```
|
|
92
|
+
## Testing
|
|
129
93
|
|
|
130
|
-
-
|
|
131
|
-
```javascript
|
|
132
|
-
const user = userIn('Enter your name: ');
|
|
133
|
-
const name = await user.input;
|
|
134
|
-
console.log('Hello,', name);
|
|
135
|
-
```
|
|
94
|
+
Full-featured tester with async support, error reporting, unresolved Promise detection.
|
|
136
95
|
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
```
|
|
96
|
+
```js
|
|
97
|
+
import { Test, assert, jsType } from '@j-o-r/sh';
|
|
144
98
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const prams = [
|
|
154
|
-
'-selection',
|
|
155
|
-
'clipboard'
|
|
156
|
-
]
|
|
157
|
-
return SH`xclip ${prams}`.options({stdio: 'inherit'}).run(text);
|
|
158
|
-
}
|
|
159
|
-
```
|
|
99
|
+
const t = new Test(true); // quiet: no console
|
|
100
|
+
t.add('sync assert', () => assert.strictEqual(1 + 1, 2));
|
|
101
|
+
t.add('async', async () => {
|
|
102
|
+
await sleep('100ms');
|
|
103
|
+
assert.strictEqual(jsType([]), 'Array');
|
|
104
|
+
});
|
|
105
|
+
const report = await t.run();
|
|
106
|
+
console.log(report); // { tests: 2, executed: 2, duration: 150, errors: 0 }
|
|
160
107
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
SH`vim`.options({stdio: 'inherit'}).runSync();
|
|
164
|
-
```
|
|
108
|
+
t.unresolved(); // Logs leaks if any
|
|
109
|
+
```
|
|
165
110
|
|
|
166
|
-
|
|
167
|
-
```javascript
|
|
168
|
-
import { assert, jsType, Test} from '@j-o-r/sh';
|
|
111
|
+
## Advanced
|
|
169
112
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
113
|
+
- **SSH Example** (interactive; use keys/sshpass for automation):
|
|
114
|
+
```js
|
|
115
|
+
// TTY/inherit for prompts
|
|
116
|
+
SH`ssh user@host`.options({ stdio: 'inherit' }).runSync();
|
|
117
|
+
// Or sshpass: SH`sshpass -p pw ssh user@host`.run()
|
|
118
|
+
```
|
|
119
|
+
See `scenarios/` for full demos.
|
|
120
|
+
|
|
121
|
+
- **CLI Args**: `parseArgs()` → `{ port: '8080', _: ['file'] }`.
|
|
122
|
+
- **Global Defaults**: Set global options on the `SH` object to apply to all subsequent commands. These can be overridden per-command via `.options()`. Examples:
|
|
123
|
+
- `SH.timeout = '5s';` – Default timeout for all runs.
|
|
124
|
+
- `SH.cwd = '/tmp';` – Default working directory.
|
|
125
|
+
- `SH.shell = false;` – Disable shell mode (uses `/usr/bin/env -S`).
|
|
126
|
+
- `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.
|
|
127
|
+
- **Kill Tree**: `dispatch.kill('SIGKILL')` → children via `pgrep -P`.
|
|
128
|
+
|
|
129
|
+
## API
|
|
130
|
+
|
|
131
|
+
Full JSDoc in `lib/*.js`. Key exports:
|
|
132
|
+
|
|
133
|
+
| Utility | Description |
|
|
134
|
+
|---------|-------------|
|
|
135
|
+
| `SH`cmd`` | Template → {@link SHDispatch} |
|
|
136
|
+
| `cd(dir)` | `process.chdir()` |
|
|
137
|
+
| `sleep('1s')` | Promise delay |
|
|
138
|
+
| `retry(3, '1s', fn)` | Retry w/ delay/gen |
|
|
139
|
+
| `userIn(prompt)` | `{ input: Promise, abort() }` |
|
|
140
|
+
| `Test` | Test runner |
|
|
141
|
+
| `AsyncTracker` | Async leak detector |
|
|
142
|
+
| `parseArgs(argv)` | CLI parser |
|
|
143
|
+
| `bashEscape(str)` | Shell-safe string |
|
|
144
|
+
|
|
145
|
+
See [types/index.d.ts](types/index.d.ts) for TS defs.
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npm run types # Generate types/
|
|
151
|
+
npm test # Run scenarios/sh.js
|
|
152
|
+
npm run release # Pack for publish
|
|
153
|
+
npm run publish # npm publish
|
|
191
154
|
```
|
|
192
155
|
|
|
156
|
+
Repo: [Codeberg](https://codeberg.org/duin/sh) | Issues: [Codeberg Issues](https://codeberg.org/duin/sh/issues)
|
|
157
|
+
|
|
193
158
|
## License
|
|
194
159
|
|
|
195
|
-
|
|
160
|
+
Apache-2.0 © [Jorrit Duin](mailto:jorrit.duin+sh@gmail.com)
|
package/TODO.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# TODOs for @j-o-r/sh project
|
|
2
|
+
|
|
3
|
+
## High Priority (pending)
|
|
4
|
+
- [ ] Review and commit recent changes to lib/ files and types/ (from git status: modified AsyncTracker.js, SH.js, etc.)
|
|
5
|
+
- [*] Generate or improve API documentation using types/*.d.ts files
|
|
6
|
+
|
|
7
|
+
## In Progress
|
|
8
|
+
- [ ]
|
|
9
|
+
|
|
10
|
+
## Later (future tasks)
|
|
11
|
+
- [ ] Create separate docs folder with full API reference
|
|
12
|
+
|
|
13
|
+
## Done
|
|
14
|
+
- [x] Initial project setup from package.json and README.md inspection (2026-04-15)
|
|
15
|
+
- [x] Update README.md to include documentation for recent changes in AsyncTracker and Test features (2026-04-15)
|
|
16
|
+
- [x] Add more examples to README.md for SHDispatch methods and utilities like retry and expBackoff (2026-04-15)
|
|
17
|
+
- [x] Reference general SSH example in README.md (demo-interactive-ssh.js not found in scenarios/) (2026-04-15)
|
|
18
|
+
- [x] Reduce SHExecute maxBuffer default from 40MB to 1MB, update all docs/JSDoc mentioning it. Make sure global SH.maxBuffer works. (2026-04-15)
|