dollar-shell 1.1.13 → 1.2.0
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/LICENSE +1 -1
- package/README.md +37 -1
- package/llms-full.txt +32 -1
- package/llms.txt +9 -1
- package/package.json +16 -14
- package/src/bq-shell.js +3 -2
- package/src/bq-spawn.js +3 -2
- package/src/build.js +113 -0
- package/src/index.d.ts +2 -2
- package/src/index.js +49 -93
- package/src/node/index.d.ts +172 -0
- package/src/node/index.js +32 -0
- package/src/spawn/bun.js +26 -1
- package/src/spawn/node.js +29 -7
- package/src/utils.js +5 -1
- package/AGENTS.md +0 -97
- package/CLAUDE.md +0 -3
- package/CONTRIBUTING.md +0 -34
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -219,6 +219,38 @@ the spawn options with the following properties:
|
|
|
219
219
|
|
|
220
220
|
The rest is identical to `$`: `$sh`, `$sh.from`, `$sh.to` and `$sh.io`/`$sh.through`.
|
|
221
221
|
|
|
222
|
+
## Forcing the Node backend
|
|
223
|
+
|
|
224
|
+
Each runtime uses its own backend by default (`node:child_process` on Node, `Bun.spawn` on Bun,
|
|
225
|
+
`Deno.Command` on Deno). Set the **`DSH_FORCE_NODE` environment variable** (e.g. `DSH_FORCE_NODE=1`) to
|
|
226
|
+
force every runtime onto the Node backend — Bun and Deno then run on their `node:child_process`
|
|
227
|
+
compatibility layer. This is handy for sidestepping runtime-specific quirks (e.g. Bun intermittently
|
|
228
|
+
dropping the last chunk of a child's piped output).
|
|
229
|
+
|
|
230
|
+
Because dollar-shell spawns children with `env` defaulting to `process.env`, the variable is inherited by
|
|
231
|
+
those children — so it forces the Node backend across the whole process tree. To force **only the
|
|
232
|
+
current process** (no leak to spawned children), set `globalThis.DSH_FORCE_NODE = true` before importing
|
|
233
|
+
instead; it's process-local, but requires a dynamic `import()` (the backend is chosen once, at import time).
|
|
234
|
+
See the [Cross-runtime notes](https://github.com/uhop/dollar-shell/wiki/Cross-runtime-notes) for details.
|
|
235
|
+
|
|
236
|
+
## Node streams (`dollar-shell/node`)
|
|
237
|
+
|
|
238
|
+
The default entry exposes [web streams](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) on
|
|
239
|
+
`stdin`/`stdout`/`stderr`. If you'd rather work with Node streams — to pipe straight into `fs`/`zlib`/etc.
|
|
240
|
+
with no Web↔Node adapter, or to skip the conversion — import from `dollar-shell/node` instead:
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
import {spawn} from 'dollar-shell/node';
|
|
244
|
+
|
|
245
|
+
const sp = spawn(['cat', 'file.txt'], {stdout: 'pipe'});
|
|
246
|
+
sp.stdout.pipe(process.stdout); // sp.stdout is a Node Readable
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The API is identical to the main entry — same `$`, `$$`, `$sh`, `shell`, helpers, and
|
|
250
|
+
`.from`/`.to`/`.through`/`.io` — only the stream types differ (`stdin` is a Node `Writable`, `stdout`/`stderr`
|
|
251
|
+
are Node `Readable`s, and `asDuplex` / `.io` / `.through` return a Node `Duplex`). It always uses the Node backend, so it also runs on Bun and Deno through their
|
|
252
|
+
`node:child_process` compatibility layer.
|
|
253
|
+
|
|
222
254
|
## For AI Agents
|
|
223
255
|
|
|
224
256
|
This package ships with files to help AI coding agents and LLMs find, understand, and use it:
|
|
@@ -229,7 +261,7 @@ This package ships with files to help AI coding agents and LLMs find, understand
|
|
|
229
261
|
- **[llms.txt](./llms.txt)** — Concise project overview following the [llms.txt standard](https://llmstxt.org/).
|
|
230
262
|
- **[llms-full.txt](./llms-full.txt)** — Self-contained complete API reference (no external links needed).
|
|
231
263
|
|
|
232
|
-
|
|
264
|
+
The machine-readable `llms.txt` and `llms-full.txt` ship inside the npm package, so AI tools can read them straight from `node_modules`. `AGENTS.md` and `CLAUDE.md` are authoring-side docs kept in the repository.
|
|
233
265
|
|
|
234
266
|
## License
|
|
235
267
|
|
|
@@ -237,6 +269,8 @@ BSD-3-Clause
|
|
|
237
269
|
|
|
238
270
|
## Release History
|
|
239
271
|
|
|
272
|
+
- 1.2.0 _Added `dollar-shell/node` with Node streams and a `DSH_FORCE_NODE` flag to force the Node backend on any runtime._
|
|
273
|
+
- 1.1.14 _Fixed Bun stdin abort path, added js-check, Bun + Deno wired into CI._
|
|
240
274
|
- 1.1.13 _Updated dev dependencies._
|
|
241
275
|
- 1.1.12 _Consolidated TypeScript tests into `tests/`, removed `ts-check/`, added CJS test, improved test coverage and documentation._
|
|
242
276
|
- 1.1.11 _Updated dev dependencies._
|
|
@@ -257,3 +291,5 @@ BSD-3-Clause
|
|
|
257
291
|
- 1.0.2 _Technical release: fixed references in the package file._
|
|
258
292
|
- 1.0.1 _Technical release: more tests, better documentation._
|
|
259
293
|
- 1.0.0 _The initial release._
|
|
294
|
+
|
|
295
|
+
The full release notes are in the wiki: [Release notes](https://github.com/uhop/dollar-shell/wiki/Release-notes).
|
package/llms-full.txt
CHANGED
|
@@ -13,7 +13,7 @@ npm i --save dollar-shell
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
15
|
```js
|
|
16
|
-
import $,
|
|
16
|
+
import {$, $$, $sh, shell, sh, spawn} from 'dollar-shell';
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## spawn()
|
|
@@ -315,3 +315,34 @@ Full TypeScript declarations are provided in `src/index.d.ts`. The package uses
|
|
|
315
315
|
## Platform Support
|
|
316
316
|
|
|
317
317
|
Works on Node.js, Deno, and Bun. The appropriate spawn implementation is selected automatically at import time. All streams use the web streams API (ReadableStream/WritableStream).
|
|
318
|
+
|
|
319
|
+
### Forcing the Node backend
|
|
320
|
+
|
|
321
|
+
By default each runtime uses its own backend (`node:child_process` on Node, `Bun.spawn` on Bun, `Deno.Command` on Deno). Force every runtime onto the Node backend — so Bun and Deno run on their `node:child_process` compatibility layer — with the **`DSH_FORCE_NODE` environment variable** (any value except `''`, `0`, `false`):
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
DSH_FORCE_NODE=1 node app.js
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
dollar-shell's `env` option defaults to `process.env`, so spawned children inherit the variable — the environment variable therefore forces the Node backend across the **whole process tree** (this process and every dollar-shell child it spawns), which is usually what you want for a build script or wrapper.
|
|
328
|
+
|
|
329
|
+
To force **only the current process** without leaking into the children it spawns, set the process-local flag instead. It requires a dynamic `import()`, because the backend is chosen once, when the module first loads:
|
|
330
|
+
|
|
331
|
+
```js
|
|
332
|
+
globalThis.DSH_FORCE_NODE = true;
|
|
333
|
+
const {$} = await import('dollar-shell');
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
(Or keep the environment variable and pass an explicit `env` to the children you don't want affected.)
|
|
337
|
+
|
|
338
|
+
This is useful to sidestep runtime-specific quirks (e.g. Bun intermittently dropping the final chunk of a child's piped Web-Stream output; the Node backend delivers it reliably). On Deno, **reading** the env var needs `--allow-env` (the read is permission-guarded, so a plain import never prompts) and **writing** `process.env` in-process needs it too, whereas the `globalThis` flag needs no permission. Forcing the Node backend on Deno makes `stdout` a regular (non-byte) ReadableStream, so BYOB readers are unavailable there.
|
|
339
|
+
|
|
340
|
+
### Node streams (`dollar-shell/node`)
|
|
341
|
+
|
|
342
|
+
The package also ships a Node-streams facade at `dollar-shell/node`:
|
|
343
|
+
|
|
344
|
+
```js
|
|
345
|
+
import {$, $$, $sh, shell, sh, spawn} from 'dollar-shell/node';
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
The API is identical to the main entry, except the streams are Node streams (from `node:stream`) instead of Web streams: `stdin`/`stdout`/`stderr` are a Node `Writable`/`Readable`, and `.io`/`.through`/`asDuplex` return a Node `Duplex` (so a process drops straight into a `.pipe()` chain or `stream.pipeline()`). This is convenient for direct Node-ecosystem interop — pipe straight into `fs`/`zlib`/etc. with no Web↔Node adapter, and skip the conversion. It always uses the Node backend, so on Bun and Deno it runs through their `node:child_process` compatibility layer. Type declarations live in `src/node/index.d.ts`.
|
package/llms.txt
CHANGED
|
@@ -37,7 +37,7 @@ Utilities: `raw()`, `winCmdEscape()`, `isWindows`, `cwd()`, `currentExecPath()`,
|
|
|
37
37
|
## Common patterns
|
|
38
38
|
|
|
39
39
|
```js
|
|
40
|
-
import $,
|
|
40
|
+
import {$, $$, $sh, raw} from 'dollar-shell';
|
|
41
41
|
|
|
42
42
|
// Simple command
|
|
43
43
|
const result = await $`echo hello`;
|
|
@@ -63,6 +63,14 @@ $.from`ls -l .`
|
|
|
63
63
|
await $sh({stdout: 'inherit'})`echo ${raw('"hello"')}`;
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
## Forcing the Node backend
|
|
67
|
+
|
|
68
|
+
By default each runtime uses its native backend. Set the `DSH_FORCE_NODE` environment variable (e.g. `DSH_FORCE_NODE=1`) to force the Node backend on every runtime (Bun and Deno then run on their `node:child_process` compat). Because dollar-shell's `env` option defaults to `process.env`, spawned children inherit the variable, so it forces the whole process tree; to force only the current process (no leak to children), set `globalThis.DSH_FORCE_NODE = true` before a dynamic `import()` instead (process-local). Useful to sidestep runtime-specific quirks. The backend is selected once, at import time. Treated as off: unset, `''`, `0`, `false`.
|
|
69
|
+
|
|
70
|
+
## Node streams (`dollar-shell/node`)
|
|
71
|
+
|
|
72
|
+
`import {$, $$, $sh, shell, spawn} from 'dollar-shell/node'` gives the identical API, but `stdin`/`stdout`/`stderr` are Node `Readable`/`Writable` (and `.io`/`.through`/`asDuplex` a Node `Duplex`) instead of Web streams — for direct Node-ecosystem piping with no adapter. Always uses the Node backend (runs on Bun/Deno via their `node:child_process` compat).
|
|
73
|
+
|
|
66
74
|
## Docs
|
|
67
75
|
|
|
68
76
|
- [$ (dollar)](https://github.com/uhop/dollar-shell/wiki/$): Spawn processes with simple return values, includes $.from, $.to, $.io/$.through
|
package/package.json
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dollar-shell",
|
|
3
|
-
"description": "Run shell commands
|
|
4
|
-
"version": "1.
|
|
3
|
+
"description": "Run OS and shell commands using template tag functions. Same API in Node, Deno, and Bun. Web streams, TypeScript typings, zero dependencies.",
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./src/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
|
-
".":
|
|
10
|
-
|
|
11
|
-
"default": "./src/index.js"
|
|
12
|
-
},
|
|
9
|
+
".": "./src/index.js",
|
|
10
|
+
"./node": "./src/node/index.js",
|
|
13
11
|
"./*": "./src/*"
|
|
14
12
|
},
|
|
15
13
|
"scripts": {
|
|
@@ -20,6 +18,7 @@
|
|
|
20
18
|
"test:seq:bun": "bun run `tape6-seq --self` --flags FO",
|
|
21
19
|
"test:seq:deno": "deno run -A `tape6-seq --self` --flags FO",
|
|
22
20
|
"ts-check": "tsc --noEmit",
|
|
21
|
+
"js-check": "tsc --project tsconfig.check.json",
|
|
23
22
|
"ts-test": "tape6 --flags FO 'tests/test-*.*ts'",
|
|
24
23
|
"ts-test:bun": "tape6-bun --flags FO 'tests/test-*.*ts'",
|
|
25
24
|
"ts-test:deno": "tape6-deno --flags FO 'tests/test-*.*ts'",
|
|
@@ -30,9 +29,6 @@
|
|
|
30
29
|
"/src",
|
|
31
30
|
"LICENSE",
|
|
32
31
|
"README.md",
|
|
33
|
-
"AGENTS.md",
|
|
34
|
-
"CLAUDE.md",
|
|
35
|
-
"CONTRIBUTING.md",
|
|
36
32
|
"llms.txt",
|
|
37
33
|
"llms-full.txt"
|
|
38
34
|
],
|
|
@@ -49,18 +45,18 @@
|
|
|
49
45
|
"keywords": [
|
|
50
46
|
"shell",
|
|
51
47
|
"spawn",
|
|
52
|
-
"
|
|
48
|
+
"subprocess",
|
|
53
49
|
"dollar",
|
|
54
50
|
"stream",
|
|
55
51
|
"process",
|
|
56
52
|
"exec",
|
|
57
53
|
"command",
|
|
58
54
|
"web-streams",
|
|
55
|
+
"node-streams",
|
|
59
56
|
"template-tag",
|
|
60
57
|
"pipeline",
|
|
61
58
|
"typescript",
|
|
62
59
|
"esm",
|
|
63
|
-
"es-modules",
|
|
64
60
|
"cross-runtime",
|
|
65
61
|
"zero-dependency",
|
|
66
62
|
"nodejs",
|
|
@@ -73,10 +69,16 @@
|
|
|
73
69
|
"url": "https://github.com/sponsors/uhop"
|
|
74
70
|
},
|
|
75
71
|
"license": "BSD-3-Clause",
|
|
72
|
+
"engines": {
|
|
73
|
+
"node": ">=22"
|
|
74
|
+
},
|
|
76
75
|
"devDependencies": {
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
76
|
+
"@types/bun": "^1.3.14",
|
|
77
|
+
"@types/deno": "^2.7.0",
|
|
78
|
+
"@types/node": "^25.9.1",
|
|
79
|
+
"prettier": "^3.8.3",
|
|
80
|
+
"tape-six": "^1.10.0",
|
|
81
|
+
"typescript": "^6.0.3"
|
|
80
82
|
},
|
|
81
83
|
"tape6": {
|
|
82
84
|
"tests": [
|
package/src/bq-shell.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {isRawValue, getRawValue, verifyStrings} from './utils.js';
|
|
1
|
+
import {isRawValue, getRawValue, verifyStrings, bqTagSymbol, isBqTag} from './utils.js';
|
|
2
2
|
|
|
3
3
|
const impl =
|
|
4
4
|
(shellEscape, shell, options) =>
|
|
@@ -28,10 +28,11 @@ const bqShell = (shellEscape, shell, options = {}) => {
|
|
|
28
28
|
if (verifyStrings(strings)) return impl(shellEscape, shell, options)(strings, ...args);
|
|
29
29
|
const derived = bqShell(shellEscape, shell, {...options, ...strings});
|
|
30
30
|
for (const [key, value] of Object.entries(bq)) {
|
|
31
|
-
derived[key] =
|
|
31
|
+
derived[key] = isBqTag(value) ? value(strings) : value;
|
|
32
32
|
}
|
|
33
33
|
return derived;
|
|
34
34
|
};
|
|
35
|
+
/** @type {any} */ (bq)[bqTagSymbol] = true;
|
|
35
36
|
return bq;
|
|
36
37
|
};
|
|
37
38
|
|
package/src/bq-spawn.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {verifyStrings, isRawValue, getRawValue} from './utils.js';
|
|
1
|
+
import {verifyStrings, isRawValue, getRawValue, bqTagSymbol, isBqTag} from './utils.js';
|
|
2
2
|
|
|
3
3
|
const appendString = (s, previousSpace, result) => {
|
|
4
4
|
previousSpace ||= /^\s/.test(s);
|
|
@@ -69,10 +69,11 @@ const bqSpawn = (spawn, options = {}) => {
|
|
|
69
69
|
if (verifyStrings(strings)) return impl(spawn, options)(strings, ...args);
|
|
70
70
|
const derived = bqSpawn(spawn, {...options, ...strings});
|
|
71
71
|
for (const [key, value] of Object.entries(bq)) {
|
|
72
|
-
derived[key] =
|
|
72
|
+
derived[key] = isBqTag(value) ? value(strings) : value;
|
|
73
73
|
}
|
|
74
74
|
return derived;
|
|
75
75
|
};
|
|
76
|
+
/** @type {any} */ (bq)[bqTagSymbol] = true;
|
|
76
77
|
return bq;
|
|
77
78
|
};
|
|
78
79
|
|
package/src/build.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Shared API builder. Given a spawn backend (`spawn`, `cwd`, `currentExecPath`,
|
|
2
|
+
// `runFileArgs`), wires up the shell helpers and all tag functions ($, $$, $sh,
|
|
3
|
+
// shell, with from/to/through/io). Both entry points reuse this — `index.js`
|
|
4
|
+
// passes a Web-Streams backend, `node/index.js` passes the raw-Node-streams one.
|
|
5
|
+
|
|
6
|
+
import bqSpawn from './bq-spawn.js';
|
|
7
|
+
import bqShell from './bq-shell.js';
|
|
8
|
+
|
|
9
|
+
import {isWindows} from './utils.js';
|
|
10
|
+
|
|
11
|
+
export const buildApi = async ({spawn, cwd, currentExecPath, runFileArgs}) => {
|
|
12
|
+
let modShell;
|
|
13
|
+
if (isWindows) {
|
|
14
|
+
modShell = await import('./shell/windows.js');
|
|
15
|
+
} else {
|
|
16
|
+
modShell = await import('./shell/unix.js');
|
|
17
|
+
}
|
|
18
|
+
const {shellEscape, currentShellPath, buildShellCommand} = modShell;
|
|
19
|
+
|
|
20
|
+
// spawn functions
|
|
21
|
+
|
|
22
|
+
const $$ = bqSpawn(spawn);
|
|
23
|
+
|
|
24
|
+
const $ = bqSpawn((command, options) => {
|
|
25
|
+
const sp = spawn(command, options);
|
|
26
|
+
return sp.exited.then(() => ({code: sp.exitCode, signal: sp.signalCode, killed: sp.killed}));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const fromProcess = bqSpawn((command, options) => {
|
|
30
|
+
const sp = spawn(command, {...options, stdout: 'pipe'});
|
|
31
|
+
return sp.stdout;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const toProcess = bqSpawn((command, options) => {
|
|
35
|
+
const sp = spawn(command, {...options, stdin: 'pipe'});
|
|
36
|
+
return sp.stdin;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const throughProcess = bqSpawn((command, options) => {
|
|
40
|
+
const sp = spawn(command, {...options, stdin: 'pipe', stdout: 'pipe'});
|
|
41
|
+
return sp.asDuplex;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const $impl = /** @type {import('./index.d.ts').DollarImpl} */ ($);
|
|
45
|
+
$impl.from = fromProcess;
|
|
46
|
+
$impl.to = toProcess;
|
|
47
|
+
$impl.through = $impl.io = throughProcess;
|
|
48
|
+
|
|
49
|
+
// shell functions
|
|
50
|
+
|
|
51
|
+
const shell = bqShell(shellEscape, (command, options) =>
|
|
52
|
+
spawn(buildShellCommand(options?.shellPath, options?.shellArgs, command), {
|
|
53
|
+
...options,
|
|
54
|
+
windowsVerbatimArguments: true
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const $sh = bqShell(shellEscape, (command, options) => {
|
|
59
|
+
const sp = spawn(buildShellCommand(options?.shellPath, options?.shellArgs, command), {
|
|
60
|
+
...options,
|
|
61
|
+
windowsVerbatimArguments: true
|
|
62
|
+
});
|
|
63
|
+
return sp.exited.then(() => ({code: sp.exitCode, signal: sp.signalCode, killed: sp.killed}));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const fromShell = bqShell(shellEscape, (command, options) => {
|
|
67
|
+
const sp = spawn(buildShellCommand(options?.shellPath, options?.shellArgs, command), {
|
|
68
|
+
...options,
|
|
69
|
+
stdout: 'pipe',
|
|
70
|
+
windowsVerbatimArguments: true
|
|
71
|
+
});
|
|
72
|
+
return sp.stdout;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const toShell = bqShell(shellEscape, (command, options) => {
|
|
76
|
+
const sp = spawn(buildShellCommand(options?.shellPath, options?.shellArgs, command), {
|
|
77
|
+
...options,
|
|
78
|
+
stdin: 'pipe',
|
|
79
|
+
windowsVerbatimArguments: true
|
|
80
|
+
});
|
|
81
|
+
return sp.stdin;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const throughShell = bqShell(shellEscape, (command, options) => {
|
|
85
|
+
const sp = spawn(buildShellCommand(options?.shellPath, options?.shellArgs, command), {
|
|
86
|
+
...options,
|
|
87
|
+
stdin: 'pipe',
|
|
88
|
+
stdout: 'pipe',
|
|
89
|
+
windowsVerbatimArguments: true
|
|
90
|
+
});
|
|
91
|
+
return sp.asDuplex;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const $shImpl = /** @type {import('./index.d.ts').ShellImpl} */ ($sh);
|
|
95
|
+
$shImpl.from = fromShell;
|
|
96
|
+
$shImpl.to = toShell;
|
|
97
|
+
$shImpl.through = $shImpl.io = throughShell;
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
spawn,
|
|
101
|
+
cwd,
|
|
102
|
+
currentExecPath,
|
|
103
|
+
runFileArgs,
|
|
104
|
+
shellEscape,
|
|
105
|
+
currentShellPath,
|
|
106
|
+
buildShellCommand,
|
|
107
|
+
$$,
|
|
108
|
+
$,
|
|
109
|
+
shell,
|
|
110
|
+
sh: shell,
|
|
111
|
+
$sh
|
|
112
|
+
};
|
|
113
|
+
};
|
package/src/index.d.ts
CHANGED
|
@@ -215,7 +215,7 @@ export interface DollarResult {
|
|
|
215
215
|
/**
|
|
216
216
|
* The type of the {@link $} function.
|
|
217
217
|
*/
|
|
218
|
-
interface DollarImpl<R = any> extends Dollar<Promise<DollarResult>> {
|
|
218
|
+
export interface DollarImpl<R = any> extends Dollar<Promise<DollarResult>> {
|
|
219
219
|
/**
|
|
220
220
|
* The tag function that can be used as a template string.
|
|
221
221
|
* It can take an options object and return self with updated defaults.
|
|
@@ -297,7 +297,7 @@ export declare const sh: typeof shell;
|
|
|
297
297
|
/**
|
|
298
298
|
* The type of the $sh function.
|
|
299
299
|
*/
|
|
300
|
-
interface ShellImpl<R = any> extends Dollar<Promise<DollarResult>, ShellOptions> {
|
|
300
|
+
export interface ShellImpl<R = any> extends Dollar<Promise<DollarResult>, ShellOptions> {
|
|
301
301
|
/**
|
|
302
302
|
* The tag function that can be used as a template string.
|
|
303
303
|
* It can take an options object and return self with updated defaults.
|
package/src/index.js
CHANGED
|
@@ -2,107 +2,63 @@
|
|
|
2
2
|
|
|
3
3
|
// load dependencies
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
import {getEnv} from './utils.js';
|
|
6
|
+
|
|
7
|
+
import {buildApi} from './build.js';
|
|
8
|
+
|
|
9
|
+
export {isWindows, raw, winCmdEscape} from './utils.js';
|
|
10
|
+
|
|
11
|
+
// Force the Node backend on every runtime with the `DSH_FORCE_NODE` environment
|
|
12
|
+
// variable (e.g. DSH_FORCE_NODE=1) — Bun/Deno then run on their Node compat (e.g. to
|
|
13
|
+
// sidestep Bun's Web-Stream child-pipe tail-drop). The env var is inherited by spawned
|
|
14
|
+
// children; to force only this process, set `globalThis.DSH_FORCE_NODE` before a dynamic
|
|
15
|
+
// import instead. Treated as off: unset, '', '0', 'false'.
|
|
16
|
+
const isFlagOn = value =>
|
|
17
|
+
value != null &&
|
|
18
|
+
value !== '' &&
|
|
19
|
+
value !== '0' &&
|
|
20
|
+
value !== 0 &&
|
|
21
|
+
String(value).toLowerCase() !== 'false';
|
|
22
|
+
|
|
23
|
+
const envForceNode = () => {
|
|
24
|
+
if (typeof Deno !== 'undefined') {
|
|
25
|
+
const status = Deno.permissions?.querySync?.({name: 'env', variable: 'DSH_FORCE_NODE'});
|
|
26
|
+
if (status && status.state !== 'granted') return undefined;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return getEnv('DSH_FORCE_NODE');
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const forceNode =
|
|
36
|
+
isFlagOn(/** @type {any} */ (globalThis).DSH_FORCE_NODE) || isFlagOn(envForceNode());
|
|
11
37
|
|
|
12
38
|
let modSpawn;
|
|
13
|
-
if (
|
|
39
|
+
if (forceNode) {
|
|
40
|
+
modSpawn = await import('./spawn/node.js');
|
|
41
|
+
} else if (typeof Deno !== 'undefined') {
|
|
14
42
|
modSpawn = await import('./spawn/deno.js');
|
|
15
43
|
} else if (typeof Bun !== 'undefined') {
|
|
16
44
|
modSpawn = await import('./spawn/bun.js');
|
|
17
45
|
} else {
|
|
18
46
|
modSpawn = await import('./spawn/node.js');
|
|
19
47
|
}
|
|
20
|
-
export const {spawn, cwd, currentExecPath, runFileArgs} = modSpawn;
|
|
21
|
-
|
|
22
|
-
let modShell;
|
|
23
|
-
if (isWindows) {
|
|
24
|
-
modShell = await import('./shell/windows.js');
|
|
25
|
-
} else {
|
|
26
|
-
modShell = await import('./shell/unix.js');
|
|
27
|
-
}
|
|
28
|
-
export const {shellEscape, currentShellPath, buildShellCommand} = modShell;
|
|
29
|
-
|
|
30
|
-
// define spawn functions
|
|
31
|
-
|
|
32
|
-
export const $$ = bqSpawn(spawn);
|
|
33
|
-
|
|
34
|
-
export const $ = bqSpawn((command, options) => {
|
|
35
|
-
const sp = spawn(command, options);
|
|
36
|
-
return sp.exited.then(() => ({code: sp.exitCode, signal: sp.signalCode, killed: sp.killed}));
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const fromProcess = bqSpawn((command, options) => {
|
|
40
|
-
const sp = spawn(command, {...options, stdout: 'pipe'});
|
|
41
|
-
return sp.stdout;
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const toProcess = bqSpawn((command, options) => {
|
|
45
|
-
const sp = spawn(command, {...options, stdin: 'pipe'});
|
|
46
|
-
return sp.stdin;
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const throughProcess = bqSpawn((command, options) => {
|
|
50
|
-
const sp = spawn(command, {...options, stdin: 'pipe', stdout: 'pipe'});
|
|
51
|
-
return sp.asDuplex;
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
$.from = fromProcess;
|
|
55
|
-
$.to = toProcess;
|
|
56
|
-
$.through = $.io = throughProcess;
|
|
57
|
-
|
|
58
|
-
// define shell functions
|
|
59
|
-
|
|
60
|
-
export const shell = bqShell(shellEscape, (command, options) =>
|
|
61
|
-
spawn(buildShellCommand(options?.shellPath, options?.shellArgs, command), {
|
|
62
|
-
...options,
|
|
63
|
-
windowsVerbatimArguments: true
|
|
64
|
-
})
|
|
65
|
-
);
|
|
66
|
-
export {shell as sh};
|
|
67
|
-
|
|
68
|
-
export const $sh = bqShell(shellEscape, (command, options) => {
|
|
69
|
-
const sp = spawn(buildShellCommand(options?.shellPath, options?.shellArgs, command), {
|
|
70
|
-
...options,
|
|
71
|
-
windowsVerbatimArguments: true
|
|
72
|
-
});
|
|
73
|
-
return sp.exited.then(() => ({code: sp.exitCode, signal: sp.signalCode, killed: sp.killed}));
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const fromShell = bqShell(shellEscape, (command, options) => {
|
|
77
|
-
const sp = spawn(buildShellCommand(options?.shellPath, options?.shellArgs, command), {
|
|
78
|
-
...options,
|
|
79
|
-
stdout: 'pipe',
|
|
80
|
-
windowsVerbatimArguments: true
|
|
81
|
-
});
|
|
82
|
-
return sp.stdout;
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const toShell = bqShell(shellEscape, (command, options) => {
|
|
86
|
-
const sp = spawn(buildShellCommand(options?.shellPath, options?.shellArgs, command), {
|
|
87
|
-
...options,
|
|
88
|
-
stdin: 'pipe',
|
|
89
|
-
windowsVerbatimArguments: true
|
|
90
|
-
});
|
|
91
|
-
return sp.stdin;
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const throughShell = bqShell(shellEscape, (command, options) => {
|
|
95
|
-
const sp = spawn(buildShellCommand(options?.shellPath, options?.shellArgs, command), {
|
|
96
|
-
...options,
|
|
97
|
-
stdin: 'pipe',
|
|
98
|
-
stdout: 'pipe',
|
|
99
|
-
windowsVerbatimArguments: true
|
|
100
|
-
});
|
|
101
|
-
return sp.asDuplex;
|
|
102
|
-
});
|
|
103
48
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
49
|
+
export const {
|
|
50
|
+
spawn,
|
|
51
|
+
cwd,
|
|
52
|
+
currentExecPath,
|
|
53
|
+
runFileArgs,
|
|
54
|
+
shellEscape,
|
|
55
|
+
currentShellPath,
|
|
56
|
+
buildShellCommand,
|
|
57
|
+
$$,
|
|
58
|
+
$,
|
|
59
|
+
shell,
|
|
60
|
+
sh,
|
|
61
|
+
$sh
|
|
62
|
+
} = await buildApi(modSpawn);
|
|
107
63
|
|
|
108
64
|
export default $;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import type {Readable, Writable, Duplex} from 'node:stream';
|
|
3
|
+
import type {
|
|
4
|
+
SpawnStreamState,
|
|
5
|
+
SpawnOptions,
|
|
6
|
+
ShellEscapeOptions,
|
|
7
|
+
DollarResult,
|
|
8
|
+
ShellOptions
|
|
9
|
+
} from '../index.js';
|
|
10
|
+
|
|
11
|
+
// The stream-free half of the public API is identical to the main entry — reuse it verbatim.
|
|
12
|
+
export type {SpawnStreamState, SpawnOptions, ShellEscapeOptions, DollarResult, ShellOptions};
|
|
13
|
+
export {
|
|
14
|
+
cwd,
|
|
15
|
+
currentExecPath,
|
|
16
|
+
runFileArgs,
|
|
17
|
+
isWindows,
|
|
18
|
+
raw,
|
|
19
|
+
winCmdEscape,
|
|
20
|
+
shellEscape,
|
|
21
|
+
currentShellPath,
|
|
22
|
+
buildShellCommand
|
|
23
|
+
} from '../index.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sub-process object. Identical to the main entry's `Subprocess`, except the standard
|
|
27
|
+
* streams are Node streams ([`Readable`]/[`Writable`]) instead of Web streams.
|
|
28
|
+
*/
|
|
29
|
+
export interface Subprocess {
|
|
30
|
+
/**
|
|
31
|
+
* The raw command that was run as an array of strings.
|
|
32
|
+
*/
|
|
33
|
+
readonly command: string[];
|
|
34
|
+
/**
|
|
35
|
+
* The options that were passed to `spawn()`.
|
|
36
|
+
*/
|
|
37
|
+
readonly options: SpawnOptions | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* The promise that will be resolved with the exit code when the process exits.
|
|
40
|
+
*/
|
|
41
|
+
readonly exited: Promise<number>;
|
|
42
|
+
/**
|
|
43
|
+
* Whether the process has finished running.
|
|
44
|
+
*/
|
|
45
|
+
readonly finished: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Whether the process was killed when finished.
|
|
48
|
+
*/
|
|
49
|
+
readonly killed: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* The exit code of the process when it was finished.
|
|
52
|
+
*/
|
|
53
|
+
readonly exitCode: number | null;
|
|
54
|
+
/**
|
|
55
|
+
* The signal code of the process when it was finished.
|
|
56
|
+
*/
|
|
57
|
+
readonly signalCode: string | null;
|
|
58
|
+
/**
|
|
59
|
+
* The standard input stream as a Node `Writable`.
|
|
60
|
+
* It is `null` if `options.stdin` was not set to `'pipe'`.
|
|
61
|
+
*/
|
|
62
|
+
readonly stdin: Writable | null;
|
|
63
|
+
/**
|
|
64
|
+
* The standard output stream as a Node `Readable`.
|
|
65
|
+
* It is `null` if `options.stdout` was not set to `'pipe'`.
|
|
66
|
+
*/
|
|
67
|
+
readonly stdout: Readable | null;
|
|
68
|
+
/**
|
|
69
|
+
* The standard error stream as a Node `Readable`.
|
|
70
|
+
* It is `null` if `options.stderr` was not set to `'pipe'`.
|
|
71
|
+
*/
|
|
72
|
+
readonly stderr: Readable | null;
|
|
73
|
+
/**
|
|
74
|
+
* The process as a Node `Duplex` stream: writes go to `stdin`, reads come from `stdout`.
|
|
75
|
+
* Built lazily on first access (and cached). Requires both `stdin` and `stdout` piped.
|
|
76
|
+
*/
|
|
77
|
+
readonly asDuplex: Duplex;
|
|
78
|
+
/**
|
|
79
|
+
* Kill the process.
|
|
80
|
+
*/
|
|
81
|
+
kill(): void;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Spawn a process with advanced ways to configure and control it.
|
|
86
|
+
*/
|
|
87
|
+
export declare function spawn(command: string[], options?: SpawnOptions): Subprocess;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Backticks (tag) function.
|
|
91
|
+
*/
|
|
92
|
+
type Backticks<R> = (strings: TemplateStringsArray, ...args: unknown[]) => R;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The type of the $ (tag) function. It can be used as a tag function for a template string,
|
|
96
|
+
* or it can take an options object and return self with updated defaults.
|
|
97
|
+
*/
|
|
98
|
+
interface Dollar<R, O = SpawnOptions> extends Backticks<R> {
|
|
99
|
+
(options: O): Dollar<R, O>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* `$$` (tag) function: spawns a process and returns a {@link Subprocess}.
|
|
104
|
+
*/
|
|
105
|
+
export declare const $$: Dollar<Subprocess>;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The type of the {@link $} function.
|
|
109
|
+
*/
|
|
110
|
+
export interface DollarImpl extends Dollar<Promise<DollarResult>> {
|
|
111
|
+
/**
|
|
112
|
+
* Returns the `stdout` of the process as a Node `Readable`.
|
|
113
|
+
*/
|
|
114
|
+
from: Dollar<Readable>;
|
|
115
|
+
/**
|
|
116
|
+
* Returns the `stdin` of the process as a Node `Writable`.
|
|
117
|
+
*/
|
|
118
|
+
to: Dollar<Writable>;
|
|
119
|
+
/**
|
|
120
|
+
* Returns the process as a Node `Duplex` stream. Alias of `io`.
|
|
121
|
+
*/
|
|
122
|
+
through: Dollar<Duplex>;
|
|
123
|
+
/**
|
|
124
|
+
* Returns the process as a Node `Duplex` stream. Alias of `through`.
|
|
125
|
+
*/
|
|
126
|
+
io: Dollar<Duplex>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The `$` function with custom properties (`from`, `to`, `through`, `io`). Used as a tag
|
|
131
|
+
* function it spawns a process and resolves to a simplified result. It is the default export.
|
|
132
|
+
*/
|
|
133
|
+
export declare const $: DollarImpl;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The shell tag function: runs a command through a shell and returns a {@link Subprocess}.
|
|
137
|
+
*/
|
|
138
|
+
export declare const shell: Dollar<Subprocess, ShellOptions>;
|
|
139
|
+
/**
|
|
140
|
+
* An alias of `shell`.
|
|
141
|
+
*/
|
|
142
|
+
export declare const sh: typeof shell;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The type of the `$sh` function.
|
|
146
|
+
*/
|
|
147
|
+
export interface ShellImpl extends Dollar<Promise<DollarResult>, ShellOptions> {
|
|
148
|
+
/**
|
|
149
|
+
* Returns the `stdout` of the shell process as a Node `Readable`.
|
|
150
|
+
*/
|
|
151
|
+
from: Dollar<Readable, ShellOptions>;
|
|
152
|
+
/**
|
|
153
|
+
* Returns the `stdin` of the shell process as a Node `Writable`.
|
|
154
|
+
*/
|
|
155
|
+
to: Dollar<Writable, ShellOptions>;
|
|
156
|
+
/**
|
|
157
|
+
* Returns the shell process as a Node `Duplex` stream. Alias of `io`.
|
|
158
|
+
*/
|
|
159
|
+
through: Dollar<Duplex, ShellOptions>;
|
|
160
|
+
/**
|
|
161
|
+
* Returns the shell process as a Node `Duplex` stream. Alias of `through`.
|
|
162
|
+
*/
|
|
163
|
+
io: Dollar<Duplex, ShellOptions>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The `$sh` function with custom properties (`from`, `to`, `through`, `io`). Used as a tag
|
|
168
|
+
* function it runs a shell command and resolves to a simplified result.
|
|
169
|
+
*/
|
|
170
|
+
export declare const $sh: ShellImpl;
|
|
171
|
+
|
|
172
|
+
export default $;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @ts-self-types="./index.d.ts"
|
|
2
|
+
|
|
3
|
+
// The Node-streams facade: identical API to the main entry, but stdin/stdout/stderr
|
|
4
|
+
// (and .from/.to/.through/.io/asDuplex) are Node streams instead of Web streams.
|
|
5
|
+
// Always uses the Node backend — on Bun/Deno it runs through their node:child_process compat.
|
|
6
|
+
|
|
7
|
+
import {buildApi} from '../build.js';
|
|
8
|
+
import * as backend from '../spawn/node.js';
|
|
9
|
+
|
|
10
|
+
export {isWindows, raw, winCmdEscape} from '../utils.js';
|
|
11
|
+
|
|
12
|
+
export const {
|
|
13
|
+
spawn,
|
|
14
|
+
cwd,
|
|
15
|
+
currentExecPath,
|
|
16
|
+
runFileArgs,
|
|
17
|
+
shellEscape,
|
|
18
|
+
currentShellPath,
|
|
19
|
+
buildShellCommand,
|
|
20
|
+
$$,
|
|
21
|
+
$,
|
|
22
|
+
shell,
|
|
23
|
+
sh,
|
|
24
|
+
$sh
|
|
25
|
+
} = await buildApi({
|
|
26
|
+
spawn: backend.nodeStreamSpawn,
|
|
27
|
+
cwd: backend.cwd,
|
|
28
|
+
currentExecPath: backend.currentExecPath,
|
|
29
|
+
runFileArgs: backend.runFileArgs
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export default $;
|
package/src/spawn/bun.js
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapt Bun's `FileSink` to a Web Streams `UnderlyingSink`. `FileSink` exposes
|
|
3
|
+
* `write`/`end` (close + flush) but no `close`/`abort` matching the spec, so
|
|
4
|
+
* `new WritableStream(fileSink)` would only work because of an undocumented
|
|
5
|
+
* runtime `.close` alias. This adapter sticks to the documented `.end` API.
|
|
6
|
+
*
|
|
7
|
+
* @param {import('bun').FileSink} sink
|
|
8
|
+
* @returns {UnderlyingSink}
|
|
9
|
+
*/
|
|
10
|
+
const makeStdinSink = sink => ({
|
|
11
|
+
async write(/** @type {any} */ chunk) {
|
|
12
|
+
await sink.write(chunk);
|
|
13
|
+
},
|
|
14
|
+
async close() {
|
|
15
|
+
await sink.end();
|
|
16
|
+
},
|
|
17
|
+
async abort(reason) {
|
|
18
|
+
await sink.end(reason instanceof Error ? reason : new Error(String(reason)));
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
1
22
|
const sanitize = (value, defaultValue = 'ignore') => {
|
|
2
23
|
switch (value) {
|
|
3
24
|
case 'pipe':
|
|
@@ -47,7 +68,11 @@ class Subprocess {
|
|
|
47
68
|
return code;
|
|
48
69
|
});
|
|
49
70
|
|
|
50
|
-
|
|
71
|
+
const stdinSink = this.childProcess.stdin;
|
|
72
|
+
this.stdin =
|
|
73
|
+
stdinSink && typeof stdinSink !== 'number'
|
|
74
|
+
? new WritableStream(makeStdinSink(stdinSink))
|
|
75
|
+
: null;
|
|
51
76
|
this.stdout = this.childProcess.stdout || null;
|
|
52
77
|
this.stderr = this.childProcess.stderr || null;
|
|
53
78
|
}
|
package/src/spawn/node.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {spawn} from 'node:child_process';
|
|
2
|
-
import {Readable, Writable} from 'node:stream';
|
|
2
|
+
import {Readable, Writable, Duplex} from 'node:stream';
|
|
3
3
|
|
|
4
4
|
const setStdio = (stdio, fd, value) => {
|
|
5
5
|
switch (value) {
|
|
@@ -18,7 +18,10 @@ const setStdio = (stdio, fd, value) => {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
class Subprocess {
|
|
21
|
-
|
|
21
|
+
#nodeStreams;
|
|
22
|
+
#asDuplex;
|
|
23
|
+
|
|
24
|
+
constructor(command, options, nodeStreams) {
|
|
22
25
|
this.command = command;
|
|
23
26
|
this.options = options;
|
|
24
27
|
|
|
@@ -27,6 +30,7 @@ class Subprocess {
|
|
|
27
30
|
this.killed = false;
|
|
28
31
|
this.finished = false;
|
|
29
32
|
|
|
33
|
+
/** @type {import('node:child_process').SpawnOptions} */
|
|
30
34
|
const spawnOptions = {stdio: ['ignore', 'ignore', 'ignore']};
|
|
31
35
|
if (options.windowsVerbatimArguments) spawnOptions.windowsVerbatimArguments = true;
|
|
32
36
|
options.cwd && (spawnOptions.cwd = options.cwd);
|
|
@@ -66,13 +70,28 @@ class Subprocess {
|
|
|
66
70
|
this.reject(error);
|
|
67
71
|
});
|
|
68
72
|
|
|
69
|
-
this
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
this.#nodeStreams = nodeStreams;
|
|
74
|
+
if (nodeStreams) {
|
|
75
|
+
this.stdin = this.childProcess.stdin || null;
|
|
76
|
+
this.stdout = this.childProcess.stdout || null;
|
|
77
|
+
this.stderr = this.childProcess.stderr || null;
|
|
78
|
+
} else {
|
|
79
|
+
this.stdin = this.childProcess.stdin && Writable.toWeb(this.childProcess.stdin);
|
|
80
|
+
this.stdout = this.childProcess.stdout && Readable.toWeb(this.childProcess.stdout);
|
|
81
|
+
this.stderr = this.childProcess.stderr && Readable.toWeb(this.childProcess.stderr);
|
|
82
|
+
}
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
get asDuplex() {
|
|
75
|
-
|
|
86
|
+
// Web-streams mode keeps the `{readable, writable}` pair (the pipeThrough shape).
|
|
87
|
+
// Node-streams mode returns a real Node `Duplex` (built lazily, once) so the process
|
|
88
|
+
// drops straight into `.pipe()` chains and `stream.pipeline()`.
|
|
89
|
+
if (!this.#nodeStreams) return {readable: this.stdout, writable: this.stdin};
|
|
90
|
+
// `@types/node` types `Duplex.from({readable, writable})` for Web streams only,
|
|
91
|
+
// but at runtime it accepts a Node readable + writable just fine.
|
|
92
|
+
return (this.#asDuplex ??= Duplex.from(
|
|
93
|
+
/** @type {any} */ ({readable: this.stdout, writable: this.stdin})
|
|
94
|
+
));
|
|
76
95
|
}
|
|
77
96
|
|
|
78
97
|
kill() {
|
|
@@ -87,4 +106,7 @@ export const runFileArgs = [];
|
|
|
87
106
|
export const cwd = () => process.cwd();
|
|
88
107
|
|
|
89
108
|
const nodeSpawn = (command, options = {}) => new Subprocess(command, options);
|
|
90
|
-
|
|
109
|
+
// Variant that keeps raw Node streams (no Web Streams conversion) — backs the
|
|
110
|
+
// `dollar-shell/node` facade. Internal: reached only through that entry.
|
|
111
|
+
const nodeStreamSpawn = (command, options = {}) => new Subprocess(command, options, true);
|
|
112
|
+
export {nodeSpawn as spawn, nodeStreamSpawn};
|
package/src/utils.js
CHANGED
|
@@ -14,7 +14,8 @@ if (typeof Deno !== 'undefined') {
|
|
|
14
14
|
}
|
|
15
15
|
export {getEnv, isWindows, isAndroid};
|
|
16
16
|
|
|
17
|
-
export const verifyStrings = strings =>
|
|
17
|
+
export const verifyStrings = strings =>
|
|
18
|
+
Array.isArray(strings) && Array.isArray(/** @type {any} */ (strings).raw);
|
|
18
19
|
|
|
19
20
|
export const toBase64 = s => {
|
|
20
21
|
// const buf = new TextEncoder().encode(s),
|
|
@@ -32,6 +33,9 @@ export const raw = value => ({[rawValueSymbol]: value});
|
|
|
32
33
|
export const isRawValue = value => value && typeof value === 'object' && rawValueSymbol in value;
|
|
33
34
|
export const getRawValue = value => value[rawValueSymbol];
|
|
34
35
|
|
|
36
|
+
export const bqTagSymbol = Symbol.for('dollar-shell.bq-tag');
|
|
37
|
+
export const isBqTag = value => typeof value === 'function' && value[bqTagSymbol] === true;
|
|
38
|
+
|
|
35
39
|
export const winCmdEscape = isWindows
|
|
36
40
|
? value => raw(String(value).replace(/./g, '^$&'))
|
|
37
41
|
: value => String(value);
|
package/AGENTS.md
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# AGENTS.md — dollar-shell
|
|
2
|
-
|
|
3
|
-
> `dollar-shell` is a micro-library for running OS and shell commands from JavaScript/TypeScript using template tag functions. It works on Node, Deno, and Bun with the same API. All streams are web streams. Zero dependencies.
|
|
4
|
-
|
|
5
|
-
For detailed usage docs and API references see the [wiki](https://github.com/uhop/dollar-shell/wiki).
|
|
6
|
-
|
|
7
|
-
## Setup
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
git clone --recursive git@github.com:uhop/dollar-shell.git
|
|
11
|
-
cd dollar-shell
|
|
12
|
-
npm install
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
The wiki is a git submodule in `wiki/`.
|
|
16
|
-
|
|
17
|
-
## Commands
|
|
18
|
-
|
|
19
|
-
- **Test (Node):** `npm test` (runs `tape6 --flags FO`)
|
|
20
|
-
- **Test (Bun):** `npm run test:bun`
|
|
21
|
-
- **Test (Deno):** `npm run test:deno`
|
|
22
|
-
- **TypeScript check:** `npm run ts-check` (`tsc --noEmit`)
|
|
23
|
-
- **TypeScript tests:** `npm run ts-test` (run `.ts` test files with tape6)
|
|
24
|
-
- **Lint:** `npm run lint` (Prettier check)
|
|
25
|
-
- **Lint fix:** `npm run lint:fix` (Prettier write)
|
|
26
|
-
|
|
27
|
-
## Project structure
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
dollar-shell/
|
|
31
|
-
├── package.json # Package config
|
|
32
|
-
├── tsconfig.json # TypeScript config (noEmit check only)
|
|
33
|
-
├── src/ # Source code
|
|
34
|
-
│ ├── index.js # Main entry point, wires everything together
|
|
35
|
-
│ ├── index.d.ts # TypeScript declarations for the full public API
|
|
36
|
-
│ ├── bq-spawn.js # Template tag factory for spawn-based functions ($, $$)
|
|
37
|
-
│ ├── bq-shell.js # Template tag factory for shell-based functions ($sh, shell)
|
|
38
|
-
│ ├── utils.js # Shared utilities (raw, isWindows, winCmdEscape, etc.)
|
|
39
|
-
│ ├── spawn/ # Runtime-specific Subprocess implementations
|
|
40
|
-
│ │ ├── node.js # Node.js: uses child_process + stream.Writable/Readable
|
|
41
|
-
│ │ ├── deno.js # Deno: uses Deno.Command
|
|
42
|
-
│ │ └── bun.js # Bun: uses Bun.spawn
|
|
43
|
-
│ └── shell/ # Platform-specific shell escaping and command building
|
|
44
|
-
│ ├── unix.js # Unix: single-quote escaping, $SHELL detection
|
|
45
|
-
│ └── windows.js # Windows: cmd.exe and PowerShell escaping
|
|
46
|
-
├── tests/ # Automated tests (tape-six): .js, .cjs, .ts
|
|
47
|
-
├── tests/manual/ # Manual verification scripts
|
|
48
|
-
└── wiki/ # GitHub wiki documentation (git submodule)
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Quick reference
|
|
52
|
-
|
|
53
|
-
```js
|
|
54
|
-
import $, {$$, $sh, shell, sh, spawn} from 'dollar-shell';
|
|
55
|
-
|
|
56
|
-
// Run a command, get exit info
|
|
57
|
-
const result = await $`echo hello`;
|
|
58
|
-
// result: {code: 0, signal: null, killed: false}
|
|
59
|
-
|
|
60
|
-
// Run a command, get full Subprocess
|
|
61
|
-
const sp = $$`sleep 5`;
|
|
62
|
-
sp.kill();
|
|
63
|
-
await sp.exited;
|
|
64
|
-
|
|
65
|
-
// Shell command (supports pipes, aliases)
|
|
66
|
-
await $sh`ls -l . | grep LICENSE | wc`;
|
|
67
|
-
|
|
68
|
-
// Stream pipelines
|
|
69
|
-
$.from`ls -l .`.pipeThrough($.io`grep LIC`).pipeTo($.to({stdout: 'inherit'})`wc`);
|
|
70
|
-
|
|
71
|
-
// Custom options (returns new tag function with updated defaults)
|
|
72
|
-
const $verbose = $({stdout: 'inherit', stderr: 'inherit'});
|
|
73
|
-
await $verbose`ls -l .`;
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Code style
|
|
77
|
-
|
|
78
|
-
- **ES modules** throughout (`"type": "module"` in package.json).
|
|
79
|
-
- **No transpilation** — code runs directly in all target runtimes.
|
|
80
|
-
- **Prettier** for formatting — run `npm run lint:fix` before committing.
|
|
81
|
-
- Imports at the top of files, using `import` syntax.
|
|
82
|
-
|
|
83
|
-
## Architecture
|
|
84
|
-
|
|
85
|
-
- `src/index.js` is the main entry point. It dynamically imports the correct `spawn` implementation (Node/Deno/Bun) and the correct `shell` implementation (Unix/Windows) at import time.
|
|
86
|
-
- All tag functions (`$`, `$$`, `$sh`, `shell`) are built by factory functions in `bq-spawn.js` and `bq-shell.js`.
|
|
87
|
-
- **Tag function + options pattern**: calling a tag function with an options object returns a new tag function with updated defaults while preserving `.from`, `.to`, `.io`, `.through` properties.
|
|
88
|
-
- **raw()**: wraps a value to bypass escaping (shell) or argument splitting (spawn).
|
|
89
|
-
- **Platform detection**: `isWindows` boolean is exported. Runtime detection (Node/Deno/Bun) happens at import via dynamic imports.
|
|
90
|
-
|
|
91
|
-
## Key conventions
|
|
92
|
-
|
|
93
|
-
- Do not add dependencies — the library is intentionally zero-dependency.
|
|
94
|
-
- All public API is exported from `src/index.js` and typed in `src/index.d.ts`. Keep them in sync.
|
|
95
|
-
- Wiki documentation lives in the `wiki/` submodule — update it alongside code changes.
|
|
96
|
-
- Tests are in `tests/` (automated, tape-six) and `tests/manual/` (manual verification scripts).
|
|
97
|
-
- TypeScript typing tests (`.ts`) are in `tests/` and checked by `npm run ts-check`. They can also be run as tests via `npm run ts-test`.
|
package/CLAUDE.md
DELETED
package/CONTRIBUTING.md
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# Contributing to dollar-shell
|
|
2
|
-
|
|
3
|
-
Thank you for your interest in contributing!
|
|
4
|
-
|
|
5
|
-
## Getting started
|
|
6
|
-
|
|
7
|
-
This project uses git submodules. Clone and set up:
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
git clone --recursive git@github.com:uhop/dollar-shell.git
|
|
11
|
-
cd dollar-shell
|
|
12
|
-
npm install
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
See the [wiki](https://github.com/uhop/dollar-shell/wiki) for API documentation.
|
|
16
|
-
|
|
17
|
-
## Development workflow
|
|
18
|
-
|
|
19
|
-
1. Make your changes.
|
|
20
|
-
2. Format: `npm run lint:fix`
|
|
21
|
-
3. Test: `npm test`
|
|
22
|
-
4. Type-check: `npm run ts-check`
|
|
23
|
-
|
|
24
|
-
## Code style
|
|
25
|
-
|
|
26
|
-
- ES modules (`import`/`export`), no CommonJS in source.
|
|
27
|
-
- Formatted with Prettier — run `npm run lint:fix` before committing.
|
|
28
|
-
- No dependencies — the library is intentionally zero-dependency.
|
|
29
|
-
- Keep `src/index.js` and `src/index.d.ts` in sync.
|
|
30
|
-
- Update wiki documentation alongside code changes.
|
|
31
|
-
|
|
32
|
-
## AI agents
|
|
33
|
-
|
|
34
|
-
If you are an AI coding agent, see [AGENTS.md](./AGENTS.md) for detailed project conventions, commands, and architecture.
|