dollar-shell 1.2.0 → 1.2.1

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
@@ -223,9 +223,11 @@ The rest is identical to `$`: `$sh`, `$sh.from`, `$sh.to` and `$sh.io`/`$sh.thro
223
223
 
224
224
  Each runtime uses its own backend by default (`node:child_process` on Node, `Bun.spawn` on Bun,
225
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).
226
+ make every runtime spawn through the Node backend — Bun and Deno then run `node:child_process` on
227
+ their compatibility layer. This swaps **only the spawn mechanism**: the runtime that runs your code and how
228
+ it's re-launched stay native, so a forced child of Bun/Deno is still `bun run …` / `deno run …`, never a
229
+ bare `node`. Handy for sidestepping runtime-specific quirks (e.g. Bun intermittently dropping the last
230
+ chunk of a child's piped output).
229
231
 
230
232
  Because dollar-shell spawns children with `env` defaulting to `process.env`, the variable is inherited by
231
233
  those children — so it forces the Node backend across the whole process tree. To force **only the
@@ -248,8 +250,8 @@ sp.stdout.pipe(process.stdout); // sp.stdout is a Node Readable
248
250
 
249
251
  The API is identical to the main entry — same `$`, `$$`, `$sh`, `shell`, helpers, and
250
252
  `.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
+ are Node `Readable`s, and `asDuplex` / `.io` / `.through` return a Node `Duplex`). It always spawns through the Node backend, so it also runs on Bun and Deno through their
254
+ `node:child_process` compatibility layer (only the spawn mechanism changes — the runtime launch stays native).
253
255
 
254
256
  ## For AI Agents
255
257
 
@@ -269,6 +271,7 @@ BSD-3-Clause
269
271
 
270
272
  ## Release History
271
273
 
274
+ - 1.2.1 _Bugfix: `DSH_FORCE_NODE` and `dollar-shell/node` now switch only the spawn mechanism — spawned children stay native (`bun run …` / `deno run …`)._
272
275
  - 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
276
  - 1.1.14 _Fixed Bun stdin abort path, added js-check, Bun + Deno wired into CI._
274
277
  - 1.1.13 _Updated dev dependencies._
package/llms-full.txt CHANGED
@@ -318,12 +318,14 @@ Works on Node.js, Deno, and Bun. The appropriate spawn implementation is selecte
318
318
 
319
319
  ### Forcing the Node backend
320
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`):
321
+ By default each runtime uses its own backend (`node:child_process` on Node, `Bun.spawn` on Bun, `Deno.Command` on Deno). Make every runtime spawn through the Node backend — so Bun and Deno run `node:child_process` on their compatibility layer — with the **`DSH_FORCE_NODE` environment variable** (any value except `''`, `0`, `false`):
322
322
 
323
323
  ```bash
324
324
  DSH_FORCE_NODE=1 node app.js
325
325
  ```
326
326
 
327
+ This swaps **only the spawn mechanism**. The runtime that executes your scripts and the way dollar-shell re-launches the current runtime (`currentExecPath` / `runFileArgs` / `cwd`) stay native — a forced child of Bun or Deno is still launched as `bun run …` / `deno run …`, never a bare `node`.
328
+
327
329
  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
330
 
329
331
  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:
@@ -345,4 +347,4 @@ The package also ships a Node-streams facade at `dollar-shell/node`:
345
347
  import {$, $$, $sh, shell, sh, spawn} from 'dollar-shell/node';
346
348
  ```
347
349
 
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`.
350
+ 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 spawns through the Node backend, so on Bun and Deno it runs `node:child_process` through their compatibility layer; like `DSH_FORCE_NODE`, this changes only the spawn mechanism — the runtime that executes your scripts and how it is re-launched stay native. Type declarations live in `src/node/index.d.ts`.
package/llms.txt CHANGED
@@ -65,11 +65,11 @@ await $sh({stdout: 'inherit'})`echo ${raw('"hello"')}`;
65
65
 
66
66
  ## Forcing the Node backend
67
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`.
68
+ By default each runtime uses its native backend. Set the `DSH_FORCE_NODE` environment variable (e.g. `DSH_FORCE_NODE=1`) to make every runtime spawn through the Node backend (`node:child_process`; Bun and Deno run it on their Node compat). This swaps **only the spawn mechanism** — the runtime that runs your code and how dollar-shell re-launches it stay native, so a forced child of Bun/Deno is still `bun run …` / `deno run …`, never a bare `node`. 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
69
 
70
70
  ## Node streams (`dollar-shell/node`)
