Skip to content

Address issues #226–#232: trusted publishing, timeout, non-login shell, env removal, live stdin, artifact selection, detached spawn - #234

Merged
konard merged 24 commits into
mainfrom
issue-233-b89721e32913
Oct 23, 2026
Merged

konard merged 24 commits into
mainfrom
issue-233-b89721e32913

Conversation

@konard

@konard konard commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Summary

This PR resolves all seven open issues (#226–#232) as #233 asks. Each requirement is mapped to its implementation and to a test that reproduces the problem without the change in dev/log/issues/233/pulls/234/analysis/requirements.md.

#226: crates.io trusted publishing

  • The release and instant-release jobs in .github/workflows/rust.yml get id-token: write.
  • They run rust-lang/crates-io-auth-action (pinned to v1.0.5) with continue-on-error: true.
  • They publish with steps.crates_auth.outputs.token || secrets.CARGO_REGISTRY_TOKEN || secrets.CARGO_TOKEN. This follows the issue's policy comment: OIDC first, token secrets kept as the fallback.
  • .github/scripts/publish-preflight.mjs checks the OIDC token first (js/tests/publish-preflight.test.mjs).
  • The setup is documented in .github/DEPLOYMENT.md.

#227: $({ timeout })

  • The JS timeout option (ms) covers simple commands, pipelines, stream(), exec specs and .sync().
  • It combines with a caller signal, reports result.timedOut, and rejects invalid values with a TypeError.
  • Rust parity: RunOptions::timeout, StreamingRunner::timeout and timed_out().
  • Tests: js/tests/timeout-option.test.mjs, rust/tests/timeout.rs.

#228: non-login shell by default

  • Command strings now run with sh -c instead of -l -c, so profile files no longer print into captured output or override the PATH from env.
  • loginShell: true restores the old behaviour.
  • js/tests/login-shell.test.mjs uses the issue's HOME/.profile reproduction.

#229: removing environment variables

  • Rust:
    • RunOptions::env_remove
    • StreamingRunner::env_remove/env_remove_all
    • TerminalCaptureOptions::env_remove/env_clear, forwarded to portable_pty::CommandBuilder
  • JS: an env value of undefined removes that variable from the child.
  • Tests: rust/tests/env_remove.rs, rust/tests/terminal_options.rs (the issue's SECRET_TOKEN case), js/tests/env-undefined-removes.test.mjs.

#230: live stdin with an exact argv (Rust)

  • StreamingRunner::stdin_pipe() keeps stdin open. OutputStream::write_stdin/close_stdin work while output streams.
  • ProcessRunner::from_argv(program, args, options) skips the shell and virtual commands.
  • Tests: rust/tests/stdin_pipe.rs, rust/tests/process_argv.rs.

#231: terminal artifact selection

  • JS artifacts: [...] and Rust TerminalCaptureOptions::artifacts select the files to write. Renderers for unselected files are skipped, so no GIF is rendered unless one is asked for.
  • Rust also gets:
    • recording.gif, encoded with gif and drawn with font8x8 glyphs, so no SVG rasteriser is needed;
    • aspect_ratio;
    • on_trace.
  • Tests: js/tests/terminal-artifact-selection.test.mjs, rust/tests/terminal_options.rs.

#232: fire-and-forget detached spawn

  • JS spawnDetached(), runner.spawnDetached() and $({ detached: true }):
    • start the command in its own session with ignored stdio and unref();
    • resolve with { pid } on spawn and reject on error.
  • Rust StreamingRunner::detach() and ProcessRunner::spawn_detached() do the same.
  • Tests: js/tests/detached-spawn.test.mjs (pid = pgid, session leader, outlives the caller), rust/tests/detach.rs.

Docs, feature-catalog examples (JS Node/Bun and Rust, checked by scripts/check-parity.mjs), a changeset and a Rust changelog fragment are included.

CI fixes needed along the way

Check Cause Fix
JS tests on Bun 1.4.4 Bun 1.4.4 replaced the glob matcher that js/src/bun-shell/glob.mjs ports, and nesting is no longer limited to 10 levels The test checks the ported v1.4.2 limit directly (probe: experiments/issue-233/bun-glob-nesting.mjs)
JS tests on macOS macOS ps has no sid keyword The session leader is read from the s in ps -o stat
Benchmarks --locked gif/font8x8 were missing from rust/benchmarks/Cargo.lock Lockfile updated
Language parity Bumping tokio in rust/benchmarks/Cargo.toml counted as a Rust benchmark change, but JS benchmark dependencies live in js/package.json, which is never counted .github/scripts/check-language-parity.sh treats the benchmark manifest like its lockfile. A new benchmark target still adds a source file and is caught (js/tests/language-parity.test.mjs)
Dependency freshness (also red on main) Newer releases of eslint, eslint-plugin-prettier, jscpd, tokio, libc, vt100, ureq and execa All updated except execa. vt100 0.18 takes NonZeroU16 sizes and returns String state, and terminal capture converts at that boundary. vt100 is not part of the public API

execa is held at 10.1.1

execa 10.2.0's own declarations fail to type-check with exactOptionalPropertyTypes and skipLibCheck: false:

TS2344 in types/return/result-all.d.ts: Types of property 'preferLocal' are incompatible.

command-stream re-exports those types and checks them with these settings.

  • I reported it upstream: TS2344 in result-all.d.ts with exactOptionalPropertyTypes since 10.2.0 sindresorhus/execa#1265. The minimal reproduction is in experiments/issue-233/execa-types/.
  • Because JSON has no comments, the freshness check now reads npm blockers from js/dependency-blockers.json. The blocker counts only while its issue is open.
  • The bare npm outdated step became bun scripts/check-dependencies.mjs --installed-npm. That step still fails when a lockfile falls behind its declaration (tests in js/tests/dependency-freshness.test.mjs).

Maintainer step after merge (#226)

Register the trusted publisher on crates.io:

package-registry-manager setup --registry crates-io --migrate --execute

Until then, releases keep publishing through the token fallback.

Verification

All nine CI workflows pass on the final commit. Locally:

  • bun test js/tests: 2783 pass, 0 fail.
  • cargo fmt --check, cargo clippy --all-targets -D warnings, cargo test, and cargo check --locked for the benchmarks all pass.
  • rust-script --test passes for the release scripts.
  • lint, check:types, format:check, check:duplication and check:parity pass.
  • bun scripts/check-dependencies.mjs and --installed-npm pass.

Fixes #233
Fixes #226
Fixes #227
Fixes #228
Fixes #229
Fixes #230
Fixes #231
Fixes #232

Adding .gitkeep for PR creation (default mode).
This file will be removed when the task is complete.

Issue: #233
@konard konard self-assigned this Oct 23, 2026
konard added 21 commits October 23, 2026 04:39
Add id-token: write and rust-lang/crates-io-auth-action (v1.0.5 commit)
before publish-crate.rs in the release and instant-release jobs, and pass
its token as CARGO_REGISTRY_TOKEN. The token secrets remain a fallback
until the trusted publisher is registered. The credential preflight now
requires an OIDC identity and checks the OIDC token first.
…ion (#228)

Every Unix shell was spawned as `sh -l -c`, so /etc/profile and ~/.profile
ran before each command: profile output leaked into captured stdout and
profile PATH edits shadowed the caller's env. Spawn `-c` by default and
make the login shell opt-in with `loginShell: true` for simple commands,
sync mode and native pipeline stages alike.

feat(js): add the timeout option and result.timedOut (#227)

`timeout` (ms) stops the command through the same path as an aborted
`signal` (killSignal + killGrace), for simple commands, pipelines,
stream(), exec-mode runners and .sync() (Bun.spawnSync / spawnSync
timeout). Results report `timedOut`. Invalid values throw a TypeError.
Aborting a pipeline whose stage is a virtual command (e.g. `sleep 3 | cat`)
now resolves with the signal exit code instead of rejecting with
'Sleep cancelled'.
…227, #229, #230, #232)

- RunOptions.timeout / StreamingRunner::timeout stop the child with
  kill_signal, escalate to SIGKILL after kill_grace_ms and report
  timed_out() plus a signal exit code.
- RunOptions.env_remove and StreamingRunner::env_remove{,_all} remove
  single inherited variables (removal wins over env overrides).
- StreamingRunner::stdin_pipe with OutputStream::write_stdin/close_stdin
  keeps stdin open for argv streaming.
- StreamingRunner::detach and ProcessRunner::spawn_detached start a
  command in its own process group with null stdio and return its pid.
…#229, #231)

- captureTerminal/openTerminal accept artifacts: ['transcript', 'frames',
  'cast', 'snapshot', 'recording', 'gif'] (default: all); renderers for
  unrequested artifacts, including the GIF, are skipped.
- The built-in env command drops undefined values like spawned processes
  do; README documents that env replaces the environment.
…ing.gif, aspect_ratio and on_trace (#229, #231)

TerminalCaptureOptions gains:
- env_remove / env_clear, forwarded to portable_pty CommandBuilder
- artifacts: Option<Vec<TerminalArtifact>> (None = all); unselected
  renderers are skipped
- recording.gif, drawn from each frame's text grid with font8x8 glyphs
  and encoded with the gif crate (no SVG rasteriser)
- aspect_ratio, deriving rows like the JS aspectRatio option
- on_trace with TerminalTraceEvent kinds matching the JS onTrace types
spawnDetached(command, options), $({ detached: true }) and
ProcessRunner#spawnDetached() start the command in a new session with
stdio ignored, unref() it and resolve with the pid once 'spawn' fires
(rejecting on 'error'). cwd, env, preferLocal and loginShell apply.

The detailed kill() documentation moves to js/docs/SIGNALS.md so the
README stays within the 2500-line documentation limit.
The benchmark crate depends on command-stream by path, so the new
recording.gif dependencies must be in its lock file for --locked builds.
…#232)

macOS ps has no sid keyword; the s flag in the state column marks a
session leader on both procps and BSD ps.
Bun 1.4.4 (installed by bun-version: latest) replaced its glob matcher
with one that nests groups without a limit, so comparing with the running
Bun.Glob failed on every Bun job. The port keeps the 1.4.2 semantics.
vt100 0.18 takes sizes as NonZeroU16 and returns formatted state as
String; terminal capture converts at the boundary. The dependency
freshness check flagged all four as stale.
JSON has no comments, so js/dependency-blockers.json maps a package to an
open issue. The declaration check exempts it while the issue stays open,
and the installed check replaces the bare npm outdated with one that only
tolerates the blocked latest release, never a lockfile behind its
declaration.
execa 10.2.0 is held back by sindresorhus/execa#1265: its declarations fail
to type-check with exactOptionalPropertyTypes, and command-stream
re-exports them.
@konard konard changed the title [WIP] Address all 7 open issues in link-foundation/command-stream Address issues #226–#232: trusted publishing, timeout, non-login shell, env removal, live stdin, artifact selection, detached spawn Oct 23, 2026
Bumping tokio in rust/benchmarks/Cargo.toml failed the parity check,
although JavaScript benchmark dependencies live in js/package.json and
never count. A new benchmark target still adds a source file and is
caught.
@konard
konard marked this pull request as ready for review October 23, 2026 07:12
@konard

konard commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Working session summary

PR #234 now covers all seven issues (#226–#232), and every CI workflow passes on the final commit (4550c8a). It's marked ready for review: #234

The PR body has a summary and tests for each issue, plus Fixes #233 and Fixes #226 through Fixes #232, each on its own line. There were no comments on the issue or the PR, and main has nothing that isn't already in the branch.

One thing I did outside the repo: I opened an issue on execa's GitHub to report a bug: sindresorhus/execa#1265. execa 10.2.0's own type declarations fail TypeScript's check under the strict settings we use (exactOptionalPropertyTypes with skipLibCheck: false). command-stream re-exports those types, so I kept execa at 10.1.1, which is the newest version that passes. The reproduction is in experiments/issue-233/execa-types/.

Changes this session to get CI green:

  • Dependency freshness check:
    • Updated eslint, eslint-plugin-prettier, jscpd, tokio, libc, ureq and vt100.
    • vt100 0.18 changed how it takes sizes and returns screen state, so terminal capture now converts at that boundary. vt100 isn't part of the crate's public API.
    • The Rust lockfiles were refreshed again for releases (cc, find-msvc-tools, syn) that came out while CI was running.
    • This check was also failing on main.
  • Holding back execa:
    • The freshness check had no way to excuse an npm package, so it now reads js/dependency-blockers.json. An entry there only counts while its linked issue is still open.
    • The plain npm outdated step now uses the same blockers. It still fails if a lockfile falls behind package.json, or if npm outdated itself errors.
    • There are tests for both, and the README documents the file.
  • Language parity check: bumping tokio in rust/benchmarks/Cargo.toml was counted as a Rust benchmark change with no JS equivalent. That file is now treated like its lockfile. Adding a new Rust benchmark file still requires a JS one, and tests cover both cases.

Locally, all 2783 Bun tests pass from the repository root, along with cargo test, clippy, fmt, lint, type checks, formatting, the parity and duplication checks, and the release-script tests. That run came before the parity-script fix, whose test file passes on its own (11/11).

Left for after merge:

  • Release key for Publish command-stream to crates.io through trusted publishing (remove CARGO_TOKEN) #226 (maintainer): a maintainer needs to register the trusted publisher on crates.io with package-registry-manager setup --registry crates-io --migrate --execute. Until then, releases keep publishing with the existing token secrets, as the issue's policy comment asks.
  • Moving to execa 10.2.0: once the upstream issue is closed, the freshness check will fail again until execa is bumped and its entry is removed from js/dependency-blockers.json.

This summary was automatically extracted from the AI working session output.

@konard

konard commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

🤖 Solution Draft Log

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost: $23.043525

📊 Context and tokens usage:

Claude Opus 5.5: (10 sub-sessions)

  1. 115.8K / 1M (12%) input tokens, 11.8K / 128K (9%) output tokens
  2. 113.6K / 1M (11%) input tokens, 27.0K / 128K (21%) output tokens
  3. 115.9K / 1M (12%) input tokens, 25.9K / 128K (20%) output tokens
  4. 115.7K / 1M (12%) input tokens, 42.7K / 128K (33%) output tokens
  5. 112.6K / 1M (11%) input tokens, 30.8K / 128K (24%) output tokens
  6. 116.6K / 1M (12%) input tokens, 34.9K / 128K (27%) output tokens
  7. 116.6K / 1M (12%) input tokens, 25.5K / 128K (20%) output tokens
  8. 115.9K / 1M (12%) input tokens, 29.6K / 128K (23%) output tokens
  9. 116.9K / 1M (12%) input tokens, 35.3K / 128K (28%) output tokens
  10. 106.5K / 1M (11%) input tokens, 33.7K / 128K (26%) output tokens

Total: (26.4K new + 997.6K cache writes + 38.5M cache reads) input tokens, 363.6K output tokens, $23.043525 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: opus (claude-opus-5-5)
  • Thinking level: high (~23999 tokens)
  • Model: Claude Opus 5.5 (claude-opus-5-5)

📎 Log file uploaded as Gist (16594KB)


Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard
konard merged commit 1da028d into main Oct 23, 2026
67 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment