Report a failed command's status through both code and exitCode - #110
Conversation
Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: #38
This change addresses issue #38 by adding error.exitCode as an alias for error.code to maintain compatibility with Node.js standard error handling patterns while preserving backward compatibility. Changes: - Add error.exitCode property alongside error.code in all error creation locations - Fix $.exit.mjs virtual command to throw proper Error objects instead of plain objects - Add comprehensive tests for exitCode compatibility - Add example script demonstrating both old and new error handling patterns Both error.code and error.exitCode now contain the same exit code value, allowing developers to use either the traditional command-stream pattern or the standard Node.js pattern. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
|
I think we should support both |
|
🤖 AI Work Session Started Starting automated work session at 2026-09-15T11:57:32.551Z The PR has been converted to draft mode while work is in progress. This comment marks the beginning of an AI work session. Please wait for the session to finish, and provide your feedback. Runtime: solve |
Resolve conflicts caused by the js/ + rust/ repository restructure: - drop the stale src/$.mjs copy (source now lives in js/src/) - keep the issue #38 files under js/ where the tree was renamed
Failing commands now throw errors that carry the exit status under both `code` (Node.js child_process naming) and `exitCode` (execa, zx, nano-spawn and Bun Shell naming). A shared createCommandError factory is the single place where both names are set, and virtual command errors build their attached result through createResult so error.result.exitCode is defined as well. Rust gains the matching accessors Error::code()/Error::exit_code() and CommandResult::error_for_status().
Extend the ported [nonzero-exit] competitor case in both languages to assert the failure path as well: Execa, zx, nano-spawn and the Bun shell name the status exitCode while Node.js names it code, and command-stream now answers to both. Adds Rust unit tests for Error::code()/exit_code() and CommandResult::error_for_status(), a competitor probe experiment and a runnable example.
Both READMEs, the JS best practices guide and the competitor audits now state that a failed command reports its status under code and exitCode (exit_code in Rust). Adds the changeset and the Rust changelog fragment that release the change.
Errors escaping the runner in errexit mode now always carry the shell-compatible status in `exitCode`, including spawn failures whose `code` stays the POSIX errno string (issue #38). Also adds temporary diagnostics to the pipeline alias test to identify a macOS-only CI failure where the virtual `exit` command is spawned as a real executable.
The macOS run of `bun test js/tests/` failed `carries both aliases for a failing pipeline` with `code: "ENOENT"` instead of 19. The shared `tests/test-helper.mjs` registers its cleanup hooks while the module is evaluated, so they belong to the first test file that imports it and no other file is reset; files that call `disableVirtualCommands()` in their own hooks leak that flag into whatever runs next, and the order differs per platform. With virtual commands disabled the parsed pipeline is spawned command by command, so the shell builtin `exit` is looked up in $PATH and Bun reports ENOENT. The test now enables virtual commands itself and restores the shell settings it changes, and both effects are reproduced in ./experiments.
Working session summaryDone — PR #110 is green and out of draft: #110 CI on
Check rollup on the PR: 28 SUCCESS, 6 SKIPPED (release jobs), 0 failures. Local verification
Rest of the working session summary (1 KB)This summary was automatically extracted from the AI working session output. |
🤖 Solution Draft LogThis log file contains the complete execution trace of the AI solution draft process. 💰 Cost: $20.744002📊 Context and tokens usage:Claude Opus 5: (5 sub-sessions)
Total: (11.3K new + 452.1K cache writes + 23.6M cache reads) input tokens, 175.4K output tokens, $20.744003 cost 🤖 Models used:
📎 Log file uploaded as Gist (7784KB)Now working session is ended, feel free to review and add any feedback on the solution draft. |
🎉 Auto-mergedThis pull request has been automatically merged by hive-mind.
Auto-merged by hive-mind with --auto-merge flag |
Fixes #38 with Option 1 (backward-compatible alias): a failing command now
reports its status under both
code(Node.jschild_processnaming) andexitCode(Execa, zx, nano-spawn, Bun shell naming), in both languages, asrequested in review.
How to reproduce
What changed
JavaScript
js/src/$.result.mjs: newcreateCommandError(message, { code, stdout, stderr, result })factory — the single place that sets
codeandexitCodeon a failure,mirroring how
createResulthandles the result-level alias from Add exitCode alias for code #36.$.process-runner-execution.mjs(async and sync),$.process-runner-pipeline.mjs(4 sites),
$.process-runner-virtual.mjs,commands/$.exit.mjs.handleVirtualErrorbuilds its result throughcreateResult, so the attachederror.result.exitCodeis defined too; it also normalizes a POSIX errnocode(ENOENT,EACCES, …) into a numeric status instead of leaking thestring into
result.code.attachExitCodeAlias(error, code)covers the failures that never reached arunning process. Node.js keeps the POSIX errno in
codethere (ENOENT,EACCES, …) and the library relies on that string to recognise a launchfailure, so
codeis left alone andexitCodereports the shell-compatiblestatus the result already carries (127 for a missing executable, 126 for a
permission error).
Rust
Error::code()reports the status of a failure (CommandFailed, 127 for amissing command, 126 for a permission error, 130 for a cancelled command,
Nonefor a parse error) andError::exit_code()is its alias.CommandResult::error_for_status()turns a non-zero result into that error —the Rust counterpart of
errexit.Competitor verification
experiments/issue-38-error-exitcode-competitors.mjsprobes the installedpackages with the same failing command and prints what each one exposes:
Both spellings are in real use, which is why command-stream answers to both.
The ported competitor case
[nonzero-exit]is extended accordingly in bothlanguages (
js/tests/competitor-compatibility.test.mjs,rust/tests/competitor_compatibility/behavior.rs), so the corpus now covers thefailure path, not just the result path.
Tests
js/tests/error-exitcode-alias.test.mjs(new, 9 cases): errexit error,attached result, statuses 1/2/127/255, failing external command, failing
pipeline with pipefail, failing
.pipe()result, missing executable, abinary that cannot be launched at all, and the unchanged non-errexit path.
rust/tests/utils.rs: 5 cases forError::code()/exit_code()anderror_for_status(), next to the existing Add exitCode alias for code #36 alias test.js/examples/error-exitcode-alias.mjs: runnable demonstration of both styles.The macOS-only CI failure this PR had to chase
carries both aliases for a failing pipelinefailed onbun on macos-latestonly, reporting
code: "ENOENT"instead of19. Root cause, reproduced in./experiments:js/tests/test-helper.mjsregisters itsbeforeEach/afterEachwhile themodule is evaluated. ES modules are evaluated once, so the hooks belong to the
first test file that imports the helper and no other file is reset
(
experiments/issue-38-hook-scope/). Test files that calldisableVirtualCommands()in their own hooks therefore leak that flag intowhatever Bun runs next, and the file order differs per platform.
command, so the shell builtin
exitis looked up in$PATHand Bun reportsENOENT/127 instead of the pipeline status(
experiments/issue-38-virtual-disabled-pipeline.mjsprints both outcomes).The test now enables virtual commands in its own
beforeEachand restores theshell settings it changes, so it neither depends on nor adds to the leak.
Both underlying defects are pre-existing and out of scope here, but worth their
own issues:
js/tests/test-helper.mjsonly cleans up one test file, so the whole suiteruns with per-file state leaking in a platform-dependent order.
real executables (
exit, and the same would apply tocd,export, …)instead of handling them as builtins.
Local runs:
bun test js/tests/→ 1370 pass, 11 skip, 28 fail — all 28 arejq: not foundin this sandbox and unrelated to this change;cargo test→all suites pass, including the new doctest; eslint, prettier and
cargo fmt/clippy -D warningsare clean.Release
js/.changeset/error-exitcode-alias.md(patch)rust/changelog.d/20260915_000000_error_exit_code_alias.md(patch)