diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml
index 3740af10..21538718 100644
--- a/.github/workflows/js.yml
+++ b/.github/workflows/js.yml
@@ -330,6 +330,7 @@ jobs:
node --test js/tests/node-terminal-artifacts.mjs
node --test js/tests/terminal-changes.test.mjs
node --test js/tests/core-entry.test.mjs
+ node --test js/tests/command-builder.node.test.mjs
node --test js/tests/node-commonjs-entry.mjs
node --test js/tests/node-process-regressions.mjs
node --test js/tests/windows-tree-kill.test.mjs
diff --git a/README.md b/README.md
index 6981fb72..76ca7623 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,10 @@ The [stream processing guide](./docs/stream-processing-guide.md) covers both
languages, deadline batching, backpressure, live analytics, and comparisons
with existing streaming APIs.
+The [complete API and feature comparison](./js/docs/COMPARISON.md) covers the
+tracked JavaScript and Rust alternatives; the [builder guide](./js/docs/COMMAND-BUILDER.md)
+documents the command construction and execution styles.
+
## Repository Layout
| Path | Purpose |
diff --git a/dev/log/issues/34/pulls/113/analysis/requirements.md b/dev/log/issues/34/pulls/113/analysis/requirements.md
new file mode 100644
index 00000000..8e3937db
--- /dev/null
+++ b/dev/log/issues/34/pulls/113/analysis/requirements.md
@@ -0,0 +1,39 @@
+# Issue 34 / PR 113 acceptance evidence
+
+Sources reviewed: the complete [issue](https://github.com/link-foundation/command-stream/issues/34), its empty comment collection, the PR description, all conversation comments (including the [latest quality/comparison request](https://github.com/link-foundation/command-stream/pull/113#issuecomment-6383907892)), inline comments and reviews, and merged PRs 225 and 236. There are no required sub-issues. The implementation is reconciled with main commit `2f8cea139a1b00f3067b11785616f3f2ea00ecc4`.
+
+| Requirement | Implementation and verification |
+| --- | --- |
+| Exact `$.command('cat', path).pipe($.command.stdout('inherit'), $.command.exitCode).run()` example | `js/src/command-builder.mjs`; test `the exact issue example inherits stdout and returns an exit code` in `js/tests/command-builder.test.mjs` executes a real temporary file. |
+| Avoid quoting and shell injection; produce a runnable string | Native exec specification, no shell reparsing; quote executable and each argument for POSIX rendering. Tests round-trip empty strings, spaces, quotes, newlines, operators and command substitutions, plus executable paths containing special characters. Windows runs argv directly; rendered text is explicitly POSIX. |
+| Immediate execution and reusable descriptions | Await/run, synchronous execution, and runner/stream access; immutable copies retain argument/configuration state. Tests execute descriptions repeatedly and verify result/exit-code selection. |
+| Support API styles and configurations from the PR feedback | Variadic and argv-array factories, constructor, fluent methods, functional transforms, option objects, nested factory defaults, configured tags and `create`; all ProcessOptions pass through the existing runner. Tests cover environment precedence/removal, cwd, buffered/live stdin, stdio, capture/mirror, overrides, deadlines, abort, events, pipelines and streams. Node tests cover full/core ESM/CommonJS; strict TypeScript tests cover the public exports and signatures. |
+| Rust counterpart and the referenced Rust Command patterns | Owned fluent/functional composition, env/envs/clear/removal, stdio, cwd, async and sync execution using one native command configuration. A 160 KiB stdin/stdout test verifies concurrent draining instead of blocking on a full pipe. |
+| Resolve conflicts without removing current features | Merge main into the prepared branch, retain the monorepo layout and current core/compatibility implementations; only the obsolete draft builder tests/examples are replaced. Compare the final diff against main and run the existing suites. |
+| Clear docs and comparison across all features | `js/docs/COMMAND-BUILDER.md` documents every builder operation and runner option, precedence, execution contracts and safety boundaries. `js/docs/COMPARISON.md` covers process, shell, streams, lifecycle, compatibility, terminal, translation and development features with primary-source links, covering all twelve projects in the pinned JavaScript audit, both issue references, and the fourteen-project Rust audit. `rust/docs/COMPARISON.md` now gives a per-project construction, I/O, configuration, lifecycle and tooling comparison for all fourteen Rust projects. Generated feature examples provide further per-feature alternatives. Both audits identify implemented features accurately and preserve exact upstream conformance limits. |
+| Competitive claims supported by evidence | Credit competitors' argv safety, fluent options, streams, transforms, plugins and native process controls. Link import-budget checks, compatibility corpora, feature examples and benchmarks. No unmeasured claim that one library universally outperforms every competitor; the comparison records specialized contracts and runtime limits explicitly. |
+| Prepare release and review | JavaScript minor changeset and Rust minor changelog fragment; update PR title/body, preserve `Fixes #34`, verify current-SHA checks and mark ready without merging. |
+
+## Reproduced failures and root causes
+
+Before implementation, the focused JavaScript suite failed on missing fluent methods, array/options construction, preset factories and per-run configuration. The main builder exposed only a subset of those entry styles and ignored run arguments. Preset env objects also remained caller-owned until command construction; a separate failing regression demonstrated that mutation leak before snapshots were added.
+
+Review found another env-preset failure: `command.with({env: {A: 'a'}})('echo', [])` dereferenced an absent options argument. The array/preset regression was run failing before `mergeOptions` received its default override object.
+
+The Rust reproduction initially failed to compile with E0599 at `.pipe` and `.env_clear`. Adding synchronous execution and sharing native command configuration across sync/async resolves those missing contracts while retaining literal OsString arguments and signal exit status normalization.
+
+All regression cases remain in the normal automated suites. Investigation logs are kept under `.git/issue-34/` locally; fresh remote failures, if any, are downloaded under `ci-logs/` and checked against commit timestamps and SHA.
+
+## Follow-up quality audit
+
+- New failing runtime tests demonstrated that synchronous native execution always piped stdio, ignored `capture:false`, and failed to forward inherited stdin. The extracted shared sync module now applies native stdio modes, emits only piped data, and discards disabled capture. Node subprocess tests verify output appears exactly once and parent stdin reaches the child; Bun tests cover ignored output and events with capture disabled.
+- A failing mutation regression showed that `preferLocal` directory arrays remained caller-owned. Options snapshots now also copy/freeze those arrays and ANSI configuration, with regressions for presets and direct construction. Buffers and AbortSignals remain explicitly shared resources.
+- Failing strict TypeScript assertions exposed the old variadic `any` fallback: long pipelines could erase exit-code selection and join incompatible result types. Typed overloads now validate up to twenty transforms, including generic configuration after selection; homogeneous transforms can be composed without that limit or pipelines can be chained. A separate failing test at the twentieth-transform boundary caught a shadowed type parameter in the draft overload; its input and output types now remain independent.
+- Rust's environment changes now replay in configuration order through native Command rather than a case-sensitive unordered map. A cross-platform fixture verifies replacement/removal, re-addition and Windows case-insensitive versus Unix case-sensitive names in sync and async executions. The Windows branch is verified by the hosted Windows matrix, not inferred from a Linux run.
+- Environment-clear/removal accounting was stale in both audits. Three Deno units and ten Rust units now map to executable public invariants in the paired decisions/dispositions ledgers. Other entries state which core/adapter implementations exist and which exact upstream contracts are still unported; adding an option is not treated as proof of full upstream conformance.
+
+The complete comparison uses the published twelve-project JavaScript and fourteen-project Rust discovery sets, plus Effect as the issue reference. Universal superiority over every open-source project is not established: specialized native handles, expect sessions, persistent result caching and domain return adapters remain documented differences. No unmeasured performance ranking or complete upstream API conformance is claimed.
+
+Validation logs from this continuation live in `.git/issue-34/`. The first full Rust attempt failed at `rust/tests/cancelled_output.rs:105` (exit -1 versus 0), while its immediate focused rerun passed; the same intermittent cancellation-test behavior is recorded in `dev/log/issues/219/pulls/222/README.md`. The full serial rerun passed 962 tests with 7 ignored, without increasing that test's deadlines. Rust formatting, all-target/all-feature Clippy with warnings denied, and rustdoc with warnings denied also pass. JavaScript lint, formatting, duplication and strict type checks pass, including the twentieth-transform regression. All 37 feature examples pass parity checks in JavaScript and Rust, generated documentation is current (39 files), and documentation validation passes all 251 checks including the new Rust comparison.
+
+The final full Bun run passed 2,899 tests with 10 skipped and no failures (66,897 assertions across 183 files). The standalone Node builder suite passed all 7 tests. Hosted checks must be verified against the new commit SHA before the PR is marked ready; the PR description records their final status without treating the earlier commit's passing checks as current evidence.
diff --git a/docs/README.md b/docs/README.md
index e7110412..8092726e 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -88,7 +88,7 @@ All 37 examples were executed in JavaScript and Rust. JavaScript was checked in
| [Execa compatibility mode](features/execa-compat.md) | ✓ | — | — | ✓ | — | — |
| [ShellJS compatibility mode](features/shelljs-compat.md) | ✓ | — | — | — | ✓ | — |
| [Native head, tail, sort and uniq](features/native-text.md) | ✓ | — | ✓ | ✓ | ✓ | ✓ |
-| [Build commands with literal arguments](features/command-builder.md) | ✓ | — | — | ✓ | — | ✓ |
+| [Build commands with literal arguments](features/command-builder.md) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| [Process streams incrementally](features/stream-processing.md) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| [Track virtual terminal frames](features/terminal-buffer.md) | ✓ | — | — | — | — | — |
| [Compose and extend virtual commands](features/command-packages.md) | ✓ | — | — | — | — | — |
diff --git a/docs/features/command-builder.md b/docs/features/command-builder.md
index bee9a32c..9893cf96 100644
--- a/docs/features/command-builder.md
+++ b/docs/features/command-builder.md
@@ -88,11 +88,15 @@ exit code: 7
### [Bun.$](https://bun.com/docs/runtime/shell)
-Not supported — No command-stream transform API; invoke the executable with separate arguments.
+```js
+await Bun.spawn(['printf', '%s', 'literal ; text']).exited; // exact argv
+```
### [zx](https://github.com/google/zx)
-Not supported — No command-stream transform API; invoke the executable with separate arguments.
+```js
+await $`printf %s ${'literal ; text'}`; // escaped shell interpolation
+```
### [execa](https://github.com/sindresorhus/execa)
@@ -102,7 +106,9 @@ await execa('printf', ['%s', 'literal ; text']);
### [ShellJS](https://github.com/shelljs/shelljs)
-Not supported — No command-stream transform API; invoke the executable with separate arguments.
+```js
+shell.cmd('printf', '%s', 'literal ; text'); // exact argv
+```
### [node:child_process](https://nodejs.org/api/child_process.html)
diff --git a/docs/screenshots/command-builder-after.png b/docs/screenshots/command-builder-after.png
new file mode 100644
index 00000000..f6b32653
Binary files /dev/null and b/docs/screenshots/command-builder-after.png differ
diff --git a/docs/screenshots/command-builder-before.png b/docs/screenshots/command-builder-before.png
new file mode 100644
index 00000000..0961b46a
Binary files /dev/null and b/docs/screenshots/command-builder-before.png differ
diff --git a/docs/site/index.html b/docs/site/index.html
index fccbdef5..98352dc0 100644
--- a/docs/site/index.html
+++ b/docs/site/index.html
@@ -1111,16 +1111,10 @@
command-stream — feature comparison
"file": "js/examples/features/command-builder.mjs",
"api": ["command", "CommandBuilder"],
"alternatives": {
- "bun-shell": {
- "unsupported": "No command-stream transform API; invoke the executable with separate arguments"
- },
- "zx": {
- "unsupported": "No command-stream transform API; invoke the executable with separate arguments"
- },
+ "bun-shell": "await Bun.spawn(['printf', '%s', 'literal ; text']).exited; // exact argv",
+ "zx": "await $`printf %s ${\"literal ; text\"}`; // escaped shell interpolation",
"execa": "await execa('printf', ['%s', 'literal ; text']);",
- "shelljs": {
- "unsupported": "No command-stream transform API; invoke the executable with separate arguments"
- },
+ "shelljs": "shell.cmd('printf', '%s', 'literal ; text'); // exact argv",
"child_process": "spawn('printf', ['%s', 'literal ; text']);"
},
"source": "import { command } from '../../src/$.mjs';\nimport { example } from './_harness.mjs';\nawait example(\n { id: 'command-builder', title: 'Build commands with literal arguments' },\n async ({ record }) => {\n const builder = command(\n process.execPath,\n '-e',\n 'console.log(process.argv[1])',\n 'hello ; $HOME'\n );\n record('literal argument', (await (await builder).text()).trim());\n record(\n 'exit code',\n await command(process.execPath, '-e', 'process.exit(7)')\n .pipe(command.exitCode)\n .run()\n );\n }\n);",
diff --git a/js/.changeset/complete-command-builder.md b/js/.changeset/complete-command-builder.md
new file mode 100644
index 00000000..82f79880
--- /dev/null
+++ b/js/.changeset/complete-command-builder.md
@@ -0,0 +1,9 @@
+---
+'command-stream': minor
+---
+
+Complete literal command builders with immutable fluent methods, argv arrays,
+factory and shell presets, merged environment layers, per-run overrides and
+synchronous execution. Honor inherited/ignored stdio and disabled capture in
+sync calls, snapshot nested configuration, and preserve long pipeline types.
+Document every API style and correct the library-wide competitor comparison.
diff --git a/js/README.md b/js/README.md
index a3e88254..89907977 100644
--- a/js/README.md
+++ b/js/README.md
@@ -16,6 +16,9 @@ A modern $ shell utility library with streaming, async iteration, and EventEmitt
## Features
+The [command builder guide](docs/COMMAND-BUILDER.md) documents literal argv,
+fluent and functional configuration, presets, run/await/sync and streams.
+
- 🐚 **Shell-like by Default**: Commands behave exactly like running in terminal (stdout→stdout, stderr→stderr, stdin→stdin)
- 🎛️ **Fully Controllable**: Override default behavior with options (`mirror`, `capture`, `stdin`)
- 🚀 **Multiple Usage Patterns**: Classic await, async iteration, EventEmitter, .pipe() method, and mixed patterns
@@ -30,54 +33,15 @@ A modern $ shell utility library with streaming, async iteration, and EventEmitt
## Comparison with Other Libraries
-| Feature | [**command-stream**](https://github.com/link-foundation/command-stream) | [**execa**](https://github.com/sindresorhus/execa) | [**cross-spawn**](https://github.com/moxystudio/node-cross-spawn) | [**Bun.$**](https://github.com/oven-sh/bun) | [**ShellJS**](https://github.com/shelljs/shelljs) | [**zx**](https://github.com/google/zx) |
-| ------------------------------ | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
-| **📦 NPM Package** | [](https://www.npmjs.com/package/command-stream) | [](https://www.npmjs.com/package/execa) | [](https://www.npmjs.com/package/cross-spawn) | N/A (Built-in) | [](https://www.npmjs.com/package/shelljs) | [](https://www.npmjs.com/package/zx) |
-| **⭐ GitHub Stars** | [**⭐ 2** (Please ⭐ us!)](https://github.com/link-foundation/command-stream) | [⭐ 7,264](https://github.com/sindresorhus/execa) | [⭐ 1,149](https://github.com/moxystudio/node-cross-spawn) | [⭐ 80,169](https://github.com/oven-sh/bun) (Full Runtime) | [⭐ 14,375](https://github.com/shelljs/shelljs) | [⭐ 44,569](https://github.com/google/zx) |
-| **📊 Monthly Downloads** | **893** (New project!) | **381M** | **409M** | N/A (Built-in) | **35M** | **4.2M** |
-| **📈 Total Downloads** | **Growing** | **6B+** | **5.4B** | N/A (Built-in) | **596M** | **37M** |
-| **Runtime Support** | ✅ Bun + Node.js (+ Deno for `command-stream/bun`) | ✅ Node.js | ✅ Node.js | 🟡 Bun only | ✅ Node.js | ✅ Node.js |
-| **Template Literals** | ✅ `` $`cmd` `` | ✅ `` $`cmd` `` | ❌ Function calls | ✅ `` $`cmd` `` | ❌ Function calls | ✅ `` $`cmd` `` |
-| **Real-time Streaming** | ✅ Live output | 🟡 Limited | ✅ Native ChildProcess stdout/stderr streams | ❌ Buffer only | ❌ Buffer only | ❌ Buffer only |
-| **Synchronous Execution** | ✅ `.sync()` with events | ✅ `execaSync` | ✅ `spawnSync` | ❌ No | ✅ Sync by default | ❌ No |
-| **Async Iteration** | ✅ `for await (chunk of $.stream())` | ❌ No | ✅ Node readable streams support async iteration | ❌ No | ❌ No | ✅ Stdout lines: ``for await (const line of $`cmd`)`` |
-| **EventEmitter Pattern** | ✅ `.on('data', ...)` | 🟡 Limited events | ✅ Native ChildProcess and stream events | ❌ No | ❌ No | ❌ No |
-| **Mixed Patterns** | ✅ Events + await/sync | ❌ No | ❌ No | ❌ No | ❌ No | ❌ No |
-| **Bun.$ Compatibility** | ✅ **Full API** in [`command-stream/bun`](docs/BUN_SHELL.md), tested with Bun's own tests | ❌ No | ❌ No | ✅ Native API | ❌ No | ❌ No |
-| **Shell Injection Protection** | ✅ Smart auto-quoting | ✅ Safe by default | ✅ Safe by default | ✅ Built-in | 🟡 Manual escaping | ✅ Safe by default |
-| **Cross-platform** | ✅ macOS/Linux/Windows | ✅ Yes | ✅ **Specialized** cross-platform | ✅ Yes | ✅ Yes | ✅ Yes |
-| **Performance** | [Measured](benchmarks/README.md) | [Measured](benchmarks/README.md) | [Measured](benchmarks/README.md) | [Measured](benchmarks/README.md) | [Measured](benchmarks/README.md) | [Measured](benchmarks/README.md) |
-| **Memory Efficiency** | [Measured](benchmarks/README.md) | [Measured](benchmarks/README.md) | [Measured](benchmarks/README.md) | [Measured](benchmarks/README.md) | [Measured](benchmarks/README.md) | [Measured](benchmarks/README.md) |
-| **Error Handling** | ✅ Configurable (`set -e`/`set +e`, non-zero OK by default) | ✅ Throws on error | ❌ Basic (exit codes) | ✅ Throws on error | ✅ Configurable | ✅ Throws on error |
-| **Shell Settings** | ✅ `set -e`/`set +e` equivalent | ❌ No | ❌ No | ❌ No | 🟡 Limited (`set()`) | ❌ No |
-| **Stdout Support** | ✅ Real-time streaming + events | ✅ Node.js streams + interleaved | ✅ Inherited/buffered | ✅ Shell redirection + buffered | ✅ Direct output | ✅ Readable streams + `.pipe.stdout` |
-| **Stderr Support** | ✅ Real-time streaming + events | ✅ Streams + interleaved output | ✅ Inherited/buffered | ✅ Redirection + `.quiet()` access | ✅ Error output | ✅ Readable streams + `.pipe.stderr` |
-| **Stdin Support** | ✅ string/Buffer/inherit/ignore | ✅ Input/output streams | ✅ Full stdio support | ✅ Pipe operations | 🟡 Basic | ✅ Basic stdin |
-| **Built-in Commands** | ✅ **26 commands**: cat, ls, mkdir, rm, mv, cp, touch, basename, dirname, seq, yes + all Bun.$ commands | ❌ Uses system | ❌ Uses system | ✅ echo, cd, etc. | ✅ **20+ commands**: cat, ls, mkdir, rm, mv, cp, etc. | ❌ Uses system |
-| **Virtual Commands Engine** | ✅ **Revolutionary**: Register JavaScript functions as shell commands with full pipeline support | ❌ No custom commands | ❌ No custom commands | ❌ No extensibility | ❌ No custom commands | ❌ No custom commands |
-| **Pipeline/Piping Support** | ✅ **Advanced**: System + Built-ins + Virtual + Mixed + `.pipe()` method | ✅ Programmatic `.pipe()` + multi-destination | ✅ Child streams can be piped | ✅ Standard shell piping | ✅ Shell piping + `.to()` method | ✅ Shell piping + `.pipe()` method |
-| **Bundle Size** | [Measured](benchmarks/README.md) | [Measured](benchmarks/README.md) | [Measured](benchmarks/README.md) | [Measured](benchmarks/README.md) | [Measured](benchmarks/README.md) | [Measured](benchmarks/README.md) |
-| **Signal Handling** | ✅ **Advanced SIGINT/SIGTERM forwarding** with cleanup | 🟡 Basic | ✅ **Excellent** cross-platform | 🟡 Basic | 🟡 Basic | 🟡 Basic |
-| **Process Management** | ✅ **Robust child process lifecycle** with proper termination | ✅ Good | ✅ **Excellent** spawn wrapper | ❌ Basic | 🟡 Limited | 🟡 Limited |
-| **Debug Tracing** | ✅ **Comprehensive VERBOSE logging** for CI/debugging | 🟡 Limited | ❌ No | ❌ No | 🟡 Basic | ❌ No |
-| **Test Coverage** | ✅ **518+ tests, 1165+ assertions** | ✅ Excellent | ✅ Good | 🟡 Good coverage | ✅ Good | 🟡 Good |
-| **CI Reliability** | ✅ **Platform-specific handling** (macOS/Ubuntu) | ✅ Good | ✅ **Excellent** | 🟡 Basic | ✅ Good | 🟡 Basic |
-| **Documentation** | ✅ **Comprehensive examples + guides** | ✅ Excellent | 🟡 Basic | ✅ Good | ✅ Good | 🟡 Limited |
-| **TypeScript** | ✅ **Bundled strict declarations** (ESM + CommonJS), typed events, streams and virtual commands | ✅ Full support | ✅ Built-in | ✅ Built-in | 🟡 Community types | ✅ Full support |
-| **License** | ✅ **Unlicense (Public Domain)** | 🟡 MIT | 🟡 MIT | 🟡 MIT (+ LGPL dependencies) | 🟡 BSD-3-Clause | 🟡 Apache 2.0 |
-
-Performance, memory, and package-size values depend on the runtime and host.
-Use the [reproducible benchmark playground](benchmarks/README.md) for measured
-same-host comparisons, raw statistics, and an interactive report rather than
-static estimates.
-
-**📊 Popularity & Adoption:**
-
-- **⭐ GitHub Stars:** [Bun: 80,169](https://github.com/oven-sh/bun) • [zx: 44,569](https://github.com/google/zx) • [ShellJS: 14,375](https://github.com/shelljs/shelljs) • [execa: 7,264](https://github.com/sindresorhus/execa) • [cross-spawn: 1,149](https://github.com/moxystudio/node-cross-spawn) • [**command-stream: 2 ⭐ us!**](https://github.com/link-foundation/command-stream)
-- **📈 Total Downloads:** [execa: 6B+](https://www.npmjs.com/package/execa) • [cross-spawn: 5.4B](https://www.npmjs.com/package/cross-spawn) • [ShellJS: 596M](https://www.npmjs.com/package/shelljs) • [zx: 37M](https://www.npmjs.com/package/zx) • [command-stream: Growing](https://www.npmjs.com/package/command-stream)
-- **📊 Monthly Downloads:** [cross-spawn: 409M](https://www.npmjs.com/package/cross-spawn) • [execa: 381M](https://www.npmjs.com/package/execa) • [ShellJS: 35M](https://www.npmjs.com/package/shelljs) • [zx: 4.2M](https://www.npmjs.com/package/zx) • [command-stream: 893 (growing!)](https://www.npmjs.com/package/command-stream)
-
-**⭐ Help Us Grow!** If command-stream's **revolutionary virtual commands** and **advanced streaming capabilities** help your project, [**please star us on GitHub**](https://github.com/link-foundation/command-stream) to help the project grow!
+The [complete feature and API comparison](docs/COMPARISON.md) covers command
+construction, all execution styles, streaming, configuration, process control,
+compatibility layers and development tools across the tracked competitors.
+It credits competitors' literal argv, streams, async iteration and plugin APIs,
+and links to primary references and the executable compatibility audits.
+
+Use the [benchmark playground](benchmarks/README.md) for measured process latency,
+memory and package size on the same host. The [core entry](docs/CORE.md) keeps
+shell-only imports below the enforced module budget.
### Tracked compatibility corpus
@@ -91,18 +55,11 @@ Run the focused executable corpus with `bun run test:competitors`.
### Why Choose command-stream?
-- **🆓 Truly Free**: **Unlicense (Public Domain)** - No restrictions, no attribution required, use however you want
-- **🚀 Revolutionary Virtual Commands**: **World's first** fully customizable virtual commands engine - register JavaScript functions as shell commands!
-- **🔗 Advanced Pipeline System**: **Only library** where virtual commands work seamlessly in pipelines with built-ins and system commands
-- **🔧 Built-in Commands**: **18 essential commands** work identically across all platforms - no system dependencies!
-- **📡 Real-time Processing**: Typed stdout, stderr, and exit chunks through async iteration
-- **🔄 Flexible Patterns**: Multiple usage patterns (await, events, iteration, mixed)
-- **🐚 Shell Replacement**: Dynamic error handling with `set -e`/`set +e` equivalents for .sh file replacement
-- **⚡ Bun Optimized**: Designed for Bun with Node.js fallback compatibility
-- **💾 Memory Efficient**: Streaming prevents large buffer accumulation
-- **🛡️ Production Ready**: **518+ tests, 1165+ assertions** with comprehensive coverage including CI reliability
-- **🎯 Advanced Signal Handling**: Robust SIGINT/SIGTERM forwarding with proper child process cleanup
-- **🔍 Debug-Friendly**: Comprehensive VERBOSE tracing for CI debugging and troubleshooting
+- Combine literal command builders, shell templates, events, sync execution and
+ async iteration through a shared runner.
+- Reuse scripts through the Bun, zx, ShellJS, Execa and cross-spawn APIs.
+- Add virtual commands, packages, stream analytics, terminal tools and a debugger.
+- Use the Unlicense and maintained JavaScript and Rust implementations.
## Built-in Commands (🚀 NEW!)
diff --git a/js/docs/ADVANCED-WORKFLOWS.md b/js/docs/ADVANCED-WORKFLOWS.md
index 4e99e67b..f5980558 100644
--- a/js/docs/ADVANCED-WORKFLOWS.md
+++ b/js/docs/ADVANCED-WORKFLOWS.md
@@ -2,6 +2,9 @@
## Literal command builders
+See the [complete command builder API](COMMAND-BUILDER.md) for fluent methods,
+functional transforms, argv arrays, presets, sync execution and overrides.
+
```js
import $ from 'command-stream';
const cat = $.command('cat', 'file with spaces');
diff --git a/js/docs/COMMAND-BUILDER.md b/js/docs/COMMAND-BUILDER.md
new file mode 100644
index 00000000..827e6c2f
--- /dev/null
+++ b/js/docs/COMMAND-BUILDER.md
@@ -0,0 +1,163 @@
+# Command builder API
+
+`$.command`, the named `command` factory, and `CommandBuilder` construct lazy,
+immutable descriptions. Execution passes the executable and arguments directly
+to ProcessRunner in `exec` mode. No shell parses them. This API is available from
+`command-stream`, `command-stream/core`, and `command-stream/process-runner`;
+ESM and CommonJS share the same implementation.
+
+## Build and execute
+
+```js
+import { $, command, CommandBuilder } from 'command-stream';
+
+// The API from issue #34: inherit output and resolve to the integer exit code.
+const code = await $.command('cat', './some-file.txt')
+ .pipe($.command.stdout('inherit'), $.command.exitCode)
+ .run();
+
+const base = command('node', '-e', 'console.log(process.argv[1])');
+const configured = base.arg('hello ; $HOME').stdout('pipe').mirror(false);
+const result = await configured.run();
+console.log(await result.text()); // hello ; $HOME\n
+
+// Equivalent construction styles.
+const argv = command('node', ['-e', 'console.log(42)'], { capture: true });
+const explicit = new CommandBuilder('node', ['-e', 'console.log(42)']);
+const status = explicit.exitCode().sync();
+```
+
+Every configuration method returns a new builder. Keep the return value when
+adding arguments or options. `base` above remains unchanged. Each `await builder`,
+`.run()` or `.sync()` starts a fresh execution, so running a description twice
+executes it twice. `.run()` returns a promise of the result; `.runner()` exposes
+the ProcessRunner when events, live stdin, killing or process pipelines are needed.
+
+| Construction or operation | Behavior |
+| --------------------------------------------------------------------------- | ------------------------------------------------------------ |
+| `command(file, ...args)` | Variadic literal arguments |
+| `command(file, args, options?)` | An argv array plus optional ProcessOptions |
+| `new CommandBuilder(file, args?, options?)` | Explicit construction |
+| `.arg(...args)` / `command.arg(...args)` | Append arguments, preserving empty strings |
+| `.with(options)` / `command.options(options)` | Configure any ProcessOptions |
+| `.stdin(input)` / `command.stdin(input)` | String, Buffer, `pipe`, `inherit`, or `ignore` |
+| `.stdout(mode)`, `.stderr(mode)` / matching transforms | `pipe`, `inherit`, or `ignore` |
+| `.cwd(path)`, `.env(vars)` / matching transforms | Set directory; merge successive explicit environment layers |
+| `.capture(enabled = true)`, `.mirror(enabled = true)` / matching transforms | Collect output; independently mirror piped output live |
+| `.pipe(...transforms)` | Apply synchronous builder-to-builder configuration functions |
+| `.exitCode()` / `command.exitCode` | Select an integer status instead of the full result |
+| `.run(options?)`, `await builder` | Execute asynchronously |
+| `.sync(options?)` | Execute synchronously, applying the same result selection |
+| `.runner(options?)` | Get a fresh, lazy ProcessRunner |
+| `.stream(queueOptions?)` | Iterate raw chunks or use line-processing operators |
+| `.toString()`, `String(builder)`, `.buildCommand()` | POSIX-quoted executable and argv |
+| `.file` (`.cmd` alias), `.args`, `.options` | Inspect the description |
+
+Arguments may be strings, numbers, booleans or bigints; scalars are converted to
+strings. Objects, null, undefined, nested arrays, NUL bytes and empty executable
+names are rejected before spawning. Executable names are path strings.
+
+## Options, presets and precedence
+
+```js
+const local = command.with({ cwd: '/workspace', mirror: false });
+const child = local.with({ env: { ...process.env, MODE: 'test' } });
+const builder = child('node', ['-e', 'console.log(process.env.MODE)'])
+ .env({ EXTRA: 'value' })
+ .pipe(command.options({ timeout: 5000 }), command.capture());
+const result = await builder.run({ env: { MODE: 'production' } });
+
+// Shell presets also expose a command factory with the same defaults.
+const shell = $({ mirror: false, cwd: '/workspace' });
+await shell.command('node', '--version');
+```
+
+Precedence is builder defaults, factory presets, array-call options, chained
+configuration, then per-execution overrides. Later values win. Explicit `env`
+layers merge with each other; setting a key to `undefined` removes it. When `env`
+is supplied, it replaces the inherited process environment: include
+`...process.env` to extend the parent. An initial `env: {}` starts with an empty environment.
+Options, their env and ANSI maps, and `preferLocal` directory arrays are copied
+and frozen when bound; later caller edits cannot change them. Buffers and
+AbortSignals remain shared resources.
+
+Builders default to `capture:true` and `mirror:false`. ProcessRunner's other
+settings apply, including inherited stdin. Inherit/ignore stdout or stderr is
+never captured or emitted as data. To both capture and print output, use
+`stdout('pipe').capture(true).mirror(true)`. With `capture:false`, events and
+stream iteration still receive piped output, while the result retains no output.
+
+All ProcessOptions are accepted through `.with()`, factory presets and run/runner
+options: stdio, `cwd`, `env`, `capture`, `mirror`, `preferLocal`, `interactive`,
+`signal`, `timeout`, `killSignal`, `killGrace`, `exitPumpGrace`, `detached`, `ansi`,
+and `trace`. Shell-specific `loginShell` and `shellOperators` do not turn a
+literal builder into a shell command. For shell grammar use the tagged template
+API or explicitly execute an interpreter. Synchronous execution follows
+ProcessRunner's sync limitations, including unsupported async virtual handlers.
+AbortSignal cancellation applies to asynchronous execution; use `timeout` for
+synchronous deadlines. Sync calls honor the same inherit/ignore output and
+capture policies. Sync events fire after the child exits, and native sync spawn
+buffers piped output even with `capture:false`; use asynchronous streaming for
+bounded memory. Inherited output bypasses the runner and is never mirrored twice.
+
+```js
+const runner = command('node', ['-e', 'process.stdin.pipe(process.stdout)'])
+ .stdin('pipe')
+ .runner();
+runner.on('stdout', (chunk) => console.log(chunk.toString()));
+runner.start();
+runner.stdin.end('live input');
+await runner;
+
+const source = command('node', '-e', 'console.log("data")').runner();
+const sink = command(
+ 'node',
+ '-e',
+ 'process.stdin.pipe(process.stdout)'
+).runner();
+const result = await source.pipe(sink);
+```
+
+Builder `.pipe()` configures a description. Runner `.pipe()` connects process
+streams. `.stream({highWaterMark, highWaterBytes})` configures queue capacity;
+configure process options on the builder first. Use `capture:false` for bounded
+streaming. The [stream guide](../../docs/stream-processing-guide.md) explains
+line framing, backpressure and cancellation.
+
+## Runnable strings and safety
+
+`String(builder)` quotes the executable as well as every argument for a POSIX
+shell, preserving empty arguments, apostrophes, whitespace, newlines, `$`, and
+shell operators. It renders argv only: environment, cwd, stdio, deadlines and
+result selection are execution configuration, not text in the rendered string.
+Use the builder directly for portable execution; the rendered string is not
+Windows command-line syntax.
+
+Literal argv prevents the shell from interpreting an argument. A target program
+may still interpret its own options or code arguments. In particular, calling
+`command('sh', '-c', untrustedText)` explicitly asks a shell to execute that text.
+
+## Rust
+
+`command_stream::CommandBuilder` offers owned reusable descriptions with
+`arg`, `args`, `cwd`, `env`, `envs`, `env_clear`, `env_remove`, `stdin`, `stdout`,
+`stderr`, and `.pipe(|builder| ...)` transforms. `.run().await` and `.run_sync()`
+return CommandResult; `.exit_code().await` selects status. `.shell_string()`
+renders POSIX argv. Arguments and environment use OsString; configured stdin and
+captured results are UTF-8 strings. Native non-UTF-8 values execute losslessly;
+the shell-string renderer uses lossy text conversion for them.
+Use the Rust ProcessRunner, stream, and compatibility APIs for events,
+cancellation, pipelines and additional execution options.
+
+## Verification and alternatives
+
+Run `bun test js/tests/command-builder.test.mjs js/tests/advanced-command.test.mjs`,
+`node --test js/tests/command-builder.node.test.mjs` and
+`cargo test --manifest-path rust/Cargo.toml --test command_builder`.
+The tests execute hostile argv and executable paths, the issue example, immutable
+presets and env layers, overrides, stdio policies, sync/async selection, events,
+pipelines, streaming, timeouts and cancellation.
+
+See the [JavaScript comparison](COMPARISON.md) and [Rust comparison](../../rust/docs/COMPARISON.md)
+for competitor APIs across the library's features, and the pinned compatibility
+corpora for coverage and limits.
diff --git a/js/docs/COMPARISON.md b/js/docs/COMPARISON.md
new file mode 100644
index 00000000..13843d13
--- /dev/null
+++ b/js/docs/COMPARISON.md
@@ -0,0 +1,121 @@
+# Command execution API comparison
+
+This comparison covers the complete library: command construction, execution,
+streams, shell behavior, process control, compatibility APIs, and development
+tools. It describes public capabilities, with links to executable evidence.
+A differently named API still counts as support. Process APIs that offer safe
+argv, streams or plugins are credited for those features.
+
+## Execution and output
+
+The source links in the column headings are the upstream references, reviewed
+2026-10-24. “Via” names an additional public API or adapter needed for the behavior.
+“Manual” means application code must compose the underlying primitives. A dash
+means no corresponding API is documented in that reference, not that application
+code could never implement the behavior.
+
+| Capability | command-stream | [Execa](https://github.com/sindresorhus/execa) | [cross-spawn](https://github.com/moxystudio/node-cross-spawn) | [Bun Shell](https://bun.com/docs/runtime/shell) | [ShellJS](https://github.com/shelljs/shelljs) | [zx](https://google.github.io/zx/api) |
+| --------------------------------- | ---------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------- | ----------------------------------------------- | --------------------------------------------- | ------------------------------------- |
+| Literal executable + argv | `command`, `exec`, `spawn` | `execa(file, args)` | `spawn(file, args)` | Via `Bun.spawn` | `cmd(file, ...args)` and shell built-ins | Escaped templates / argv-array calls |
+| Tagged templates | `$`, `bun.$`, `zx.$`, Execa `$` | `$`, `execa` | — | `$` | — | `$` |
+| Lazy reusable command description | Immutable `command` builder | Bound defaults; subprocess execution API | Manual | ShellPromise with fluent options | Manual | Lazy process/presets (`halt`) |
+| Functional builder transforms | `builder.pipe(command.stdout(...), command.exitCode)` | Options and pipeline API | Manual | Fluent ShellPromise | Fluent ShellString | Fluent ProcessPromise |
+| Async / sync execution | `run` / `sync`, compatibility entries | `execa` / `execaSync` | `spawn` / `sync` | Async ShellPromise; sync via Bun.spawnSync | Sync or callback `exec` | `$` / `$.sync` |
+| Captured text / bytes | `text`, captured streams / raw stream chunks | Configurable text or bytes | Collect child streams | ShellOutput / `text`, `bytes` | ShellString | ProcessOutput / bytes and text |
+| Live output | Events, Node streams, async chunks | Node/Web streams, events | Child stdout/stderr streams | Inherited output; `lines()` | Async exec child streams | ProcessPromise streams |
+| Async line iteration | `.stream().lines()` / fluent operators | Progressive iteration | Readable stream + framing | `lines()` | Manual framing | `lines()` / iterator |
+| Events and awaited result | ProcessRunner; `runner()` from builder | Child events + promise | Child events; manual promise | Promise and line API | Async exec events + callback | ProcessPromise events + promise |
+| stdin policies and data | Inherit/ignore/pipe, strings, Buffer, live writable; richer inputs via Execa | Files, streams, iterables, objects, text, bytes | Node stdio | Shell redirects and JS objects | ShellString pipes; exec child stdin | Input data, streams, process results |
+| Separate / interleaved stderr | Separate core streams; `all` via Execa | Separate and `all` | Separate streams; manual interleave | Redirection, ShellOutput stderr | ShellString stderr | Separate and combined ProcessOutput |
+| Capture and print together | `capture:true`, `mirror:true` | Configurable stdout/stderr | Manual tee | Default inherited output plus result | Configurable silent mode | `quiet:false` and ProcessOutput |
+| Real process pipelines | Runner `.pipe`, shell pipes, compatibility APIs | `.pipe`, multiple sources/destinations | Pipe child streams | Shell `\|` | ShellString methods / shell pipes | `.pipe`, shell pipes |
+| Output transforms / backpressure | Async map/filter/reduce, batch/window/split/merge, bounded queues | Transform functions, streams | Node streams | Shell stages / line iteration | String/array transforms | Node stream transforms |
+| Stream statistics | Incremental throughput/error/latency statistics | Application transform | Application stream | Application iteration | Application code | Application stream |
+
+## Configuration, shell behavior and lifecycle
+
+| Capability | command-stream | Execa | cross-spawn | Bun Shell | ShellJS | zx |
+| ------------------------------------ | ----------------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------ | -------------------------------- | ------------------------------------------------ | ------------------------------ |
+| cwd / environment / presets | Builder methods/transforms/options, `create`, `$({...})` | Options and bound defaults | Spawn options | `cwd`, `env`, defaults | `cd`, `env`, config | Options and chained presets |
+| Argument safety | Exact argv builder; escaped templates; explicit raw escape hatch | Exact argv by default | Exact argv with shell disabled | Escaped interpolation | Literal `cmd`; shell-source `exec` needs quoting | Escaped interpolation |
+| Shell grammar and redirects | Tagged core parser; full Bun compatibility entry | Opt-in shell | Opt-in shell | Cross-platform shell interpreter | Host shell exec + built-ins | Host shell |
+| Portable shell commands | 26 core commands; Bun and ShellJS entries | System utilities | System utilities | Built-in utilities | Portable Unix commands | System utilities |
+| Custom commands | Registry, generators, middleware and packages | JS transforms | Child programs | JS objects in redirections | Plugin API | Custom spawn and helpers |
+| Nonzero status | Core returns code; shell errexit and adapter policies | Rejects by default, configurable | Exit event / status | Throws by default, configurable | Configurable fatal policy | Throws by default, nothrow |
+| Timeout and abort | ProcessOptions; builder overrides; adapter options | timeout, signal, graceful cancel | Spawn timeout/signal | Via Bun process controls | exec options/child control | timeout, signal, kill controls |
+| Signals and process trees | Configurable signal/grace and tree cleanup | Graceful termination and cleanup | ChildProcess lifecycle | Runtime process controls | ChildProcess for async exec | ProcessPromise kill controls |
+| Detached process / live stdin / PID | Core and builder runner | Process API options | ChildProcess | Bun.spawn | Async exec child | ProcessPromise |
+| Local executable resolution | `preferLocal`, spawn/Execa entries | preferLocal | PATH, PATHEXT, shebang support | Runtime resolution | PATH / which | preferLocal |
+| Low-level fd / IPC / credentials | `spawn` or Execa entry | Rich spawn/IPC options | Node spawn options | Bun.spawn options | Exec child/options | Spawn options/hooks |
+| ANSI, terminal snapshots / recording | ANSI config, headless terminal, recording | Output options; external terminal tooling | External terminal tooling | Runtime terminal APIs | External terminal tooling | External terminal tooling |
+| Tracing and diagnostics | Disabled-by-default trace, process/stream/debugger tools | Verbose/custom logging | Child events | Runtime diagnostics | Verbose/config | Verbose/custom log |
+| Typed ESM / CommonJS | Bundled strict declarations; shared entry identity | Typed package exports | Node/@types integration | Runtime types | @types/shelljs | Bundled types |
+| Runtime and platform checks | Node/Bun Linux/macOS/Windows CI; Deno CDN compatibility probes; separate Rust crate | Node library; runtime behavior varies | Node spawn contract | Bun runtime | Node library | Node/Bun/Deno |
+
+Core builder options are documented in [COMMAND-BUILDER.md](COMMAND-BUILDER.md).
+Core ProcessRunner, Bun compatibility, zx compatibility, ShellJS compatibility,
+Execa compatibility and cross-spawn have distinct option/result contracts. Use
+[the migration guides](../README.md#features) when running existing scripts.
+For example, Execa's stream inputs and configurable output encoding belong to
+`command-stream/execa`; passing those names to a core builder does not silently
+implement an Execa option. The full package integrates the actual Execa method;
+its runtime limits are recorded in [EXECA_MIGRATION.md](EXECA_MIGRATION.md).
+
+## Other tracked command APIs and the issue's references
+
+All twelve projects selected by the repository's published discovery rule are
+covered here or above. The [pinned audit](COMPETITOR_TEST_AUDIT.md) records their
+exact commits, portable process invariants, API-specific exclusions and gaps.
+Effect and Rust Command are included because issue #34 explicitly references
+them, regardless of their inclusion in that discovery set.
+
+| Project and primary reference | API and strengths | Corresponding command-stream surface |
+| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
+| [Node child_process](https://nodejs.org/api/child_process.html) | argv, streams/events, sync calls, fd/IPC, process credentials and lifecycle | Core exec/runner; cross-spawn export for the native spawn contract |
+| [Deno.Command](https://docs.deno.com/api/deno/subprocess/) | Reusable argv/options object, output/outputSync/spawn, Web streams, clearEnv | Literal builder, runner/stream, explicit replacement env; Deno CDN support is integration-tested |
+| [Dax](https://github.com/dsherret/dax) and [@david/shell](https://github.com/dsherret/shell) | Shell builders, portable built-ins, presets, streaming; Dax adds requests/prompts | Builder, Bun interpreter, command registry and Node/Bun APIs; no Dax import shim |
+| [nano-spawn](https://github.com/sindresorhus/nano-spawn) | Small dependency-free process API, line iterator, piping and interleaved output | Builder/runner/stream; interleaving via Execa; core subpath reduces loaded modules but is not dependency-free |
+| [@actions/exec](https://github.com/actions/toolkit/tree/main/packages/exec) | argv, listeners, CI output and captured status | Runner events/results and mirror; no Actions-specific logger shim |
+| [cross-env](https://github.com/kentcdodds/cross-env) | Portable inline environment assignments in package scripts | Explicit env options; tagged Bun interpreter for shell assignments; no cross-env CLI replacement |
+| [Effect platform Command](https://effect.website/docs/v3/platform/command) | Immutable `make`, functional pipes, environment/stdio, scoped effect execution and streaming | Immutable `command`, configuration transforms, exit-code selection, runner streams; no Effect runtime dependency |
+| [Rust std::process::Command](https://doc.rust-lang.org/std/process/struct.Command.html) | Native argv/OsString, env clear/removal, stdio handles, status/output/spawn | Rust CommandBuilder and ProcessRunner, plus reusable async/sync descriptions; native std handles remain available directly |
+
+## Features beyond a process wrapper
+
+command-stream combines several layers in one package, and provides portable
+counterparts in Rust. The following links lead to runnable examples and output:
+
+| Feature area | Public guide / executable evidence |
+| ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
+| Every catalog feature and equivalent competitor snippets | [Generated feature guide](../../docs/README.md) |
+| Exact argv, templates, sync, callbacks/events, iteration and pipelines | [Builder guide](COMMAND-BUILDER.md), [core guide](CORE.md), [compatibility tests](../tests/competitor-compatibility.test.mjs) |
+| ShellJS, zx, Execa and Bun script APIs | [ShellJS](SHELLJS_MIGRATION.md), [zx](ZX_MIGRATION.md), [Execa](EXECA_MIGRATION.md), [Bun](BUN_SHELL.md) |
+| Virtual commands, typed packages, composition, inheritance and middleware | [Advanced workflows](ADVANCED-WORKFLOWS.md) |
+| Stream batches/windows, backpressure, monitoring and statistics | [Stream guide](../../docs/stream-processing-guide.md) |
+| ANSI processing, live terminal buffers and recordings | [Feature catalog](../../docs/README.md) |
+| Shell-to-module translation and executable scripts | [Translation guide](FY.md) |
+| REPL, watch/reload, browser pipeline debugger and VS Code tools | [Development guide](INTERACTIVE-DEVELOPMENT.md) |
+| Command mocks, pipeline assertions and snapshots | [Advanced workflows](ADVANCED-WORKFLOWS.md) |
+| Cold import/module budget | [Core entry](CORE.md), [automated module-budget tests](../tests/core-entry.test.mjs) |
+| Process latency, memory, package size, features and real workloads | [JavaScript benchmarks](../benchmarks/README.md), [Rust benchmarks](../../rust/benchmarks/README.md) |
+
+The [Rust comparison](../../rust/docs/COMPARISON.md) compares construction,
+execution, output, configuration, lifecycle and tooling for all fourteen Rust
+projects. Rust's [competitor audit](../../rust/docs/COMPETITOR_TEST_AUDIT.md) covers
+Tokio, async-process, duct, subprocess, xshell, rust_cmd_lib, rust-shell,
+run_script, shellfn, bkt, assert_cmd, rexpect and expectrl. The audit records
+contracts such as binary collection, arbitrary handles, expect sessions,
+persistent result caching and typed domain adapters that the core does not
+claim to replace. JavaScript compatibility imports are not Rust modules.
+
+## Assessing the comparison
+
+The combination of immutable literal builders, multiple compatibility APIs,
+custom shell commands, stream analytics and development tools is the useful
+advantage to evaluate. A global “faster/smaller/better than every competitor”
+claim is not established by API presence or passing tests. The repository's
+benchmarks validate identical output before recording timings, publish raw
+measurements and compare base/head on the same runner. Use those reports for
+a specific workload and the missing-feature ledgers for specialized contracts.
+Neither upstream test accounting nor adapter availability is a claim that every
+upstream test or every platform contract runs unchanged against the core API.
diff --git a/js/docs/COMPETITOR_TEST_AUDIT.md b/js/docs/COMPETITOR_TEST_AUDIT.md
index 032bf46d..d3cb150b 100644
--- a/js/docs/COMPETITOR_TEST_AUDIT.md
+++ b/js/docs/COMPETITOR_TEST_AUDIT.md
@@ -66,8 +66,8 @@ frameworks. Copying their files byte-for-byte would mostly test that Execa,
Bun, or zx exists. Instead, every scoped test belongs to exactly one disposition:
1. A portable public process invariant is adapted into the executable suite.
-2. A public invariant needs a feature command-stream does not have and is listed
- in the missing-feature ledger below.
+2. A public upstream contract is not fully ported and is recorded in the
+ remaining-contract ledger below, with its current implementation status.
3. A test is inapplicable because it verifies competitor API shape, private
internals, unrelated utilities, runtime behavior, or harness mechanics.
@@ -93,32 +93,33 @@ summary-level assertion.
## Executable behavior ports
-| Case ID | Public invariant |
-| ----------------------------- | ------------------------------------------------------------------------------------------------ |
-| `direct-exact-argv` | Direct execution bypasses shell tokenization. |
-| `argument-edge-cases` | Empty, whitespace, multiline, Unicode, quote, and shell-metacharacter arguments survive exactly. |
-| `safe-template-interpolation` | An untrusted interpolated value remains one literal argument. |
-| `array-interpolation` | An interpolated array expands to separate, safely quoted arguments. |
-| `cwd-string` | An explicit string working directory reaches the child. |
-| `environment` | An explicit environment reaches the child without value corruption. |
-| `stdout-stderr-separation` | Standard output and error remain separate. |
-| `newline-preservation` | Captured output preserves final and repeated newlines. |
-| `unicode-output` | UTF-8 output is decoded without loss. |
-| `large-output` | One MiB of output is captured without truncation or deadlock. |
-| `nonzero-exit` | Non-zero status reads through `code` and `exitCode`, on results and on errors. |
-| `result-text` | `text()` returns captured stdout. |
-| `stdin-string` | String input is written completely and stdin is closed. |
-| `stdin-buffer` | Buffer input is written without textual coercion. |
-| `lazy-execution` | A runner starts only when consumed. |
-| `concurrent-execution` | Concurrent children keep their results isolated. |
-| `streamed-before-exit` | Output events arrive before a delayed child exits. |
-| `events-and-await` | Events and the awaited result observe identical bytes. |
-| `bound-options` | Options bound to a tagged command apply when the command executes. |
-| `sync-execution` | Synchronous direct execution preserves exact argv. |
-| `programmatic-pipeline` | Source stdout becomes destination stdin. |
-| `spawn-error-result` | Sync and async unavailable executables produce results instead of uncaught errors. |
-| `abort-signal` | An external `AbortSignal` terminates a running child. |
-| `zx-compatible-api` | Every zx unit runs one-to-one against the zx-compatible layer, `command-stream/zx`. |
+| Case ID | Public invariant |
+| ------------------------------ | ------------------------------------------------------------------------------------------------ |
+| `direct-exact-argv` | Direct execution bypasses shell tokenization. |
+| `argument-edge-cases` | Empty, whitespace, multiline, Unicode, quote, and shell-metacharacter arguments survive exactly. |
+| `safe-template-interpolation` | An untrusted interpolated value remains one literal argument. |
+| `array-interpolation` | An interpolated array expands to separate, safely quoted arguments. |
+| `cwd-string` | An explicit string working directory reaches the child. |
+| `environment` | An explicit environment reaches the child without value corruption. |
+| `environment-clear-and-remove` | Sync and async execution replace inherited env and omit undefined values. |
+| `stdout-stderr-separation` | Standard output and error remain separate. |
+| `newline-preservation` | Captured output preserves final and repeated newlines. |
+| `unicode-output` | UTF-8 output is decoded without loss. |
+| `large-output` | One MiB of output is captured without truncation or deadlock. |
+| `nonzero-exit` | Non-zero status reads through `code` and `exitCode`, on results and on errors. |
+| `result-text` | `text()` returns captured stdout. |
+| `stdin-string` | String input is written completely and stdin is closed. |
+| `stdin-buffer` | Buffer input is written without textual coercion. |
+| `lazy-execution` | A runner starts only when consumed. |
+| `concurrent-execution` | Concurrent children keep their results isolated. |
+| `streamed-before-exit` | Output events arrive before a delayed child exits. |
+| `events-and-await` | Events and the awaited result observe identical bytes. |
+| `bound-options` | Options bound to a tagged command apply when the command executes. |
+| `sync-execution` | Synchronous direct execution preserves exact argv. |
+| `programmatic-pipeline` | Source stdout becomes destination stdin. |
+| `spawn-error-result` | Sync and async unavailable executables produce results instead of uncaught errors. |
+| `abort-signal` | An external `AbortSignal` terminates a running child. |
+| `zx-compatible-api` | Every zx unit runs one-to-one against the zx-compatible layer, `command-stream/zx`. |
### zx-compatible layer
@@ -139,44 +140,61 @@ the invariant appears upstream. Existing focused command-stream suites continue
to exercise shell operators, virtual commands, built-ins, terminal capture,
signals, cleanup, CommonJS, and runtime-specific paths in greater depth.
-## Missing-feature ledger
+## Remaining upstream contracts
-These are comparison inputs for later work, not promises that every competitor
-choice fits command-stream. Stability of the existing API takes priority. A
-future implementation should add a failing focused test and update the manifest
-in the same change.
+The stable `missing` disposition IDs below track upstream contracts that have
+not been ported completely into this corpus. They are not a claim that every
+named feature is absent from the current package: later core implementations
+and compatibility entries are identified explicitly. A supported option alone
+does not establish all upstream error, timing or platform semantics. The
+[current comparison](COMPARISON.md) identifies the public surface to use.
+Environment replacement/removal is now executable in both sync and async ports;
+its three Deno units have moved from missing to ported in both exact ledgers.
### timeout-option
Execa, zx, Dax, and nano-spawn provide a per-command deadline option.
-command-stream accepts external abort signals but has no public timeout option.
+Core ProcessOptions and literal builders support `timeout`, `signal`,
+`killSignal` and `killGrace`; zx and Execa entries expose their own deadlines.
+This corpus has not ported every upstream deadline/error contract, including
+specialized shell/script behavior.
### ipc-and-fork
Node, Bun, Deno's Node compatibility layer, and Execa expose IPC channels or
-fork helpers. command-stream currently exposes only stdin/stdout/stderr process
-communication.
+fork helpers. The core builder has three stdio channels; use the native
+cross-spawn or Execa entry for IPC. These corpus units have not been ported
+against those entries.
### custom-stdio-descriptors
Node-derived APIs accept arbitrary stdio arrays, extra file descriptors, files,
-and inherited streams. command-stream has fixed stdin/stdout/stderr policies.
+and inherited streams. The core builder has fixed stdin/stdout/stderr policies;
+the native spawn and Execa entries support richer descriptors. Their complete
+upstream descriptor conformance is outside these portable core ports.
### configurable-encoding
Several competitors support caller-selected encodings or raw binary output.
command-stream results currently expose strings, with Buffer access available
-through streaming interfaces rather than a result encoding option.
+through streaming interfaces rather than a result encoding option. The Execa
+entry supports encoding/byte results under its own result contract.
### max-buffer-policy
Node `exec`, Execa, and ShellJS allow a maximum captured-output policy.
-command-stream streams and captures without a configurable upper bound.
+The core builder has no configurable maximum capture size (Node synchronous
+spawn also has its native buffer limit). Use asynchronous `capture:false` with
+bounded stream queues for bounded collection, or Execa's capture policy when
+its semantics are required.
### output-transforms-and-line-iteration
Execa, zx, Dax, and nano-spawn provide line iteration, transforms, generators,
-or verbose output hooks beyond command-stream's typed chunk stream.
+or verbose output hooks. Core streams now provide line framing, map/filter,
+batching/windows, statistics and bounded queues; zx and Execa have additional
+adapter-specific transforms. These units still require per-contract checks
+rather than a blanket equivalence with those interfaces.
### local-binary-resolution
@@ -190,7 +208,8 @@ audit.
cross-spawn and nano-spawn normalize Windows shebangs, `PATHEXT`, and `cmd.exe`
escaping. command-stream relies on the runtime and platform shell for those
-rules and does not provide a dedicated resolver.
+rules in the core runner. The cross-spawn export uses the actual cross-spawn
+resolver; selecting that export does not change core-builder resolution.
### cross-platform-inline-environment-syntax
@@ -199,21 +218,18 @@ variable references for Windows command processors. command-stream accepts an
explicit environment object, but it does not provide this command-line rewrite
layer.
-### environment-clear-and-remove
-
-Deno can clear inherited environment values before spawning. command-stream
-can add or replace values but has no environment clear/remove policy.
-
### process-credentials
Node and Deno can select child user and group IDs on supported platforms.
-command-stream has no public process-credential options.
+The core builder has no credential options; native spawn/Execa entries accept
+their platform-specific process options. The corpus does not claim those
+privileged platform contracts have been ported.
### windows-process-window-options
Node and Deno expose Windows-only process creation controls such as hidden
-windows and raw command-line arguments. command-stream delegates those choices
-to the JavaScript runtime.
+windows and raw command-line arguments. Use the native spawn entry for those
+Node options; the core ProcessOptions surface does not expose them.
### child-process-handle-lifecycle
@@ -234,19 +250,23 @@ command-stream's current virtual-command set.
### rich-error-and-timing-metadata
Competitors expose fields such as command text, escaped command, failed stage,
-signal description, duration, and timing. command-stream's result is limited to
-status and captured streams.
+signal description, duration, and timing. Core results include status, signal,
+PID, timeout state and streams; richer error/timing fields belong to the
+compatibility entries. This corpus does not assert every upstream metadata
+field or failed-stage contract.
### graceful-termination
Execa, zx, and Dax provide configurable graceful cancellation and forced-kill
-fallbacks. command-stream supports a kill signal but no public grace-period
-policy.
+fallbacks. Core async execution now supports `killSignal` and `killGrace`, with
+process-tree cleanup; sync native spawn has its own termination behavior. The
+complete upstream cancellation protocols remain distinct from this core policy.
### combined-all-output
Execa and nano-spawn can preserve interleaved stdout and stderr in a combined
-result. command-stream deliberately keeps the two captured streams separate.
+result. The core builder keeps captured streams separate; Execa's `all` and
+zx's combined ProcessOutput use their respective compatibility entries.
### iterable-and-stream-input-options
@@ -256,9 +276,11 @@ ignore, or an interactively accessed stdin pipe.
### layered-bound-option-merging
-Execa can bind several option layers and merge selected nested fields while
-letting invocation-level values override defaults. command-stream supports one
-bound tagged-template option object but not layered or per-invocation merging.
+Literal builders support nested `command.with` presets, fluent/functional
+configuration, explicit env merging and per-run overrides. Execa's own binding
+contract also treats undefined/default and selected nested options according to
+its API. Use the Execa entry for that exact contract; these upstream units have
+not been reclassified merely because builder presets now exist.
### url-working-directory
diff --git a/js/examples/command-builder-demo.mjs b/js/examples/command-builder-demo.mjs
new file mode 100644
index 00000000..76e92fb2
--- /dev/null
+++ b/js/examples/command-builder-demo.mjs
@@ -0,0 +1,21 @@
+import { command } from '../src/$.mjs';
+
+// Run from any working directory with Node.js or Bun.
+const node = command.with({ mirror: false });
+const base = node(process.execPath, ['-e', 'console.log(process.argv[1])']);
+const literal = base.arg("spaces; $HOME and 'quotes'");
+console.log('Literal output:', (await (await literal).text()).trim());
+console.log('POSIX argv:', String(literal));
+console.log('Sync status:', literal.exitCode().sync());
+const configured = literal.pipe(command.stdout('inherit'), command.exitCode);
+console.log('Inherited output status:', await configured.run());
+const lines = await node(
+ process.execPath,
+ '-e',
+ 'console.log("first\\nsecond")'
+)
+ .capture(false)
+ .stream()
+ .map((line) => line.toUpperCase())
+ .toArray();
+console.log('Streamed lines:', lines);
diff --git a/js/examples/features/catalog.mjs b/js/examples/features/catalog.mjs
index a0614fd8..2466451f 100644
--- a/js/examples/features/catalog.mjs
+++ b/js/examples/features/catalog.mjs
@@ -695,19 +695,11 @@ export const features = [
file: 'js/examples/features/command-builder.mjs',
api: ['command', 'CommandBuilder'],
alternatives: {
- 'bun-shell': {
- unsupported:
- 'No command-stream transform API; invoke the executable with separate arguments',
- },
- zx: {
- unsupported:
- 'No command-stream transform API; invoke the executable with separate arguments',
- },
+ 'bun-shell':
+ "await Bun.spawn(['printf', '%s', 'literal ; text']).exited; // exact argv",
+ zx: 'await $`printf %s ${"literal ; text"}`; // escaped shell interpolation',
execa: "await execa('printf', ['%s', 'literal ; text']);",
- shelljs: {
- unsupported:
- 'No command-stream transform API; invoke the executable with separate arguments',
- },
+ shelljs: "shell.cmd('printf', '%s', 'literal ; text'); // exact argv",
child_process: "spawn('printf', ['%s', 'literal ; text']);",
},
},
diff --git a/js/examples/test-command-builder-result.mjs b/js/examples/test-command-builder-result.mjs
new file mode 100644
index 00000000..66083d9b
--- /dev/null
+++ b/js/examples/test-command-builder-result.mjs
@@ -0,0 +1,7 @@
+import { command } from '../src/$.mjs';
+
+const builder = command(process.execPath, '-e', 'console.log("hello")');
+const result = await builder.run();
+console.log('Output:', await result.text());
+console.log('Exit code:', result.code);
+console.log('Selected exit code:', await builder.pipe(command.exitCode));
diff --git a/js/src/$.process-runner-execution.mjs b/js/src/$.process-runner-execution.mjs
index 791df8f1..634cc21c 100644
--- a/js/src/$.process-runner-execution.mjs
+++ b/js/src/$.process-runner-execution.mjs
@@ -3,7 +3,6 @@ import { pumpInheritedStdin } from './$.process-runner-stdin.mjs';
import cp from 'child_process';
import { toStreamResult } from './$.result-streams.mjs';
import { trace } from './$.trace.mjs';
-import { normalizeBunExitSignal } from './$.process-runner-signal.mjs';
import {
normalizeTimeout,
setupCommandTimeout,
@@ -21,12 +20,9 @@ import {
attachExitCodeAlias,
createCancelledResult,
createCommandError,
- createExecutionErrorResult,
createResult,
determineFinalExitCode,
finishExecutionError,
- getStdinString,
- getSyncStdinInput,
isProcessLaunchError,
prepareSpawnErrorResult,
} from './$.result.mjs';
@@ -41,6 +37,10 @@ import {
drainPumpsAfterExit,
} from './$.process-runner-exit.mjs';
import { effectiveCwd, effectiveEnv } from './$.process-context.mjs';
+import {
+ executeSyncProcess,
+ processSyncResult,
+} from './$.process-runner-sync.mjs';
import {
startDetached,
startDetachedSync,
@@ -498,143 +498,6 @@ function throwErrexitIfNeeded(runner, globalShellSettings) {
);
}
-/**
- * Execute sync process using Bun
- * @param {Array} argv - Command arguments
- * @param {object} options - Spawn options
- * @returns {object} Result object
- */
-function executeSyncBun(argv, options) {
- const { cwd, env, stdin, timeout, killSignal } = options;
- let proc;
- try {
- proc = Bun.spawnSync(argv, {
- cwd,
- env,
- stdin: getSyncStdinInput(stdin),
- stdout: 'pipe',
- stderr: 'pipe',
- timeout,
- killSignal,
- });
- } catch (error) {
- return createExecutionErrorResult(error);
- }
-
- const signal = normalizeBunExitSignal(proc.exitCode, proc.signalCode);
- const result = createResult({
- code: determineFinalExitCode(proc.exitCode, signal),
- signal,
- stdout: proc.stdout?.toString('utf8') || '',
- stderr: proc.stderr?.toString('utf8') || '',
- stdin: getStdinString(stdin),
- });
- result.timedOut = Boolean(proc.exitedDueToTimeout);
- result.child = proc;
- return result;
-}
-
-/**
- * Execute sync process using Node
- * @param {Array} argv - Command arguments
- * @param {object} options - Spawn options
- * @returns {object} Result object
- */
-function executeSyncNode(argv, options) {
- const { cwd, env, stdin, shell, timeout, killSignal } = options;
- const proc = cp.spawnSync(argv[0], argv.slice(1), {
- cwd,
- env,
- shell,
- input: getSyncStdinInput(stdin),
- encoding: 'utf8',
- stdio: ['pipe', 'pipe', 'pipe'],
- timeout,
- killSignal,
- });
- // A `timeout` kill sets `error` (ETIMEDOUT) next to the kill signal; report
- // it as a signal exit like the async path does, not as a spawn failure.
- const timedOut = proc.error?.code === 'ETIMEDOUT';
-
- if (proc.error && !timedOut) {
- const result = createExecutionErrorResult(proc.error);
- result.child = proc;
- return result;
- }
-
- const signal = isBun
- ? normalizeBunExitSignal(proc.status, proc.signal)
- : (proc.signal ?? null);
- const result = createResult({
- code: determineFinalExitCode(proc.status, signal),
- signal,
- stdout: proc.stdout || '',
- stderr: proc.stderr || '',
- stdin: getStdinString(stdin),
- });
- result.timedOut = timedOut;
- result.child = proc;
- return result;
-}
-
-/**
- * Execute sync process with appropriate runtime
- * @param {Array} argv - Command arguments
- * @param {object} options - Spawn options
- * @returns {object} Result object
- */
-function executeSyncProcess(argv, options) {
- // Guard against a deleted/inaccessible working directory (issue #44).
- options = { ...options, cwd: resolveSpawnCwd(options.cwd) };
- return isBun && !options.shell
- ? executeSyncBun(argv, options)
- : executeSyncNode(argv, options);
-}
-
-/**
- * Handle sync result processing
- * @param {object} runner - ProcessRunner instance
- * @param {object} result - Result object
- * @param {object} globalShellSettings - Shell settings
- * @returns {object} Result
- */
-function processSyncResult(runner, result, globalShellSettings) {
- // The sync spawn has already exited, but reports the pid it ran under.
- runner._pid = result.child?.pid ?? runner._pid;
-
- if (runner.options.mirror) {
- if (result.stdout) {
- safeWrite(process.stdout, result.stdout);
- }
- if (result.stderr) {
- safeWrite(process.stderr, result.stderr);
- }
- }
-
- runner.outChunks = result.stdout ? [Buffer.from(result.stdout)] : [];
- runner.errChunks = result.stderr ? [Buffer.from(result.stderr)] : [];
-
- if (result.stdout) {
- runner._emitProcessedData('stdout', Buffer.from(result.stdout));
- }
- if (result.stderr) {
- runner._emitProcessedData('stderr', Buffer.from(result.stderr));
- }
-
- runner.finish(result);
-
- if (globalShellSettings.errexit && result.code !== 0) {
- throw createCommandError(`Command failed with exit code ${result.code}`, {
- code: result.code,
- stdout: result.stdout,
- stderr: result.stderr,
- result,
- });
- }
-
- return result;
-}
-
/**
* Stop a runner for an aborted `signal` or an expired `timeout`. Honors the
* configured killSignal (defaults to SIGTERM) so both use the same stop signal
@@ -1439,6 +1302,8 @@ export function attachExecutionMethods(ProcessRunner, deps) {
shell: shellArgv,
timeout: normalizeTimeout(this.options.timeout) || undefined,
killSignal: this.options.killSignal,
+ stdout: this.options.stdout,
+ stderr: this.options.stderr,
});
return processSyncResult(this, result, shellSettings);
};
diff --git a/js/src/$.process-runner-sync.mjs b/js/src/$.process-runner-sync.mjs
new file mode 100644
index 00000000..aaabd03e
--- /dev/null
+++ b/js/src/$.process-runner-sync.mjs
@@ -0,0 +1,187 @@
+// Native synchronous execution shared by shell runners and literal builders.
+import cp from 'node:child_process';
+import { trace } from './$.trace.mjs';
+import { normalizeBunExitSignal } from './$.process-runner-signal.mjs';
+import { resolveSpawnCwd } from './$.shell.mjs';
+import { safeWrite } from './$.stream-utils.mjs';
+import {
+ createCommandError,
+ createExecutionErrorResult,
+ createResult,
+ determineFinalExitCode,
+ getStdinString,
+ getSyncStdinInput,
+} from './$.result.mjs';
+
+const isBun = typeof globalThis.Bun !== 'undefined';
+
+/**
+ * Execute sync process using Bun
+ * @param {Array} argv - Command arguments
+ * @param {object} options - Spawn options
+ * @returns {object} Result object
+ */
+function executeSyncBun(argv, options) {
+ const { cwd, env, stdin, timeout, killSignal } = options;
+ let proc;
+ try {
+ proc = Bun.spawnSync(argv, {
+ cwd,
+ env,
+ stdin:
+ getSyncStdinInput(stdin) ??
+ (stdin === 'inherit' ? 'inherit' : 'ignore'),
+ stdout: options.stdout ?? 'pipe',
+ stderr: options.stderr ?? 'pipe',
+ timeout,
+ killSignal,
+ });
+ } catch (error) {
+ return createExecutionErrorResult(error);
+ }
+
+ const signal = normalizeBunExitSignal(proc.exitCode, proc.signalCode);
+ const result = createResult({
+ code: determineFinalExitCode(proc.exitCode, signal),
+ signal,
+ stdout: proc.stdout?.toString('utf8') || '',
+ stderr: proc.stderr?.toString('utf8') || '',
+ stdin: getStdinString(stdin),
+ });
+ result.timedOut = Boolean(proc.exitedDueToTimeout);
+ result.child = proc;
+ return result;
+}
+
+/**
+ * Execute sync process using Node
+ * @param {Array} argv - Command arguments
+ * @param {object} options - Spawn options
+ * @returns {object} Result object
+ */
+function executeSyncNode(argv, options) {
+ const { cwd, env, stdin, shell, timeout, killSignal } = options;
+ const proc = cp.spawnSync(argv[0], argv.slice(1), {
+ cwd,
+ env,
+ shell,
+ input: getSyncStdinInput(stdin),
+ encoding: 'utf8',
+ stdio: [
+ stdin === 'inherit' || stdin === 'ignore' ? stdin : 'pipe',
+ options.stdout ?? 'pipe',
+ options.stderr ?? 'pipe',
+ ],
+ timeout,
+ killSignal,
+ });
+ // A `timeout` kill sets `error` (ETIMEDOUT) next to the kill signal; report
+ // it as a signal exit like the async path does, not as a spawn failure.
+ const timedOut = proc.error?.code === 'ETIMEDOUT';
+
+ if (proc.error && !timedOut) {
+ const result = createExecutionErrorResult(proc.error);
+ result.child = proc;
+ return result;
+ }
+
+ const signal = isBun
+ ? normalizeBunExitSignal(proc.status, proc.signal)
+ : (proc.signal ?? null);
+ const result = createResult({
+ code: determineFinalExitCode(proc.status, signal),
+ signal,
+ stdout: proc.stdout || '',
+ stderr: proc.stderr || '',
+ stdin: getStdinString(stdin),
+ });
+ result.timedOut = timedOut;
+ result.child = proc;
+ return result;
+}
+
+/**
+ * Execute sync process with appropriate runtime
+ * @param {Array} argv - Command arguments
+ * @param {object} options - Spawn options
+ * @returns {object} Result object
+ */
+export function executeSyncProcess(argv, options) {
+ // Guard against a deleted/inaccessible working directory (issue #44).
+ options = { ...options, cwd: resolveSpawnCwd(options.cwd) };
+ trace(
+ 'ProcessRunner',
+ () =>
+ `sync stdio | ${JSON.stringify({
+ stdin:
+ getSyncStdinInput(options.stdin) === undefined
+ ? options.stdin
+ : 'buffer',
+ stdout: options.stdout ?? 'pipe',
+ stderr: options.stderr ?? 'pipe',
+ })}`,
+ { options }
+ );
+ return isBun && !options.shell
+ ? executeSyncBun(argv, options)
+ : executeSyncNode(argv, options);
+}
+
+/**
+ * Handle sync result processing
+ * @param {object} runner - ProcessRunner instance
+ * @param {object} result - Result object
+ * @param {object} globalShellSettings - Shell settings
+ * @returns {object} Result
+ */
+export function processSyncResult(runner, result, globalShellSettings) {
+ // The sync spawn has already exited, but reports the pid it ran under.
+ runner._pid = result.child?.pid ?? runner._pid;
+
+ if (runner.options.mirror) {
+ if (result.stdout) {
+ safeWrite(process.stdout, result.stdout);
+ }
+ if (result.stderr) {
+ safeWrite(process.stderr, result.stderr);
+ }
+ }
+
+ runner.outChunks = runner.options.capture
+ ? result.stdout
+ ? [Buffer.from(result.stdout)]
+ : []
+ : null;
+ runner.errChunks = runner.options.capture
+ ? result.stderr
+ ? [Buffer.from(result.stderr)]
+ : []
+ : null;
+
+ if (result.stdout) {
+ runner._emitProcessedData('stdout', Buffer.from(result.stdout));
+ }
+ if (result.stderr) {
+ runner._emitProcessedData('stderr', Buffer.from(result.stderr));
+ }
+
+ if (!runner.options.capture) {
+ result.stdout = undefined;
+ result.stderr = undefined;
+ result.stdin = undefined;
+ result.text = () => Promise.resolve('');
+ }
+
+ runner.finish(result);
+
+ if (globalShellSettings.errexit && result.code !== 0) {
+ throw createCommandError(`Command failed with exit code ${result.code}`, {
+ code: result.code,
+ stdout: result.stdout,
+ stderr: result.stderr,
+ result,
+ });
+ }
+
+ return result;
+}
diff --git a/js/src/command-builder.mjs b/js/src/command-builder.mjs
index cf5b0a5c..6000ab89 100644
--- a/js/src/command-builder.mjs
+++ b/js/src/command-builder.mjs
@@ -1,7 +1,47 @@
import { ProcessRunner } from './process-runner.mjs';
+import { trace } from './$.trace.mjs';
const identity = (value) => value;
const posixQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
+const stdioModes = ['pipe', 'inherit', 'ignore'];
+
+function argument(value) {
+ if (!['string', 'number', 'bigint', 'boolean'].includes(typeof value)) {
+ throw new TypeError('Command arguments must be strings or scalar values');
+ }
+ const text = String(value);
+ if (text.includes('\0')) {
+ throw new TypeError('Command arguments must not contain NUL');
+ }
+ return text;
+}
+
+function stdio(stream, mode) {
+ if (!stdioModes.includes(mode)) {
+ throw new TypeError(`Invalid ${stream} mode: ${mode}`);
+ }
+ return mode;
+}
+
+function mergeOptions(base, overrides = {}) {
+ return {
+ ...base,
+ ...overrides,
+ ...(base.env &&
+ overrides.env && { env: { ...base.env, ...overrides.env } }),
+ };
+}
+
+function snapshotOptions(options) {
+ return Object.freeze({
+ ...options,
+ ...(options.env && { env: Object.freeze({ ...options.env }) }),
+ ...(options.ansi && { ansi: Object.freeze({ ...options.ansi }) }),
+ ...(Array.isArray(options.preferLocal) && {
+ preferLocal: Object.freeze([...options.preferLocal]),
+ }),
+ });
+}
/** An immutable, lazy exact-argv command specification. */
export class CommandBuilder {
@@ -9,43 +49,112 @@ export class CommandBuilder {
if (typeof file !== 'string' || !file || file.includes('\0')) {
throw new TypeError('Command file must be a nonempty string without NUL');
}
- if (args.some((arg) => typeof arg !== 'string' || arg.includes('\0'))) {
- throw new TypeError('Command arguments must be strings without NUL');
+ if (!Array.isArray(args)) {
+ throw new TypeError('Command arguments must be an array');
+ }
+ for (const stream of ['stdout', 'stderr']) {
+ if (options[stream] !== undefined) {
+ stdio(stream, options[stream]);
+ }
}
this.file = file;
- this.args = Object.freeze([...args]);
- this.options = Object.freeze({
+ this.args = Object.freeze(args.map(argument));
+ this.options = snapshotOptions({
mirror: false,
capture: true,
...options,
- ...(options.env && { env: Object.freeze({ ...options.env }) }),
});
this.select = select;
Object.freeze(this);
}
+ get cmd() {
+ return this.file;
+ }
+
+ arg(...args) {
+ return new CommandBuilder(
+ this.file,
+ [...this.args, ...args],
+ this.options,
+ this.select
+ );
+ }
+
with(options) {
return new CommandBuilder(
this.file,
this.args,
- { ...this.options, ...options },
+ mergeOptions(this.options, options),
this.select
);
}
+ stdin(input) {
+ return this.with({ stdin: input });
+ }
+
+ stdout(mode) {
+ return this.with({ stdout: stdio('stdout', mode) });
+ }
+
+ stderr(mode) {
+ return this.with({ stderr: stdio('stderr', mode) });
+ }
+
+ env(vars) {
+ return this.with({ env: vars });
+ }
+
+ cwd(dir) {
+ return this.with({ cwd: dir });
+ }
+
+ capture(enabled = true) {
+ return this.with({ capture: enabled });
+ }
+
+ mirror(enabled = true) {
+ return this.with({ mirror: enabled });
+ }
+
+ exitCode() {
+ return new CommandBuilder(
+ this.file,
+ this.args,
+ this.options,
+ (result) => result.code
+ );
+ }
+
pipe(...transforms) {
- return transforms.reduce((builder, transform) => transform(builder), this);
+ return transforms.reduce((builder, transform) => {
+ if (typeof transform !== 'function') {
+ throw new TypeError('Command pipe() expects builder transforms');
+ }
+ const next = transform(builder);
+ if (!(next instanceof CommandBuilder)) {
+ throw new TypeError('Command transforms must return a CommandBuilder');
+ }
+ return next;
+ }, this);
}
- runner() {
+ runner(options = {}) {
+ const configured = this.with(options);
+ trace('CommandBuilder', () => `runner() | file: ${this.file}`);
return new ProcessRunner(
{ mode: 'exec', file: this.file, args: [...this.args] },
- this.options
+ configured.options
);
}
- async run() {
- return this.select(await this.runner());
+ async run(options) {
+ return this.select(await this.runner(options));
+ }
+
+ sync(options) {
+ return this.select(this.runner(options).sync());
}
then(resolve, reject) {
@@ -59,27 +168,41 @@ export class CommandBuilder {
toString() {
return [this.file, ...this.args].map(posixQuote).join(' ');
}
-}
-export function command(file, ...args) {
- return new CommandBuilder(file, args);
+ buildCommand() {
+ return this.toString();
+ }
}
-for (const stream of ['stdout', 'stderr']) {
- command[stream] = (mode) => {
- if (!['pipe', 'inherit', 'ignore'].includes(mode)) {
- throw new TypeError(`Invalid ${stream} mode: ${mode}`);
+function factory(options = {}) {
+ const defaults = snapshotOptions(options);
+ const make = (file, ...args) => {
+ if (Array.isArray(args[0])) {
+ if (args.length > 2) {
+ throw new TypeError('Use command(file, args, options) for argv arrays');
+ }
+ return new CommandBuilder(file, args[0], mergeOptions(defaults, args[1]));
}
- return (builder) => builder.with({ [stream]: mode });
+ return new CommandBuilder(file, args, defaults);
};
+ for (const stream of ['stdout', 'stderr']) {
+ make[stream] = (mode) => {
+ stdio(stream, mode);
+ return (builder) => builder[stream](mode);
+ };
+ }
+ for (const method of ['arg', 'cwd', 'env', 'stdin', 'capture', 'mirror']) {
+ make[method] =
+ (...args) =>
+ (builder) =>
+ builder[method](...args);
+ }
+ // Factory presets and pipe transforms share the same options vocabulary.
+ // `with` on the factory binds defaults; use `options` as a pipe transform.
+ make.options = (options) => (builder) => builder.with(options);
+ make.with = (options) => factory(mergeOptions(defaults, options));
+ make.exitCode = (builder) => builder.exitCode();
+ return make;
}
-command.cwd = (cwd) => (builder) => builder.with({ cwd });
-command.env = (env) => (builder) => builder.with({ env });
-command.stdin = (stdin) => (builder) => builder.with({ stdin });
-command.exitCode = (builder) =>
- new CommandBuilder(
- builder.file,
- builder.args,
- builder.options,
- (result) => result.code
- );
+
+export const command = factory();
diff --git a/js/src/core.mjs b/js/src/core.mjs
index 5dc446ba..6a019e65 100644
--- a/js/src/core.mjs
+++ b/js/src/core.mjs
@@ -165,7 +165,7 @@ function $tagged(strings, ...values) {
);
// Return a new tagged template function with those options
- return (innerStrings, ...innerValues) => {
+ const tagged = (innerStrings, ...innerValues) => {
trace(
'API',
() =>
@@ -193,6 +193,8 @@ function $tagged(strings, ...values) {
);
return runner;
};
+ tagged.command = command.with(options);
+ return tagged;
}
// Normal tagged template literal usage
@@ -258,6 +260,7 @@ function create(defaultOptions = {}, handler, metadata) {
return runner;
};
+ tagged.command = command.with(defaultOptions);
trace('API', () => `create EXIT | ${JSON.stringify({}, null, 2)}`);
return tagged;
}
diff --git a/js/tests/command-builder.node.test.mjs b/js/tests/command-builder.node.test.mjs
new file mode 100644
index 00000000..463f6006
--- /dev/null
+++ b/js/tests/command-builder.node.test.mjs
@@ -0,0 +1,84 @@
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { createRequire } from 'node:module';
+import { spawnSync } from 'node:child_process';
+import { $, command } from '../src/$.mjs';
+
+const require = createRequire(import.meta.url);
+for (const [format, api] of [
+ ['ESM', { $, command }],
+ ['CommonJS', require('../src/$.cjs')],
+ ['core ESM', await import('../src/core.mjs')],
+ ['core CommonJS', require('../src/core.cjs')],
+]) {
+ test(
+ `${format} builders share literal argv, presets, selection and sync behavior`,
+ { timeout: 10000 },
+ async () => {
+ const args = ['', "a'b", 'a b', '$HOME; echo injected', '\n'];
+ const builder = api.command(process.execPath, [
+ '-e',
+ 'console.log(JSON.stringify(process.argv.slice(1)))',
+ ...args,
+ ]);
+ assert.deepEqual(JSON.parse(await (await builder).text()), args);
+ assert.deepEqual(JSON.parse(await builder.sync().text()), args);
+ assert.equal(builder.exitCode().sync(), 0);
+ assert.equal(await builder.pipe(api.command.exitCode), 0);
+ const preset = api.$({ stdin: 'preset', mirror: false }).command;
+ assert.equal(
+ await (
+ await preset(
+ process.execPath,
+ '-e',
+ 'process.stdin.pipe(process.stdout)'
+ )
+ ).text(),
+ 'preset'
+ );
+ }
+ );
+}
+
+for (const execution of ['run', 'sync']) {
+ test(
+ `${execution}: inherit writes exactly once while ignore discards output`,
+ { timeout: 10000 },
+ () => {
+ const source = `
+ const { command } = await import('./js/src/command-builder.mjs');
+ const builder = command(process.execPath, '-e', "console.log('visible');console.error('hidden')")
+ .stdout('inherit').stderr('ignore').mirror(true);
+ const result = await builder.${execution}();
+ if (String(result.stdout) !== '' || String(result.stderr) !== '') process.exit(9);
+ `;
+ const result = spawnSync(
+ process.execPath,
+ ['--input-type=module', '-e', source],
+ { encoding: 'utf8' }
+ );
+ assert.equal(result.status, 0, result.stderr);
+ assert.equal(result.stdout, 'visible\n');
+ assert.equal(result.stderr, '');
+ }
+ );
+}
+
+test('sync inherits piped stdin from the parent', { timeout: 10000 }, () => {
+ const source = `
+ const { command } = await import('./js/src/command-builder.mjs');
+ const builder = command(process.execPath, '-e', 'process.stdin.pipe(process.stdout)');
+ const result = builder.stdin('inherit').sync();
+ process.stdout.write(String(result.stdout));
+ `;
+ const result = spawnSync(
+ process.execPath,
+ ['--input-type=module', '-e', source],
+ {
+ input: 'inherited input',
+ encoding: 'utf8',
+ }
+ );
+ assert.equal(result.status, 0, result.stderr);
+ assert.equal(result.stdout, 'inherited input');
+});
diff --git a/js/tests/command-builder.test.mjs b/js/tests/command-builder.test.mjs
new file mode 100644
index 00000000..f7806609
--- /dev/null
+++ b/js/tests/command-builder.test.mjs
@@ -0,0 +1,309 @@
+import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
+import { beforeTestCleanup, afterTestCleanup } from './test-cleanup.mjs';
+import { $, command, CommandBuilder, create, sh } from '../src/$.mjs';
+import { mkdtempSync, writeFileSync, rmSync, realpathSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+
+beforeEach(beforeTestCleanup);
+afterEach(afterTestCleanup);
+const argvScript = 'console.log(JSON.stringify(process.argv.slice(1)))';
+const readInput = 'process.stdin.pipe(process.stdout)';
+const node = (...args) => command(process.execPath, '-e', ...args);
+
+describe('command builder API styles', () => {
+ test('fluent configuration preserves immutable reusable descriptions', async () => {
+ const base = node(argvScript).arg('a b', '', 42, true, 4n);
+ const configured = base
+ .stdout('pipe')
+ .stderr('ignore')
+ .capture()
+ .mirror(false);
+ expect(base.args).toEqual(['-e', argvScript, 'a b', '', '42', 'true', '4']);
+ expect(base.options.stderr).toBeUndefined();
+ expect(configured).toBeInstanceOf(CommandBuilder);
+ expect(Object.isFrozen(configured)).toBe(true);
+ expect(JSON.parse(await (await configured).text())).toEqual([
+ 'a b',
+ '',
+ '42',
+ 'true',
+ '4',
+ ]);
+ expect(await (await configured.run()).text()).toBe(
+ await (await base).text()
+ );
+ });
+
+ test('argv array and constructor styles accept the same options', async () => {
+ const args = ['-e', argvScript, '', 'a b'];
+ const builder = command(process.execPath, args, { stderr: 'ignore' });
+ args.push('late mutation');
+ expect(builder.args).toEqual(['-e', argvScript, '', 'a b']);
+ expect(await (await builder).text()).toBe(
+ await (
+ await new CommandBuilder(
+ process.execPath,
+ builder.args,
+ builder.options
+ )
+ ).text()
+ );
+ expect(command('echo', 42).args).toEqual(['42']);
+ expect(command('echo', []).args).toEqual([]);
+ expect(
+ command.with({ env: { BUILDER_VALUE: 'preset' } })('echo', []).options.env
+ .BUILDER_VALUE
+ ).toBe('preset');
+ });
+
+ test('functional transforms and fluent methods select the same exit code', async () => {
+ const base = node('process.exit(7)');
+ const functional = base.pipe(
+ command.stdout('ignore'),
+ command.stderr('ignore'),
+ command.capture(false),
+ command.mirror(false),
+ command.exitCode
+ );
+ expect(await functional.run()).toBe(7);
+ expect(base.stdout('ignore').stderr('ignore').exitCode().sync()).toBe(7);
+ expect((await base).code).toBe(7);
+ });
+
+ test('each option style merges env and execution overrides without mutation', async () => {
+ const env = { ...process.env, BUILDER_A: 'a', BUILDER_DROP: 'remove' };
+ const builder = node(
+ 'console.log(JSON.stringify([process.env.BUILDER_A, process.env.BUILDER_B, process.env.BUILDER_DROP]))'
+ )
+ .env(env)
+ .pipe(command.env({ BUILDER_B: 'b' }));
+ env.BUILDER_A = 'mutated';
+ const result = await builder.run({
+ env: { BUILDER_DROP: undefined, BUILDER_B: 'override' },
+ });
+ expect(JSON.parse(await result.text())).toEqual(['a', 'override', null]);
+ expect(JSON.parse(await (await builder).text())).toEqual([
+ 'a',
+ 'b',
+ 'remove',
+ ]);
+ });
+
+ test('configured factories and tagged presets retain builder helpers', async () => {
+ for (const factory of [
+ command.with({ stdin: 'factory' }),
+ create({ stdin: 'factory' }).command,
+ $({ stdin: 'factory' }).command,
+ ]) {
+ const builder = factory(process.execPath, ['-e', readInput]);
+ expect(await (await builder).text()).toBe('factory');
+ expect(
+ await builder
+ .pipe(factory.stdin('transform'))
+ .run()
+ .then((result) => result.text())
+ ).toBe('transform');
+ expect(
+ await factory
+ .with({ stdin: 'nested' })(process.execPath, '-e', readInput)
+ .run()
+ .then((result) => result.text())
+ ).toBe('nested');
+ }
+ });
+
+ test('factory defaults snapshot caller-owned environment objects', () => {
+ const env = { BUILDER_VALUE: 'before' };
+ const defaults = { env, mirror: false };
+ const preset = command.with(defaults);
+ env.BUILDER_VALUE = 'after';
+ defaults.mirror = true;
+ expect(preset('echo').options.env.BUILDER_VALUE).toBe('before');
+ expect(preset('echo').options.mirror).toBe(false);
+ });
+
+ test('stdin, cwd and run options work through fluent and functional styles', async () => {
+ const dir = mkdtempSync(join(tmpdir(), 'command-builder-'));
+ try {
+ const builder = node(
+ 'console.log(process.cwd()); process.stdin.pipe(process.stdout)'
+ )
+ .cwd(dir)
+ .stdin(Buffer.from('input'))
+ .pipe(command.capture(), command.mirror(false));
+ const result = await builder.run();
+ expect(await result.text()).toBe(`${realpathSync(dir)}\ninput`);
+ expect(await (await builder.run({ stdin: 'override' })).text()).toBe(
+ `${realpathSync(dir)}\noverride`
+ );
+ } finally {
+ rmSync(dir, { recursive: true, force: true });
+ }
+ });
+
+ test('the exact issue example inherits stdout and returns an exit code', async () => {
+ const dir = mkdtempSync(join(tmpdir(), 'command-builder-cat-'));
+ try {
+ const file = join(dir, 'some file.txt');
+ writeFileSync(file, 'builder example\n');
+ expect(
+ await $.command('cat', file)
+ .pipe($.command.stdout('inherit'), $.command.exitCode)
+ .run()
+ ).toBe(0);
+ } finally {
+ rmSync(dir, { recursive: true, force: true });
+ }
+ });
+
+ test('runner retains events and real process pipelines', async () => {
+ const source = node("process.stdout.write('pipeline')").runner();
+ let output = '';
+ source.on('stdout', (chunk) => {
+ output += chunk;
+ });
+ const destination = node(readInput).runner();
+ const result = await source.pipe(destination);
+ expect(output).toBe('pipeline');
+ expect(await result.text()).toBe('pipeline');
+ });
+
+ test('streaming retains raw output and fluent line processing', async () => {
+ const builder = node("process.stdout.write('first\\nsecond\\n')").capture(
+ false
+ );
+ expect(
+ await builder
+ .stream()
+ .map((line) => line.toUpperCase())
+ .toArray()
+ ).toEqual(['FIRST', 'SECOND']);
+ });
+
+ test('run overrides support timeout and cancellation', async () => {
+ const builder = node('setInterval(() => {}, 1000)');
+ expect((await builder.run({ timeout: 100 })).timedOut).toBe(true);
+ const controller = new AbortController();
+ controller.abort();
+ expect((await builder.run({ signal: controller.signal })).code).not.toBe(0);
+ }, 5000);
+
+ test('sync honors ignored stdio without capture, events or mirroring', async () => {
+ const builder = node("console.log('hidden'); console.error('hidden')")
+ .stdout('ignore')
+ .stderr('ignore')
+ .mirror(true);
+ const runner = builder.runner();
+ const events = [];
+ runner.on('data', (chunk) => events.push(chunk));
+ const result = runner.sync();
+ expect(result.code).toBe(0);
+ expect(await result.text()).toBe('');
+ expect(String(result.stderr)).toBe('');
+ expect(events).toEqual([]);
+ });
+
+ test('sync capture:false keeps events but discards retained output', async () => {
+ const runner = node("console.log('visible'); console.error('error')")
+ .capture(false)
+ .runner();
+ let stdout = '';
+ let stderr = '';
+ runner.on('stdout', (chunk) => {
+ stdout += chunk;
+ });
+ runner.on('stderr', (chunk) => {
+ stderr += chunk;
+ });
+ const result = runner.sync();
+ expect(stdout).toBe('visible\n');
+ expect(stderr).toBe('error\n');
+ expect(result.stdout).toBeUndefined();
+ expect(result.stderr).toBeUndefined();
+ expect(await result.text()).toBe('');
+ expect(runner.outChunks).toBeNull();
+ expect(runner.errChunks).toBeNull();
+ });
+
+ test('presets and builders snapshot nested configuration', () => {
+ const directories = ['/initial'];
+ const ansi = { preserveAnsi: false };
+ const options = { preferLocal: directories, ansi };
+ const preset = command.with(options);
+ const builder = command('echo', [], options);
+ directories.push('/mutated');
+ ansi.preserveAnsi = true;
+ for (const configured of [preset('echo'), builder]) {
+ expect(configured.options.preferLocal).toEqual(['/initial']);
+ expect(configured.options.ansi.preserveAnsi).toBe(false);
+ expect(Object.isFrozen(configured.options.preferLocal)).toBe(true);
+ expect(Object.isFrozen(configured.options.ansi)).toBe(true);
+ }
+ });
+});
+
+describe('command builder safety and rendering', () => {
+ test('literal argv stays literal with shell parsing and virtual commands enabled', async () => {
+ const args = [
+ '',
+ 'a b',
+ "a'b",
+ '"quoted"',
+ '\n',
+ '$HOME',
+ '$(echo injected)',
+ '`echo injected`',
+ '*',
+ '; exit 9',
+ '&&',
+ '|',
+ ];
+ const builder = node(argvScript).arg(...args);
+ expect(JSON.parse(await (await builder).text())).toEqual(args);
+ expect(JSON.parse(await builder.sync().text())).toEqual(args);
+ expect(builder.cmd).toBe(builder.file);
+ expect(builder.buildCommand()).toBe(String(builder));
+ if (process.platform !== 'win32') {
+ expect(await (await sh(String(builder), { mirror: false })).text()).toBe(
+ await (await builder).text()
+ );
+ }
+ });
+
+ test('quotes executable paths as well as arguments', async () => {
+ if (process.platform === 'win32') {
+ return;
+ }
+ const dir = mkdtempSync(join(tmpdir(), "builder ' executable "));
+ try {
+ const executable = join(dir, "test ' $HOME");
+ writeFileSync(executable, '#!/bin/sh\nprintf "%s" "$1"\n', {
+ mode: 0o755,
+ });
+ const builder = command(executable, 'literal ; $HOME');
+ expect(await (await builder).text()).toBe('literal ; $HOME');
+ expect(await (await sh(String(builder), { mirror: false })).text()).toBe(
+ 'literal ; $HOME'
+ );
+ } finally {
+ rmSync(dir, { recursive: true, force: true });
+ }
+ });
+
+ test('rejects invalid argv and stdio modes before spawning', () => {
+ for (const file of ['', '\0', null, 12]) {
+ expect(() => command(file)).toThrow(TypeError);
+ }
+ for (const arg of ['\0', null, undefined, {}, ['nested']]) {
+ expect(() => node(argvScript).arg(arg)).toThrow(TypeError);
+ }
+ expect(() => command('echo', ['ok', '\0'])).toThrow(TypeError);
+ expect(() => command('echo').with({ stdout: 'invalid' })).toThrow(
+ TypeError
+ );
+ expect(() => command('echo').stderr('invalid')).toThrow(TypeError);
+ expect(() => command.stdout('invalid')).toThrow(TypeError);
+ expect(() => command('echo').pipe(() => null)).toThrow(TypeError);
+ });
+});
diff --git a/js/tests/competitor-compatibility.test.mjs b/js/tests/competitor-compatibility.test.mjs
index 52844309..21ca3722 100644
--- a/js/tests/competitor-compatibility.test.mjs
+++ b/js/tests/competitor-compatibility.test.mjs
@@ -451,6 +451,32 @@ describe('ported public process behavior', () => {
}
);
+ port(
+ 'environment-clear-and-remove',
+ 'replaces inherited environment and removes explicitly undefined keys',
+ async () => {
+ const [parentKey] = Object.keys(process.env);
+ expect(parentKey).toBeDefined();
+ const env = {
+ COMMAND_STREAM_CORPUS_ALPHA: 'one',
+ [parentKey]: undefined,
+ };
+ for (const sync of [false, true]) {
+ const runner = fixtureRunner(
+ 'env',
+ [parentKey, 'COMMAND_STREAM_CORPUS_ALPHA'],
+ { env }
+ );
+ const result = sync ? runner.sync() : await runner;
+ expect(result.code).toBe(0);
+ expect(JSON.parse(result.stdout)).toEqual({
+ [parentKey]: null,
+ COMMAND_STREAM_CORPUS_ALPHA: 'one',
+ });
+ }
+ }
+ );
+
port(
'stdout-stderr-separation',
'captures stdout-only, stderr-only, and mixed output independently',
diff --git a/js/tests/competitor-corpus.mjs b/js/tests/competitor-corpus.mjs
index 9d697ba6..799459ec 100644
--- a/js/tests/competitor-corpus.mjs
+++ b/js/tests/competitor-corpus.mjs
@@ -488,6 +488,14 @@ export const portedCases = [
'mod.test.ts',
],
},
+ {
+ id: 'environment-clear-and-remove',
+ competitors: ['deno-command'],
+ upstream: [
+ 'tests/unit/command_test.ts',
+ 'tests/unit_node/child_process_test.ts',
+ ],
+ },
{
// Every zx unit is ported one-to-one under js/tests/zx against the
// zx-compatible layer (`command-stream/zx`, issue #26).
@@ -528,10 +536,6 @@ export const missingFeatures = [
id: 'cross-platform-inline-environment-syntax',
competitors: ['cross-env'],
},
- {
- id: 'environment-clear-and-remove',
- competitors: ['deno-command'],
- },
{
id: 'process-credentials',
competitors: ['node-child-process', 'deno-command'],
diff --git a/js/tests/competitor-decisions.jsonl b/js/tests/competitor-decisions.jsonl
index b090b31e..e14d4f67 100644
--- a/js/tests/competitor-decisions.jsonl
+++ b/js/tests/competitor-decisions.jsonl
@@ -571,7 +571,7 @@
{"id":"deno-command:tests/unit_node/child_process_test.ts:880:1:registration","disposition":{"kind":"ported","id":"abort-signal"}}
{"id":"deno-command:tests/unit_node/child_process_test.ts:901:1:registration","disposition":{"kind":"ported","id":"abort-signal"}}
{"id":"deno-command:tests/unit_node/child_process_test.ts:917:1:registration","disposition":{"kind":"ported","id":"environment"}}
-{"id":"deno-command:tests/unit_node/child_process_test.ts:950:1:registration","disposition":{"kind":"missing","id":"environment-clear-and-remove"}}
+{"id":"deno-command:tests/unit_node/child_process_test.ts:950:1:registration","disposition":{"kind":"ported","id":"environment-clear-and-remove"}}
{"id":"deno-command:tests/unit_node/child_process_test.ts:985:1:registration","disposition":{"kind":"ported","id":"environment"}}
{"id":"deno-command:tests/unit_node/child_process_test.ts:1020:1:registration","disposition":{"kind":"ported","id":"environment"}}
{"id":"deno-command:tests/unit_node/child_process_test.ts:1039:1:registration","disposition":{"kind":"ported","id":"sync-execution"}}
@@ -639,8 +639,8 @@
{"id":"deno-command:tests/unit/command_test.ts:751:1:registration","disposition":{"kind":"ported","id":"environment"}}
{"id":"deno-command:tests/unit/command_test.ts:769:1:registration","disposition":{"kind":"ported","id":"environment"}}
{"id":"deno-command:tests/unit/command_test.ts:793:1:registration","disposition":{"kind":"ported","id":"environment"}}
-{"id":"deno-command:tests/unit/command_test.ts:811:1:registration","disposition":{"kind":"missing","id":"environment-clear-and-remove"}}
-{"id":"deno-command:tests/unit/command_test.ts:838:1:registration","disposition":{"kind":"missing","id":"environment-clear-and-remove"}}
+{"id":"deno-command:tests/unit/command_test.ts:811:1:registration","disposition":{"kind":"ported","id":"environment-clear-and-remove"}}
+{"id":"deno-command:tests/unit/command_test.ts:838:1:registration","disposition":{"kind":"ported","id":"environment-clear-and-remove"}}
{"id":"deno-command:tests/unit/command_test.ts:865:1:registration","disposition":{"kind":"missing","id":"process-credentials"}}
{"id":"deno-command:tests/unit/command_test.ts:893:1:registration","disposition":{"kind":"missing","id":"windows-shebang-and-pathext-resolution"}}
{"id":"deno-command:tests/unit/command_test.ts:924:1:registration","disposition":{"kind":"missing","id":"process-credentials"}}
diff --git a/js/tests/competitor-dispositions.jsonl b/js/tests/competitor-dispositions.jsonl
index 774397a8..fea43965 100644
--- a/js/tests/competitor-dispositions.jsonl
+++ b/js/tests/competitor-dispositions.jsonl
@@ -583,7 +583,7 @@
{"record":"unit","id":"deno-command:tests/unit_node/child_process_test.ts:880:1:registration","source":"deno-command","unit":"registration","path":"tests/unit_node/child_process_test.ts","line":880,"column":1,"label":"[node/child_process spawn] AbortSignal aborts the child and emits AbortError","disposition":{"kind":"ported","id":"abort-signal"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit_node/child_process_test.ts#L880"}
{"record":"unit","id":"deno-command:tests/unit_node/child_process_test.ts:901:1:registration","source":"deno-command","unit":"registration","path":"tests/unit_node/child_process_test.ts","line":901,"column":1,"label":"[node/child_process] kill() on an exited process returns false and does not throw","disposition":{"kind":"ported","id":"abort-signal"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit_node/child_process_test.ts#L901"}
{"record":"unit","id":"deno-command:tests/unit_node/child_process_test.ts:917:1:registration","source":"deno-command","unit":"registration","path":"tests/unit_node/child_process_test.ts","line":917,"column":1,"label":"[node/child_process spawn] child inherits Deno.env when options.env is not provided","disposition":{"kind":"ported","id":"environment"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit_node/child_process_test.ts#L917"}
-{"record":"unit","id":"deno-command:tests/unit_node/child_process_test.ts:950:1:registration","source":"deno-command","unit":"registration","path":"tests/unit_node/child_process_test.ts","line":950,"column":1,"label":"[node/child_process spawn] child doesn","disposition":{"kind":"missing","id":"environment-clear-and-remove"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit_node/child_process_test.ts#L950"}
+{"record":"unit","id":"deno-command:tests/unit_node/child_process_test.ts:950:1:registration","source":"deno-command","unit":"registration","path":"tests/unit_node/child_process_test.ts","line":950,"column":1,"label":"[node/child_process spawn] child doesn","disposition":{"kind":"ported","id":"environment-clear-and-remove"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit_node/child_process_test.ts#L950"}
{"record":"unit","id":"deno-command:tests/unit_node/child_process_test.ts:985:1:registration","source":"deno-command","unit":"registration","path":"tests/unit_node/child_process_test.ts","line":985,"column":1,"label":"undefinedValueInEnvVar","disposition":{"kind":"ported","id":"environment"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit_node/child_process_test.ts#L985"}
{"record":"unit","id":"deno-command:tests/unit_node/child_process_test.ts:1020:1:registration","source":"deno-command","unit":"registration","path":"tests/unit_node/child_process_test.ts","line":1020,"column":1,"label":"spawnSyncUndefinedValueInEnvVar","disposition":{"kind":"ported","id":"environment"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit_node/child_process_test.ts#L1020"}
{"record":"unit","id":"deno-command:tests/unit_node/child_process_test.ts:1039:1:registration","source":"deno-command","unit":"registration","path":"tests/unit_node/child_process_test.ts","line":1039,"column":1,"label":"spawnSyncStdioUndefined","disposition":{"kind":"ported","id":"sync-execution"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit_node/child_process_test.ts#L1039"}
@@ -651,8 +651,8 @@
{"record":"unit","id":"deno-command:tests/unit/command_test.ts:751:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":751,"column":1,"label":"commandEnv","disposition":{"kind":"ported","id":"environment"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L751"}
{"record":"unit","id":"deno-command:tests/unit/command_test.ts:769:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":769,"column":1,"label":"commandLoaderEnvValueValidation","disposition":{"kind":"ported","id":"environment"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L769"}
{"record":"unit","id":"deno-command:tests/unit/command_test.ts:793:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":793,"column":1,"label":"commandSyncEnv","disposition":{"kind":"ported","id":"environment"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L793"}
-{"record":"unit","id":"deno-command:tests/unit/command_test.ts:811:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":811,"column":1,"label":"commandClearEnv","disposition":{"kind":"missing","id":"environment-clear-and-remove"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L811"}
-{"record":"unit","id":"deno-command:tests/unit/command_test.ts:838:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":838,"column":1,"label":"commandSyncClearEnv","disposition":{"kind":"missing","id":"environment-clear-and-remove"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L838"}
+{"record":"unit","id":"deno-command:tests/unit/command_test.ts:811:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":811,"column":1,"label":"commandClearEnv","disposition":{"kind":"ported","id":"environment-clear-and-remove"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L811"}
+{"record":"unit","id":"deno-command:tests/unit/command_test.ts:838:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":838,"column":1,"label":"commandSyncClearEnv","disposition":{"kind":"ported","id":"environment-clear-and-remove"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L838"}
{"record":"unit","id":"deno-command:tests/unit/command_test.ts:865:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":865,"column":1,"label":"commandUid","disposition":{"kind":"missing","id":"process-credentials"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L865"}
{"record":"unit","id":"deno-command:tests/unit/command_test.ts:893:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":893,"column":1,"label":"rejectBatAndCmdFiles","disposition":{"kind":"missing","id":"windows-shebang-and-pathext-resolution"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L893"}
{"record":"unit","id":"deno-command:tests/unit/command_test.ts:924:1:registration","source":"deno-command","unit":"registration","path":"tests/unit/command_test.ts","line":924,"column":1,"label":"commandSyncUid","disposition":{"kind":"missing","id":"process-credentials"},"url":"https://github.com/denoland/deno/blob/336da420f4343cbb1dcbd5eed9d075ff555ed6ee/tests/unit/command_test.ts#L924"}
diff --git a/js/tests/sync.test.mjs b/js/tests/sync.test.mjs
index e16a8abc..99a916fe 100644
--- a/js/tests/sync.test.mjs
+++ b/js/tests/sync.test.mjs
@@ -110,9 +110,8 @@ describe('Synchronous Execution (.sync())', () => {
cmd.options.capture = false;
const result = cmd.sync();
- // capture option doesn't affect sync mode - always captures
- // This is different from async mode
- expect(result.stdout.trim()).toBe('test');
+ expect(result.stdout).toBeUndefined();
+ expect(result.stderr).toBeUndefined();
});
test('should handle stdin options', () => {
diff --git a/js/tests/types/command-builder.ts b/js/tests/types/command-builder.ts
new file mode 100644
index 00000000..1f79e30e
--- /dev/null
+++ b/js/tests/types/command-builder.ts
@@ -0,0 +1,100 @@
+import { $, command, CommandBuilder, create } from 'command-stream';
+import { command as coreCommand } from 'command-stream/core';
+import type { StreamResult } from '../../types/index.js';
+
+const literal = command('echo', ['a b', 42, true, 4n] as const, {
+ capture: true,
+});
+const fluent: CommandBuilder = literal
+ .arg('more')
+ .stdout('ignore')
+ .stderr('pipe')
+ .env({ A: 'a' })
+ .cwd('.')
+ .capture(false)
+ .mirror()
+ .exitCode();
+const sync: number = fluent.sync({ stdin: 'ignore' });
+const run: Promise = fluent.run({ timeout: 100 });
+const configured: CommandBuilder = $({ mirror: false }).command(
+ 'echo',
+ 'preset'
+);
+const created: CommandBuilder = create({ capture: true }).command('echo');
+const preset = coreCommand.with({ stdin: 'input' }).with({ mirror: false });
+const result: StreamResult = preset('cat').sync();
+const transformed: CommandBuilder = literal.pipe(
+ command.options({ timeout: 100 }),
+ command.arg('extra'),
+ command.capture(),
+ command.mirror(false),
+ command.exitCode
+);
+const inferred = literal.pipe(
+ command.stdout('ignore'),
+ command.capture(),
+ command.exitCode
+);
+// @ts-expect-error Multi-transform pipelines retain exit-code selection.
+const wrongSelection: string = inferred.sync();
+void wrongSelection;
+void [sync, run, configured, created, result, transformed];
+// @ts-expect-error Objects are not command arguments.
+command('echo', {});
+// @ts-expect-error Stdio has a closed vocabulary.
+literal.stdout('bad');
+// @ts-expect-error Exit-code builders return a number.
+const invalid: StreamResult = fluent.sync();
+void invalid;
+
+const manyTransforms = literal.pipe(
+ command.capture(),
+ command.mirror(false),
+ command.arg('six'),
+ command.stdout('ignore'),
+ command.stderr('ignore'),
+ command.exitCode,
+ command.options({ timeout: 100 }),
+ command.capture(false)
+);
+const manyResult: number = manyTransforms.sync();
+void manyResult;
+// @ts-expect-error Incompatible result types cannot be joined in a pipeline.
+literal.pipe(command.exitCode, (builder: CommandBuilder) => builder);
+literal.pipe(
+ command.capture(),
+ command.mirror(),
+ command.arg('a'),
+ command.cwd('.'),
+ // @ts-expect-error Long pipelines must also check intermediate result types.
+ command.exitCode,
+ (builder: CommandBuilder) => builder
+);
+// @ts-expect-error Long pipelines preserve selection after generic transforms.
+const erasedSelection: string = manyTransforms.sync();
+void erasedSelection;
+
+const twentyTransforms = literal.pipe(
+ (builder: CommandBuilder) => builder,
+ command.capture(),
+ command.capture(),
+ command.capture(),
+ command.capture(),
+ command.capture(),
+ command.capture(),
+ command.capture(),
+ command.capture(),
+ command.capture(),
+ command.capture(),
+ command.capture(),
+ command.capture(),
+ command.capture(),
+ command.capture(),
+ command.capture(),
+ command.capture(),
+ command.capture(),
+ command.capture(),
+ command.exitCode
+);
+const twentiethResult: number = twentyTransforms.sync();
+void twentiethResult;
diff --git a/js/tests/types/esm.types.ts b/js/tests/types/esm.types.ts
index 0853cdaa..c5e75ef8 100644
--- a/js/tests/types/esm.types.ts
+++ b/js/tests/types/esm.types.ts
@@ -68,7 +68,7 @@ import $, {
type AnsiConfig,
type CommandError,
type CommandResult,
- type CommandTag,
+ type ConfiguredCommandTag,
type DetachedProcess,
type OutputChunk,
type ParsedAsciicast,
@@ -114,7 +114,7 @@ export async function taggedTemplates(): Promise {
timeout: 5000,
loginShell: false,
});
- expectType>();
+ expectType>();
expectType, ProcessRunner>>();
const tag = create({ capture: true, env: { PATH: '/bin' } });
diff --git a/js/types/advanced.d.cts b/js/types/advanced.d.cts
index 77b9332c..878bda9c 100644
--- a/js/types/advanced.d.cts
+++ b/js/types/advanced.d.cts
@@ -16,17 +16,31 @@ import type { Readable, Writable } from 'node:stream';
import { EventEmitter } from 'node:events';
import type { ChildProcess, SpawnOptions } from 'node:child_process';
+export type CommandArgument = string | number | bigint | boolean;
+export type CommandStdio = 'pipe' | 'inherit' | 'ignore';
+
export class CommandBuilder implements PromiseLike {
constructor(
file: string,
- args?: readonly string[],
+ args?: readonly CommandArgument[],
options?: ProcessOptions,
select?: (result: StreamResult) => T
);
readonly file: string;
+ readonly cmd: string;
readonly args: readonly string[];
readonly options: Readonly;
+ arg(...args: CommandArgument[]): CommandBuilder;
with(options: ProcessOptions): CommandBuilder;
+ stdin(input: ProcessOptions['stdin']): CommandBuilder;
+ stdout(mode: CommandStdio): CommandBuilder;
+ stderr(mode: CommandStdio): CommandBuilder;
+ cwd(path: string): CommandBuilder;
+ env(vars: NodeJS.ProcessEnv): CommandBuilder;
+ capture(enabled?: boolean): CommandBuilder;
+ mirror(enabled?: boolean): CommandBuilder;
+ exitCode(): CommandBuilder;
+ pipe(): CommandBuilder;
pipe(
a: (builder: CommandBuilder) => CommandBuilder
): CommandBuilder;
@@ -34,25 +48,282 @@ export class CommandBuilder implements PromiseLike {
a: (builder: CommandBuilder) => CommandBuilder,
b: (builder: CommandBuilder) => CommandBuilder
): CommandBuilder;
+ pipe(
+ a: (builder: CommandBuilder) => CommandBuilder,
+ b: (builder: CommandBuilder) => CommandBuilder,
+ c: (builder: CommandBuilder) => CommandBuilder
+ ): CommandBuilder;
+ pipe(
+ a: (builder: CommandBuilder) => CommandBuilder,
+ b: (builder: CommandBuilder) => CommandBuilder,
+ c: (builder: CommandBuilder) => CommandBuilder,
+ d: (builder: CommandBuilder) => CommandBuilder
+ ): CommandBuilder;
+ pipe(
+ a: (builder: CommandBuilder) => CommandBuilder,
+ b: (builder: CommandBuilder) => CommandBuilder,
+ c: (builder: CommandBuilder) => CommandBuilder,
+ d: (builder: CommandBuilder) => CommandBuilder,
+ e: (builder: CommandBuilder) => CommandBuilder
+ ): CommandBuilder;
+ pipe(
+ a: (builder: CommandBuilder) => CommandBuilder,
+ b: (builder: CommandBuilder) => CommandBuilder,
+ c: (builder: CommandBuilder) => CommandBuilder,
+ d: (builder: CommandBuilder) => CommandBuilder,
+ e: (builder: CommandBuilder) => CommandBuilder,
+ f: (builder: CommandBuilder) => CommandBuilder
+ ): CommandBuilder;
+ pipe(
+ a: (builder: CommandBuilder) => CommandBuilder,
+ b: (builder: CommandBuilder) => CommandBuilder,
+ c: (builder: CommandBuilder) => CommandBuilder,
+ d: (builder: CommandBuilder) => CommandBuilder,
+ e: (builder: CommandBuilder) => CommandBuilder,
+ f: (builder: CommandBuilder) => CommandBuilder,
+ g: (builder: CommandBuilder) => CommandBuilder
+ ): CommandBuilder;
+ pipe(
+ a: (builder: CommandBuilder) => CommandBuilder,
+ b: (builder: CommandBuilder) => CommandBuilder,
+ c: (builder: CommandBuilder) => CommandBuilder,
+ d: (builder: CommandBuilder) => CommandBuilder,
+ e: (builder: CommandBuilder) => CommandBuilder,
+ f: (builder: CommandBuilder) => CommandBuilder,
+ g: (builder: CommandBuilder) => CommandBuilder,
+ h: (builder: CommandBuilder) => CommandBuilder
+ ): CommandBuilder;
+ pipe(
+ a: (builder: CommandBuilder) => CommandBuilder,
+ b: (builder: CommandBuilder) => CommandBuilder,
+ c: (builder: CommandBuilder) => CommandBuilder,
+ d: (builder: CommandBuilder) => CommandBuilder,
+ e: (builder: CommandBuilder) => CommandBuilder,
+ f: (builder: CommandBuilder) => CommandBuilder,
+ g: (builder: CommandBuilder) => CommandBuilder,
+ h: (builder: CommandBuilder) => CommandBuilder,
+ i: (builder: CommandBuilder) => CommandBuilder
+ ): CommandBuilder;
+ pipe(
+ a: (builder: CommandBuilder) => CommandBuilder,
+ b: (builder: CommandBuilder) => CommandBuilder,
+ c: (builder: CommandBuilder) => CommandBuilder,
+ d: (builder: CommandBuilder) => CommandBuilder,
+ e: (builder: CommandBuilder) => CommandBuilder,
+ f: (builder: CommandBuilder) => CommandBuilder,
+ g: (builder: CommandBuilder) => CommandBuilder,
+ h: (builder: CommandBuilder) => CommandBuilder,
+ i: (builder: CommandBuilder) => CommandBuilder,
+ j: (builder: CommandBuilder) => CommandBuilder
+ ): CommandBuilder;
+ pipe(
+ a: (builder: CommandBuilder) => CommandBuilder,
+ b: (builder: CommandBuilder) => CommandBuilder,
+ c: (builder: CommandBuilder) => CommandBuilder,
+ d: (builder: CommandBuilder) => CommandBuilder,
+ e: (builder: CommandBuilder) => CommandBuilder,
+ f: (builder: CommandBuilder) => CommandBuilder,
+ g: (builder: CommandBuilder) => CommandBuilder,
+ h: (builder: CommandBuilder) => CommandBuilder,
+ i: (builder: CommandBuilder) => CommandBuilder,
+ j: (builder: CommandBuilder) => CommandBuilder,
+ k: (builder: CommandBuilder) => CommandBuilder
+ ): CommandBuilder;
+ pipe(
+ a: (builder: CommandBuilder) => CommandBuilder,
+ b: (builder: CommandBuilder) => CommandBuilder,
+ c: (builder: CommandBuilder) => CommandBuilder,
+ d: (builder: CommandBuilder) => CommandBuilder,
+ e: (builder: CommandBuilder) => CommandBuilder,
+ f: (builder: CommandBuilder) => CommandBuilder,
+ g: (builder: CommandBuilder) => CommandBuilder,
+ h: (builder: CommandBuilder) => CommandBuilder,
+ i: (builder: CommandBuilder) => CommandBuilder,
+ j: (builder: CommandBuilder) => CommandBuilder,
+ k: (builder: CommandBuilder) => CommandBuilder,
+ l: (builder: CommandBuilder) => CommandBuilder
+ ): CommandBuilder;
+ pipe(
+ a: (builder: CommandBuilder) => CommandBuilder,
+ b: (builder: CommandBuilder) => CommandBuilder,
+ c: (builder: CommandBuilder) => CommandBuilder,
+ d: (builder: CommandBuilder) => CommandBuilder,
+ e: (builder: CommandBuilder) => CommandBuilder,
+ f: (builder: CommandBuilder) => CommandBuilder,
+ g: (builder: CommandBuilder) => CommandBuilder,
+ h: (builder: CommandBuilder) => CommandBuilder,
+ i: (builder: CommandBuilder) => CommandBuilder,
+ j: (builder: CommandBuilder) => CommandBuilder,
+ k: (builder: CommandBuilder) => CommandBuilder,
+ l: (builder: CommandBuilder) => CommandBuilder,
+ m: (builder: CommandBuilder) => CommandBuilder
+ ): CommandBuilder;
+ pipe(
+ a: (builder: CommandBuilder) => CommandBuilder,
+ b: (builder: CommandBuilder) => CommandBuilder,
+ c: (builder: CommandBuilder) => CommandBuilder,
+ d: (builder: CommandBuilder) => CommandBuilder,
+ e: (builder: CommandBuilder) => CommandBuilder,
+ f: (builder: CommandBuilder) => CommandBuilder,
+ g: (builder: CommandBuilder) => CommandBuilder,
+ h: (builder: CommandBuilder) => CommandBuilder,
+ i: (builder: CommandBuilder) => CommandBuilder,
+ j: (builder: CommandBuilder) => CommandBuilder,
+ k: (builder: CommandBuilder) => CommandBuilder,
+ l: (builder: CommandBuilder) => CommandBuilder,
+ m: (builder: CommandBuilder) => CommandBuilder,
+ n: (builder: CommandBuilder) => CommandBuilder
+ ): CommandBuilder;
+ pipe(
+ a: (builder: CommandBuilder) => CommandBuilder,
+ b: (builder: CommandBuilder) => CommandBuilder,
+ c: (builder: CommandBuilder