cli: bootstrap the CodeRouter CLI when cmux cr finds none - #12144
Merged
Merged
Conversation
Regression tests for #12139, committed before the fix so CI shows them failing: - non-interactive `cmux cr --version` with no coderouter/cr on PATH must print the exact install command and exit 127 without touching HOME - a CodeRouter installed by the official installer (`~/.coderouter/bin` or `$CODEROUTER_INSTALL/bin`) must be exec'd even when PATH does not list it yet - at a terminal, cmux must offer the documented installer once, run it after `y`, and exec the fresh install with the original arguments - declining must install nothing, print the install command, exit 127 The interactive cases run the CLI on a pseudo-terminal and drive the Debug-only CMUX_CODEROUTER_INSTALLER_SCRIPT seam with a local stand-in for install.sh, so no test reaches the network. Every test isolates HOME and PATH so a developer's own CodeRouter never takes part. The superseded `missingExecutableIsActionable` test, which asserted the old generic "Required CLI not found" message and that CodeRouter was never named, moves into the new suite with the new contract. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GrWb19oD1CAyTEPLjiJpe9
`cmux cr` (and every `cmux coderouter` verb cmux does not own) exec'd whatever `coderouter`/`cr` PATH offered and exited 127 otherwise, so on a machine where CodeRouter was never installed the advertised alias was dead on arrival. The passthrough now lives in one place, CLI/CMUXCLI+CoderouterPassthrough.swift, which owns resolve, bootstrap, and exec: - resolution is PATH `coderouter`, PATH `cr`, then the official installer's own target (`$CODEROUTER_INSTALL/bin`, default `~/.coderouter/bin`), so an install whose shell-profile PATH line has not reached this process still runs; no hit makes a network call - when nothing resolves and stdin and stderr are terminals, cmux shows the documented `curl -fsSL https://cmux.com/coderouter/install.sh | sh`, says what it does, asks once, and after `y` fetches the script with `/usr/bin/curl --proto =https --tlsv1.2` into a private temp dir and runs it with `/bin/sh` as a child (a failed or truncated download never reaches the shell), then re-resolves and execs the new install with the original arguments - non-interactive, declined, download failure, and installer failure all print the exact install command on stderr and exit 127 - the child environment for the installer, curl, and CodeRouter itself is the same CMUX_*/CMUXD_*-stripped one as before cmux.swift loses the moved code (net -86 lines); two of its private helpers become internal for the new file. A Debug-only CMUX_CODEROUTER_INSTALLER_SCRIPT seam lets the tests run the bootstrap against a local stand-in for install.sh. Localization: new cli.coderouter.bootstrap.* keys (en, ja); the unused generic cli.coderouter.error.notFound is retired; the alias help line changes in en and ja and its other locales are marked needs_review. docs/cli-contract.md documents the contract. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GrWb19oD1CAyTEPLjiJpe9
|
Warning Review limit reachedNext included review available in 32 minutes. View limit detailsLimit details: You’ve used all 10 included reviews currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (9)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Contributor
|
All contributors have signed the CLA ✍️ ✅ |
…oderouter-bootstrap
austinywang
marked this pull request as ready for review
September 8, 2026 11:19
austinywang
added a commit
that referenced
this pull request
Sep 8, 2026
Brings in #12131 (cmux notify inside a machine), #12154, #12144 (cmux cr bootstrap), #12112 (cloud notifications with per-client acks), #10623, #11358, #12118. Conflicts resolved: - cmux.xcodeproj/project.pbxproj: cmuxTests group children — kept both sides (CloudFileDeliveryTests from this branch, CloudNotificationSyncTests from main); normalized, test-wiring lint ok (813 test files). - web/scripts/check-devbox-image-reachable.ts: took main's portable entry-point guard over this branch's typed import.meta.main fix. Checks on the merged tree: 18 web suites 372 pass / 0 fail, tsc clean, shim image copy byte-identical, sh -n clean, swiftc -parse on every file both sides touched, VMClientError switch still exhaustive, no duplicate definitions introduced. Claude-Session: https://claude.ai/code/session_01QBDetMeke87gUWzvok9LWr
This was referenced Sep 10, 2026
aerickson
pushed a commit
to aerickson/cmux
that referenced
this pull request
Sep 13, 2026
…i#12144) * test: cover cmux cr on a machine without CodeRouter (manaflow-ai#12139) Regression tests for manaflow-ai#12139, committed before the fix so CI shows them failing: - non-interactive `cmux cr --version` with no coderouter/cr on PATH must print the exact install command and exit 127 without touching HOME - a CodeRouter installed by the official installer (`~/.coderouter/bin` or `$CODEROUTER_INSTALL/bin`) must be exec'd even when PATH does not list it yet - at a terminal, cmux must offer the documented installer once, run it after `y`, and exec the fresh install with the original arguments - declining must install nothing, print the install command, exit 127 The interactive cases run the CLI on a pseudo-terminal and drive the Debug-only CMUX_CODEROUTER_INSTALLER_SCRIPT seam with a local stand-in for install.sh, so no test reaches the network. Every test isolates HOME and PATH so a developer's own CodeRouter never takes part. The superseded `missingExecutableIsActionable` test, which asserted the old generic "Required CLI not found" message and that CodeRouter was never named, moves into the new suite with the new contract. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GrWb19oD1CAyTEPLjiJpe9 * cli: bootstrap the CodeRouter CLI when cmux cr finds none (manaflow-ai#12139) `cmux cr` (and every `cmux coderouter` verb cmux does not own) exec'd whatever `coderouter`/`cr` PATH offered and exited 127 otherwise, so on a machine where CodeRouter was never installed the advertised alias was dead on arrival. The passthrough now lives in one place, CLI/CMUXCLI+CoderouterPassthrough.swift, which owns resolve, bootstrap, and exec: - resolution is PATH `coderouter`, PATH `cr`, then the official installer's own target (`$CODEROUTER_INSTALL/bin`, default `~/.coderouter/bin`), so an install whose shell-profile PATH line has not reached this process still runs; no hit makes a network call - when nothing resolves and stdin and stderr are terminals, cmux shows the documented `curl -fsSL https://cmux.com/coderouter/install.sh | sh`, says what it does, asks once, and after `y` fetches the script with `/usr/bin/curl --proto =https --tlsv1.2` into a private temp dir and runs it with `/bin/sh` as a child (a failed or truncated download never reaches the shell), then re-resolves and execs the new install with the original arguments - non-interactive, declined, download failure, and installer failure all print the exact install command on stderr and exit 127 - the child environment for the installer, curl, and CodeRouter itself is the same CMUX_*/CMUXD_*-stripped one as before cmux.swift loses the moved code (net -86 lines); two of its private helpers become internal for the new file. A Debug-only CMUX_CODEROUTER_INSTALLER_SCRIPT seam lets the tests run the bootstrap against a local stand-in for install.sh. Localization: new cli.coderouter.bootstrap.* keys (en, ja); the unused generic cli.coderouter.error.notFound is retired; the alias help line changes in en and ja and its other locales are marked needs_review. docs/cli-contract.md documents the contract. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GrWb19oD1CAyTEPLjiJpe9 --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #12139
Problem
cmux cr ...(and everycmux coderouter ...verb cmux does not own) exec'd whatevercoderouter/crPATH offered and exited 127 otherwise. On a machine where CodeRouter was never installed, the alias cmux advertises in--helpanddocs/cli-contract.mdwas dead on arrival, and the generic "Required CLI not found" message did not even say what to install.What changes
The passthrough now has one owner,
CLI/CMUXCLI+CoderouterPassthrough.swift(300 lines), which handles resolve → bootstrap → exec.CLI/cmux.swiftloses the moved code (net −86 lines) and two of its private helpers (resolveExecutableInPath,cliDebugLog) become internal so the new file can reuse them.coderouter, PATHcr(unchanged), then the official installer's own target:$CODEROUTER_INSTALL/bin/coderouter, default~/.coderouter/bin/coderouter, computed exactly asinstall.shcomputes it. That covers the second class of failure behind this issue: an install whose shell-profile PATH line has not reached the calling process (a terminal opened before the install, a hook, a script). No hit makes a network call.~/.coderouter/bin, PATH line appended to the shell profile), and asks once:y, cmux fetches the script with/usr/bin/curl --proto =https --tlsv1.2 -fsSL --max-time 60into a private0700temp dir, runs it with/bin/shas a child on the same terminal, then re-resolves andexecves the new install with the original arguments.Error: CodeRouter CLI is not installed. Install it, then retry:+ the command), declined, curl failure (curl's own stderr line first, then cmux's summary with curl's exit status), installer failure (installer's own stderr first, then cmux's line with its exit status).CMUX_*/CMUXD_*-stripped one as before; the cmux socket is never opened on this path.cmux coderouter status|machines|claude(cmux-owned verbs over the app socket) are untouched.Trade-offs, stated
crsymlink, and the profile PATH line. A Swift re-implementation of the download would duplicate that release-layout knowledge in a second place that drifts silently, and it would need either a version pin or a manifest fetch cmux does not own. Bundling the binary with the app would tie CodeRouter's release cadence to cmux's and add signing work. The cost: the bootstrap depends onhttps://cmux.com/coderouter/install.shstaying the stable entry point (it already is the documented one) and on/usr/bin/curland/bin/sh, both of which macOS always ships; the script itself refuses to run without curl anyway.curl … | shreturnssh's status (0 for an empty script when curl fails withoutpipefail) and can hand a truncated download to the shell. Fetch-then-run gives honest exit statuses for both halves and never executes a partial script. The user still sees the exact command they can run by hand.~/.coderouter/bin), not~/.cmux/bin. Plaincrthen works in the user's shells after the installer's PATH line, a later manual install does not create a second copy, and cmux keeps finding it without PATH. The issue floated~/.cmux/bin; that would have madecrcmux-only.CMUX_CODEROUTER_AUTO_INSTALL=1opt-in is wanted later it is a one-line addition, but I did not add an unrequested surface.cmux cr Xnever ranX, and scripts already handle 127 as "command not found". The reason is always on stderr.CMUX_CODEROUTER_INSTALLER_SCRIPT(compiled only underDEBUG, likecliDebugLog) points the bootstrap at a local stand-in forinstall.shso the tests exercise offer → confirm → install → re-resolve → exec without the network. Release builds have no override. The real fetch is covered by the manual verification below, not by a unit test.crorcoderouteron PATH is still exec'd. Detecting it would need a probe spawn (for example--version) on every invocation, which contradicts "existing installs are exec'd unchanged" and adds latency to every call, or a signature check that would break on the npm-distributed CodeRouter (a Node script, which is what Austin's own machine has at~/.local/bin/coderouter). Austin marked it probably not worth it; recorded here.cli.coderouter.aliasesnow reads "(aliases for the CodeRouter CLI; offers to install it when missing)"; the other 18 locales keep their old text and are markedneeds_reviewfor the release translation pass, per the localization workflow. The unusedcli.coderouter.error.notFoundkey is removed from the catalog.Before / after
Before, shipped 0.64.22 (Austin's daily driver), restricted PATH plus a temporary HOME, exactly the issue's command:
The shipped build predates the alias entirely (PR #10109 is not in 0.64.22), so on the daily driver
cmux cris not even 127. The exit-127 behavior in the issue ismain; the failing-test commitdfd801a9b1shows it in CI (run linked below).After, tagged Debug build of this branch: pending, filled in below once the queued cloud build lands.
Checks run
dfd801a9b1adds the tests only,1c3541e1fathe fix. Focusedtest-e2e.ymlruns (cmux-unitscheme, so the whole test target compiles):dfd801a9b1,cmuxTests/CLICoderouterBootstrapTests: red, 4 tests failed with 16 issues (exit 127 without the install command, no install-dir resolution, no offer at a terminal): https://github.com/manaflow-ai/cmux/actions/runs/342180909461c3541e1fa,cmuxTests/CLICoderouterBootstrapTests: green, 5 tests (non-interactive, decline,ybootstrap on a pseudo-terminal, install-dir resolution ×2 parameterized, existing-install guard): https://github.com/manaflow-ai/cmux/actions/runs/342181132741c3541e1fa,cmuxTests/CLICoderouterAliasTests(existing passthrough, env scrubbing, ja help entry): green, 6 tests: https://github.com/manaflow-ai/cmux/actions/runs/34218125175ci.ymlis path-filtered and does not run for CLI changes on pull requests (ci-statuscomes from the fallback). A manual dispatch on the merged HEAD (https://github.com/manaflow-ai/cmux/actions/runs/34219195504) could not reach the Swift jobs:web-typecheckfails on main's ownscripts/check-devbox-image-reachable.ts(148,17): error TS2339: Property 'main' does not exist on type 'ImportMeta'(from Check that the shipped devbox image is reachable, only when the pair changed #12132), andlinux-preflightgates every Swift job on it. Nothing in this PR touchesweb/.python3 scripts/normalize-pbxproj.py+./scripts/check-pbxproj.sh: ok../scripts/lint-pbxproj-test-wiring.sh: ok (800 test files).xcrun swiftc -parseon every touched Swift file: clean (no local build; the tagged build is the compile check)..github/swift-warning-budget.tsvchange; the new file uses no pattern from the budget.cli.coderouter.bootstrap.*keys plus thecli.coderouter.aliaseshelp line, all with en and ja values inResources/Localizable.xcstringsand resolved through the existingCMUXDiffViewerLocalizationpath (the ja--helptest still passes with the new text).docs/cli-contract.mdhas no localized variant. The one remaining bare-English surface is the pre-existing unlocalizedcoderouterUsageblock inCLI/CMUXCLI+Coderouter.swift, where I only reworded one sentence; localizing that block is out of scope for this issue.🤖 Generated with Claude Code
https://claude.ai/code/session_01GrWb19oD1CAyTEPLjiJpe9
Note
Medium Risk
Interactive bootstrap downloads and executes a remote installer script (user-confirmed only), which adds supply-chain and execution surface beyond the previous PATH-only exec.
Overview
cmux crand passthroughcmux coderouterverbs no longer fail with a generic “CLI not found” when CodeRouter is absent. Passthrough logic moves intoCMUXCLI+CoderouterPassthrough.swift, which resolvescoderouter/cron PATH, then~/.coderouter/bin(or$CODEROUTER_INSTALL/bin), stripsCMUX_*/CMUXD_*, andexecves the real CLI unchanged.When nothing resolves, interactive terminals get a one-time offer to run the documented installer: cmux downloads
install.shwith curl (HTTPS, TLS 1.2+), runs it viash(not a blindcurl | sh), re-resolves, then execs. Non-interactive, declined, or failed installs print the exactcurl -fsSL https://cmux.com/coderouter/install.sh | shline on stderr and exit 127. Cmux-ownedcoderouter status|machines|claudeover the app socket are unchanged.Help text,
docs/cli-contract.md, and localization (newcli.coderouter.bootstrap.*strings) document the new behavior;CLICoderouterBootstrapTestscover offer/decline/install paths via a DEBUG-only installer override.Reviewed by Cursor Bugbot for commit d6d07db. Bugbot is set up for automated code reviews on this repo. Configure here.