71
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).
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 spawns through the Node backend (`node:child_process`, run on Bun/Deno via their Node compat); only the spawn mechanism changes — the runtime launch stays native.
73
73
 
74
74
  ## Docs
75
75
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dollar-shell",
3
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",
4
+ "version": "1.2.1",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./src/index.d.ts",
@@ -77,7 +77,7 @@
77
77
  "@types/deno": "^2.7.0",
78
78
  "@types/node": "^25.9.1",
79
79
  "prettier": "^3.8.3",
80
- "tape-six": "^1.10.0",
80
+ "tape-six": "^1.10.1",
81
81
  "typescript": "^6.0.3"
82
82
  },
83
83
  "tape6": {
package/src/index.js CHANGED
@@ -35,16 +35,21 @@ const envForceNode = () => {
35
35
  const forceNode =
36
36
  isFlagOn(/** @type {any} */ (globalThis).DSH_FORCE_NODE) || isFlagOn(envForceNode());
37
37
 
38
- let modSpawn;
39
- if (forceNode) {
40
- modSpawn = await import('./spawn/node.js');
41
- } else if (typeof Deno !== 'undefined') {
42
- modSpawn = await import('./spawn/deno.js');
38
+ // The runtime-native backend defines how to launch *this* runtime — `currentExecPath`,
39
+ // `runFileArgs`, `cwd`. DSH_FORCE_NODE forces only the spawn *implementation*
40
+ // (`node:child_process`); it must not change which runtime we target or how it is
41
+ // invoked, so a forced child of Bun/Deno is still `bun run …` / `deno run …`, never a
42
+ // bare `node`. (`process.execPath` is already the real runtime under Bun/Deno node
43
+ // compat; `runFileArgs` is what actually differs — `[]` for node vs `['run']`.)
44
+ let modRuntime;
45
+ if (typeof Deno !== 'undefined') {
46
+ modRuntime = await import('./spawn/deno.js');
43
47
  } else if (typeof Bun !== 'undefined') {
44
- modSpawn = await import('./spawn/bun.js');
48
+ modRuntime = await import('./spawn/bun.js');
45
49
  } else {
46
- modSpawn = await import('./spawn/node.js');
50
+ modRuntime = await import('./spawn/node.js');
47
51
  }
52
+ const modSpawn = forceNode ? await import('./spawn/node.js') : modRuntime;
48
53
 
49
54
  export const {
50
55
  spawn,
@@ -59,6 +64,11 @@ export const {
59
64
  shell,
60
65
  sh,
61
66
  $sh
62
- } = await buildApi(modSpawn);
67
+ } = await buildApi({
68
+ spawn: modSpawn.spawn,
69
+ cwd: modRuntime.cwd,
70
+ currentExecPath: modRuntime.currentExecPath,
71
+ runFileArgs: modRuntime.runFileArgs
72
+ });
63
73
 
64
74
  export default $;
package/src/node/index.js CHANGED
@@ -2,13 +2,26 @@
2
2
 
3
3
  // The Node-streams facade: identical API to the main entry, but stdin/stdout/stderr
4
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.
5
+ // Spawning always goes through node:child_process (on Bun/Deno via their node compat) —
6
+ // that is the point of this entry. But, like DSH_FORCE_NODE on the main entry, this must
7
+ // affect only the spawn implementation: `currentExecPath` / `runFileArgs` / `cwd` stay
8
+ // runtime-native, so a child of Bun/Deno is still launched as `bun run …` / `deno run …`,
9
+ // never a bare `node`.
6
10
 
7
11
  import {buildApi} from '../build.js';
8
12
  import * as backend from '../spawn/node.js';
9
13
 
10
14
  export {isWindows, raw, winCmdEscape} from '../utils.js';
11
15
 
16
+ let modRuntime;
17
+ if (typeof Deno !== 'undefined') {
18
+ modRuntime = await import('../spawn/deno.js');
19
+ } else if (typeof Bun !== 'undefined') {
20
+ modRuntime = await import('../spawn/bun.js');
21
+ } else {
22
+ modRuntime = backend;
23
+ }
24
+
12
25
  export const {
13
26
  spawn,
14
27
  cwd,
@@ -24,9 +37,9 @@ export const {
24
37
  $sh
25
38
  } = await buildApi({
26
39
  spawn: backend.nodeStreamSpawn,
27
- cwd: backend.cwd,
28
- currentExecPath: backend.currentExecPath,
29
- runFileArgs: backend.runFileArgs
40
+ cwd: modRuntime.cwd,
41
+ currentExecPath: modRuntime.currentExecPath,
42
+ runFileArgs: modRuntime.runFileArgs
30
43
  });
31
44
 
32
45
  export default $;