@j-o-r/sh 1.1.23 → 1.1.25
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 +20 -1
- package/lib/SHDispatch.js +1 -1
- package/lib/SHExecute.js +7 -9
- package/lib/Test.js +22 -11
- package/module.md +154 -0
- package/package.json +1 -1
- package/types/Test.d.ts +1 -0
package/README.md
CHANGED
|
@@ -64,11 +64,14 @@ The module also provides additional utilities for common tasks:
|
|
|
64
64
|
- `sleep(duration)`: Pause execution for a specified duration.
|
|
65
65
|
- `retry(count, interval, callback)`: Retry a command a specified number of times with an optional interval.
|
|
66
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.
|
|
67
68
|
- `within(callback)`: Create an async context in a sync block.
|
|
68
69
|
- `expBackoff(max, rand)`: Generate intervals for exponential backoff.
|
|
69
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).
|
|
70
72
|
- `assert.`: Node assert library
|
|
71
73
|
- `new Test()`: A small sync/async minimal test framework
|
|
74
|
+
- `AsyncTracker`: A class for tracking asynchronous operations using Node.js async hooks.
|
|
72
75
|
|
|
73
76
|
## SHDispatch
|
|
74
77
|
|
|
@@ -118,11 +121,27 @@ This class is returned by the `SH` function. Here's a summary of its methods and
|
|
|
118
121
|
- Retry with exponential backoff:
|
|
119
122
|
```javascript
|
|
120
123
|
try {
|
|
121
|
-
const p = await retry(3, expBackoff(), () => SH`curl -s https://
|
|
124
|
+
const p = await retry(3, expBackoff(), () => SH`curl -s https://unreachable`.run());
|
|
122
125
|
} catch (e) {
|
|
123
126
|
console.error('Retry failed:', e);
|
|
124
127
|
}
|
|
125
128
|
```
|
|
129
|
+
|
|
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
|
+
```
|
|
136
|
+
|
|
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
|
+
```
|
|
144
|
+
|
|
126
145
|
- Method for copying data to the clipboard:
|
|
127
146
|
```javascript
|
|
128
147
|
/**
|
package/lib/SHDispatch.js
CHANGED
|
@@ -114,7 +114,7 @@ class SHDispatch {
|
|
|
114
114
|
* @returns {SHDispatch}
|
|
115
115
|
*/
|
|
116
116
|
options(options, prefix) {
|
|
117
|
-
if (
|
|
117
|
+
if (typeof prefix === 'string') {
|
|
118
118
|
this.#prefix = prefix;
|
|
119
119
|
}
|
|
120
120
|
if (options.stdio && typeof options.stdio === 'string') {
|
package/lib/SHExecute.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawnSync, spawn} from 'node:child_process';
|
|
1
|
+
import { spawnSync, spawn } from 'node:child_process';
|
|
2
2
|
|
|
3
3
|
// Local helpers
|
|
4
4
|
const isFinitePosInt = (n) => Number.isFinite(n) && n >= 0;
|
|
@@ -19,15 +19,13 @@ const parseDuration = (d) => {
|
|
|
19
19
|
const childrenOf = (pid) => new Promise((resolve, reject) => {
|
|
20
20
|
const p = spawn('pgrep', ['-P', String(pid)], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
21
21
|
const out = [];
|
|
22
|
-
p.stdout.on('data', (
|
|
22
|
+
p.stdout.on('data', (chunk) => out.push(chunk));
|
|
23
23
|
p.on('close', (code) => {
|
|
24
24
|
if (code === 0) {
|
|
25
25
|
const ids = Buffer.concat(out).toString('utf8').trim().split(/\s+/).map(Number).filter(Boolean);
|
|
26
26
|
resolve(ids);
|
|
27
27
|
} else if (code === 1) {
|
|
28
|
-
resolve([]); // no children
|
|
29
|
-
} else {
|
|
30
|
-
reject(new Error(`pgrep -P ${pid} failed with code ${code}`));
|
|
28
|
+
resolve([]); // no children or error
|
|
31
29
|
}
|
|
32
30
|
});
|
|
33
31
|
p.on('error', reject);
|
|
@@ -103,7 +101,7 @@ class SHExecute {
|
|
|
103
101
|
const shellOpt = this.#options?.shell;
|
|
104
102
|
if (shellOpt) {
|
|
105
103
|
const sh = typeof shellOpt === 'string' ? shellOpt : 'bash';
|
|
106
|
-
const cmd = `${this.#prefix}; ${this.#command}
|
|
104
|
+
const cmd = this.#prefix ? `${this.#prefix}; ${this.#command}` : this.#command;
|
|
107
105
|
return spawnSync(sh, ['-c', cmd], options);
|
|
108
106
|
}
|
|
109
107
|
// no-shell mode
|
|
@@ -139,7 +137,7 @@ class SHExecute {
|
|
|
139
137
|
const shellOpt = this.#options?.shell;
|
|
140
138
|
if (shellOpt) {
|
|
141
139
|
const sh = typeof shellOpt === 'string' ? shellOpt : 'bash';
|
|
142
|
-
const cmd = `${this.#prefix}; ${this.#command}
|
|
140
|
+
const cmd = this.#prefix ? `${this.#prefix}; ${this.#command}` : this.#command;
|
|
143
141
|
this.#proc = spawn(sh, ['-c', cmd], options);
|
|
144
142
|
} else {
|
|
145
143
|
this.#proc = spawn('/usr/bin/env', ['-S', this.#command], options);
|
|
@@ -199,9 +197,9 @@ class SHExecute {
|
|
|
199
197
|
resolve(stdout);
|
|
200
198
|
} else {
|
|
201
199
|
const stderrBuf = this.#stderrChunks.length ? Buffer.concat(this.#stderrChunks) : Buffer.alloc(0);
|
|
202
|
-
let stderr = stderrBuf.toString('utf8')
|
|
200
|
+
let stderr = stderrBuf.toString('utf8');
|
|
203
201
|
if (this.#truncated.stderr) stderr += '\n[stderr truncated]\n';
|
|
204
|
-
reject(new Error(`
|
|
202
|
+
reject(new Error(`Command failed with code ${code}: ${stderr}`));
|
|
205
203
|
}
|
|
206
204
|
});
|
|
207
205
|
|
package/lib/Test.js
CHANGED
|
@@ -140,7 +140,9 @@ class Test {
|
|
|
140
140
|
const len = this.#tests.length;
|
|
141
141
|
for (; i < len; i++) {
|
|
142
142
|
this.#currentTest = i;
|
|
143
|
-
if (execute && !execute.includes(i))
|
|
143
|
+
if (execute && !execute.includes(i)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
144
146
|
let duration = 0;
|
|
145
147
|
let start = getNow();
|
|
146
148
|
let executed = false;
|
|
@@ -179,9 +181,9 @@ class Test {
|
|
|
179
181
|
return this.#report();
|
|
180
182
|
}
|
|
181
183
|
/**
|
|
182
|
-
* @returns {Report}
|
|
184
|
+
* @returns {Promise<Report>}
|
|
183
185
|
*/
|
|
184
|
-
#report() {
|
|
186
|
+
async #report() {
|
|
185
187
|
let duration = 0;
|
|
186
188
|
let executed = 0;
|
|
187
189
|
const tests = this.#tests.length;
|
|
@@ -203,21 +205,29 @@ class Test {
|
|
|
203
205
|
console.log(`Total: ${tests} tests, executed: ${executed} in ${duration} ms - errors: ${errors}`);
|
|
204
206
|
if (tests !== executed) {
|
|
205
207
|
if (!this.#quiet) console.log('** Not all tests have been executed **');
|
|
206
|
-
return { tests, executed, duration, errors };
|
|
208
|
+
// return { tests, executed, duration, errors };
|
|
207
209
|
}
|
|
208
210
|
// Report on unresolved promises when NOT quiet
|
|
209
211
|
// This must be on a next tick because the current Promise (where this code is in) is not resolved yet
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
});
|
|
212
|
+
setTimeout(() => {
|
|
213
|
+
const count = this.#promiseTracker.report(true);
|
|
214
|
+
if (count > 0) {
|
|
215
|
+
console.log('--------------------------------------------------');
|
|
216
|
+
console.log(`${count} Unresolved Promises detected.`);
|
|
217
|
+
}
|
|
218
|
+
}, 10);
|
|
217
219
|
}
|
|
218
220
|
return { tests, executed, duration, errors };
|
|
219
221
|
}
|
|
220
222
|
|
|
223
|
+
unresolved() {
|
|
224
|
+
const count = this.#promiseTracker.report(true);
|
|
225
|
+
if (count > 0) {
|
|
226
|
+
console.log('--------------------------------------------------');
|
|
227
|
+
console.log(`${count} Unresolved Promises detected.`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
221
231
|
/**
|
|
222
232
|
* Empty tests
|
|
223
233
|
*/
|
|
@@ -225,6 +235,7 @@ class Test {
|
|
|
225
235
|
this.#tests = [];
|
|
226
236
|
this.#reports = [];
|
|
227
237
|
this.#errors = [];
|
|
238
|
+
this.#promiseTracker.reset();
|
|
228
239
|
}
|
|
229
240
|
/**
|
|
230
241
|
* Handle an error for the current test
|
package/module.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# @j-o-r/sh Module Documentation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
**Name:** @j-o-r/sh
|
|
6
|
+
**Version:** 1.1.23
|
|
7
|
+
**Description:** Execute shell commands on Linux-based systems from javascript.
|
|
8
|
+
|
|
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.
|
|
10
|
+
|
|
11
|
+
**Repository:** https://codeberg.org/duin/sh
|
|
12
|
+
**License:** Apache License, Version 2.0
|
|
13
|
+
|
|
14
|
+
Key features include:
|
|
15
|
+
- Execute shell commands synchronously or asynchronously.
|
|
16
|
+
- Utilities for changing directory, sleeping, retrying commands with backoff.
|
|
17
|
+
- Parsing command-line arguments.
|
|
18
|
+
- User input prompts.
|
|
19
|
+
- A minimal test framework.
|
|
20
|
+
- Async operation tracking.
|
|
21
|
+
|
|
22
|
+
## Installation Notes
|
|
23
|
+
|
|
24
|
+
The module is already installed in the current environment. For new installations, use:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npm install @j-o-r/sh
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Requires Node.js >= 20.0.0.
|
|
31
|
+
|
|
32
|
+
## API Usage Examples
|
|
33
|
+
|
|
34
|
+
### Basic Usage
|
|
35
|
+
|
|
36
|
+
To execute a shell command, use the `SH` function:
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
import { SH } from '@j-o-r/sh';
|
|
40
|
+
|
|
41
|
+
SH`your_shell_command`.run()
|
|
42
|
+
.then(output => {
|
|
43
|
+
console.log('Output:', output);
|
|
44
|
+
})
|
|
45
|
+
.catch(error => {
|
|
46
|
+
console.error('Error:', error);
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Advanced Usage
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
import { SH, cd, within, sleep, retry, expBackoff } from '@j-o-r/sh';
|
|
54
|
+
|
|
55
|
+
const res = await SH`ls -FLa | grep package.json | wc -l`.run();
|
|
56
|
+
console.log(res);
|
|
57
|
+
|
|
58
|
+
const ar = within(async () => {
|
|
59
|
+
const res = await Promise.all([
|
|
60
|
+
SH`sleep 1; echo 1`.run(),
|
|
61
|
+
SH`sleep 2; echo 2`.run(),
|
|
62
|
+
sleep(2),
|
|
63
|
+
SH`sleep 3; echo 3`.run()
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const p = await retry(3, expBackoff(), () => SH`curl -s https://unreachable`.run());
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Additional Examples
|
|
71
|
+
|
|
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
|
+
```
|
|
78
|
+
|
|
79
|
+
- Create and run a test:
|
|
80
|
+
```javascript
|
|
81
|
+
import { assert, jsType, Test } from '@j-o-r/sh';
|
|
82
|
+
|
|
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
|
+
```
|
|
89
|
+
|
|
90
|
+
## Full API Reference
|
|
91
|
+
|
|
92
|
+
### Main Exports
|
|
93
|
+
|
|
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.
|
|
108
|
+
|
|
109
|
+
### SHDispatch Class
|
|
110
|
+
|
|
111
|
+
Returned by the `SH` template tag. Methods:
|
|
112
|
+
|
|
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.
|
|
117
|
+
|
|
118
|
+
### Test Class
|
|
119
|
+
|
|
120
|
+
For running tests:
|
|
121
|
+
|
|
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.
|
|
127
|
+
|
|
128
|
+
### AsyncTracker Class
|
|
129
|
+
|
|
130
|
+
Tracks async operations:
|
|
131
|
+
|
|
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.
|
|
139
|
+
|
|
140
|
+
### Types
|
|
141
|
+
|
|
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.
|
|
147
|
+
|
|
148
|
+
## Dependencies
|
|
149
|
+
|
|
150
|
+
**Runtime Dependencies:** None
|
|
151
|
+
|
|
152
|
+
**Dev Dependencies:**
|
|
153
|
+
- @types/node: ^22.10.10
|
|
154
|
+
|
package/package.json
CHANGED