fix(cli): cross-platform config and locales paths, and a bash-path test repair - #168
Conversation
`wmux config path` built the path from the caller's own home directory:
console.log(`${os.homedir()}\.wmux\config.toml`)
That only holds when the CLI runs on the same Windows box as wmux. Run it from
WSL, or from a devcontainer over the bridge, and it prints
`/home/vscode\.wmux\config.toml` — not the file wmux reads, and not a
well-formed path on either OS. Editing "the config file" there edits nothing,
which is a long way to walk before finding out why a setting had no effect.
loadUserConfig() already records the real path and `config.get` returns it, so
ask. The local guess stays as the answer when no instance is reachable, but
built with path.join so it is at least self-consistent with the filesystem the
CLI is actually on.
`wmux locales path` cannot be fixed the same way — `config.get` carries no
locales directory, so there is nothing to ask the instance for. The commit
after this one gives both fallbacks a spelling that is at least well-formed for
the OS in hand; the locales one can still name the wrong file when the CLI and
wmux are on opposite sides of the WSL boundary.
The test spawns dist/cli/wmux.js, and compiles it first. It originally spawned
resources/cli/wmux.js on the grounds that that is what a released wmux runs,
which turns out not to be so: package.json's `bin`, electron-builder's
extraResources and step 7 of the release process all name dist/cli/wmux.js.
resources/cli/wmux.js is a checked-in copy of that build with nothing enforcing
the copy — it is 1519 diff lines behind a build of master's own src/ as of
1.0.0 — so a test spawning it asserts against whatever was committed last
rather than against the branch under test. Building in beforeAll costs ~3s and
is needed either way: dist/ is gitignored and `npm test` runs ahead of
`npm run build:main`.
Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 16b0d4c, opened as part of amirlehmam#166)
Both no-instance fallbacks hardcoded backslashes:
console.log(`${home}\.wmux\locales`)
which is the same bug the previous commit fixed for `config path`, in the one
place `config path` still has to guess and in `locales path`, which has no
instance-side answer to ask for at all. From WSL or a container that prints
`/home/vscode\.wmux\locales` — a string that names nothing on either side of
the boundary.
Pick the separator from the home directory that was found rather than from
`process.platform`, because the interesting case is exactly the one where they
disagree: a POSIX $HOME (`/home/vscode`, no backslash anywhere) gets
path.posix.join, everything else gets path.join. Node's path.join on Windows
would join a POSIX home with backslashes and produce the same hybrid.
$HOME is preferred over USERPROFILE now, reversing the old order. Under WSL
both are set — USERPROFILE is the Windows one arriving over interop — and the
CLI's own filesystem is the Linux one.
This is the two path-fallback hunks of c693c06. The third hunk
in that commit fixes findNpiperelay() to split PATH on path.delimiter and to
read `Path` as well as `PATH`; it is left out because findNpiperelay() does not
exist here — it arrives with the devcontainer bridge, and the fix will ride
with it.
`locales path` can still name the wrong directory when the CLI and wmux sit on
opposite sides of the WSL boundary. Fixing that needs a `locales.get` that
reports the loaded directory, the way config.get already reports `path`.
Co-Authored-By: Claude <noreply@anthropic.com>
`bashPathCandidates()` offered `/c`, `/mnt/c`, `/cygdrive/c` in that fixed order, and `toBashPath()` documented Git Bash's `/c` as preferred outright. That is right on a Windows box with Git for Windows and wrong everywhere else: in a Linux checkout — a container, WSL, a CI runner — the bash on PATH mounts a Windows drive at `/mnt/c` if it can see one at all, and leading with a spelling that never resolves puts the fallback branch in charge. So the order is now conditional on `process.platform` while the set stays fixed: Git Bash first on Windows, the WSL mount first elsewhere, all three always offered, and the probe still decides. `toBashPath()`'s comment is qualified to match — off Windows there is no Git Bash to prefer. `bashExists()` probes through a login shell (`-lc`) so it sees the environment the orchestrator scripts are run under rather than a bare non-interactive one, and names $0 `_`, which is what it is — a placeholder, not the shell. `orchestration-status-vocab.test.ts` gains a `bashExists(hookPath)` term in its skip condition. `hasBash()` alone only says a bash exists; it does not say that bash can see the hook script, which is the thing the suite needs and the case this change exists to handle. Unreachable now skips loudly instead of failing on a path the shell cannot open. `bash-path.test.ts` moves in the same commit because it cannot survive the helper change on its own — it pinned the Git-Bash-first order literally, and four of its assertions go red on Linux and macOS the moment the order becomes conditional. Splitting the two would leave a commit that fails `npm test` for anyone not on Windows. Rewritten so the invariant and the variant are asserted separately: the spelling set order-independently with `arrayContaining` plus a length, the order once against the same `process.platform` condition the helper uses. The probe-driven case gains a Git Bash counterpart to the WSL one, so "the probe decides, not the ordering" is covered from both directions. On a Linux run neither suite changes result — both are green before and after, and the status-vocab suite still executes rather than skipping. The change is for the hosts where the reachable bash is not the one the ordering assumed; verifying that half needs a Windows box with WSL bash but no Git Bash. This is c693c06's test-helper hunks plus 64fb8d8, from the branch in amirlehmam#166. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both PRs ended by asking which direction I wanted for the checked-in duplicates. This is the answer: keep them, and check them. resources/shell-integration/ is a copy of src/shell-integration/, and resources/cli/*.js a copy of the tsc output in dist/cli/. Nothing kept either in step and both had rotted — the packaged PowerShell integration predated the pipe's auth gate, so it sent V1 lines with no token and every report was rejected into a void with no way to notice, and the CLI copy was ~1700 diff lines behind its own source. That is exactly #137, where a second copy of the app icon went stale for three releases: a duplicate with no enforcement is not a duplicate, it is a time bomb. #169 resynced today's drift; this stops tomorrow's. Deletion was the other option offered and is not taken here. Nothing wmux ships reads either directory (extraResources maps src/shell-integration and dist/cli), but external consumers read them out of a repo checkout — which is how the drift was found — and removing a path they depend on is not something to do silently in a patch release. Split by what each half costs: - npm test covers shell-integration, which needs no build, so the check gates a release through CI's existing test step. - npm run verify:resources covers the CLI artifacts too, and CI runs it after build:main, which is what makes dist/cli exist. It skips loudly rather than passing quietly when dist/ is absent — a check that silently does nothing reads as coverage it is not providing. resources/cli/*.js is regenerated here to make the invariant true.
|
Merged in 1.0.1. Exactly the split I asked for, and it corrected two things I'd asserted in the #166 review. Where I was wrongThe artifact conflict I predicted doesn't exist. I told you And the belief underneath it was wrong too.
Windows verification — the half you couldn't runBoth branches, on 1.0.1 with everything else merged: And the separator heuristic across the cases that matter:
The UNC row is the one I went looking for, since
The decision you asked forYou offered a fourth PR — regenerate the artifacts, or a CI check. I took both, in 1.0.1, so you don't need to:
I did not delete the directories, which was your other option. Nothing wmux ships reads them, but you read them out of a checkout and you're unlikely to be the only one — removing a path external consumers depend on isn't something to do silently in a patch release. If you'd still prefer deletion, say so and I'll do it with a deprecation notice in a minor. One small thing I noticed
NextThat leaves PR 3, the bridge itself. Whenever you're ready — the two flags from my #166 review still stand (the Thanks for splitting it. Both halves shipped inside a day. |
Claude Code increasingly runs in a Linux devcontainer while wmux runs on Windows. Nothing in that container can open \\.\pipe\wmux, so the CLI, the hooks and the shell integration all failed silently: no cwd, no git branch, and a sidebar stuck on "Running" forever. The bridge from issue #78 is most of the answer already — it just could not reach the pipe from where it needed to run. Three pieces close the gap: * connectTransport() learns npiperelay.exe, so `wmux bridge` can run INSIDE WSL2 and still reach the Windows pipe over interop. That placement is the security property: 0.0.0.0 there is the WSL2 namespace, reachable from containers on the host and not from the LAN, with no firewall rule. AF_VSOCK, a Windows-side listener and cross-boundary Unix sockets were tried first and are documented as rejected in the code. * The bridge keeps relays warm and tears down half-close-aware. Spawning npiperelay measures ~7s on an AV-scanned corporate host, and destroying both sides on either 'close' killed the frame of any client that writes and hangs up — which is every Claude Code hook. Deadlines gain a floor on the slow transports for the same reason; the 5s default sat below the round-trip, so calls that had already succeeded were reported as timeouts. * `wmux raw-v1` gives the bash integration a way to send its V1 lines through the CLI's transport instead of a temp file it cannot write to, and wmux-hook.js gets the same TCP branch. report_startup_command is new and rides the same path: a shell declares how to bring its own surface back, stored per surface, so a restored pane re-enters its container instead of coming up as a bare WSL prompt. docs/DEVCONTAINER.md is the standalone setup — one binary, one command, two environment variables, no vendor tooling. Two things ride along that were separate commits in #166, because on current master they have nothing to stand on their own for. The npiperelay lookup splits PATH on path.delimiter and falls back to process.env.Path — the Windows spelling — which only matters for the finder this commit introduces; #168 landed the rest of that change and deliberately left this half behind. And resources/shell-integration/wmux-bash-integration.sh is updated in step with its source, which #169 plus the check in 42e3cca now require: the packaged copy was resynced from a pre-bridge src/, so it has the `wmux` shim but not the WMUX_REMOTE branch, and leaving it that way fails npm test. Co-Authored-By: Claude <noreply@anthropic.com>
First of the three splits of #166, in the order you asked for.
Three commits,
src/cli/wmux.tsandtests/only. Noresources/artifact is touched — see the note at the bottom, that turned out to be its own story.What is here
wmux config pathasks the instance. It built the path from the caller's own home directory, which only holds when the CLI runs on the same Windows box as wmux. From WSL or a container over the bridge it printed/home/vscode\.wmux\config.toml— not the file wmux reads, and not a well-formed path on either OS.loadUserConfig()already records the real path andconfig.getreturns it, so ask; the local guess stays as the no-instance answer.Both no-instance fallbacks are spelled for the OS in hand.
config path's remaining guess andlocales path, which has no instance-side answer to ask for at all, both hardcoded backslashes. The separator now comes from the home directory that was found rather than fromprocess.platform, because the interesting case is exactly where those two disagree: a POSIX$HOMEgetspath.posix.join, everything else getspath.join.$HOMEis now preferred overUSERPROFILE— under WSL both are set andUSERPROFILEis the Windows one arriving over interop.locales pathcan still name the wrong directory across the WSL boundary. Fixing that properly needs alocales.getreporting the loaded directory the wayconfig.getreportspath; out of scope here.The bash-path helper and its test.
bashPathCandidates()offered/c,/mnt/c,/cygdrive/cin a fixed order; in a Linux checkout the bash on PATH mounts a Windows drive at/mnt/cif it sees one at all, so leading with/cput the fallback branch in charge. The order is now conditional onprocess.platform, the set is unchanged, and the probe still decides.orchestration-status-vocab.test.tsgains abashExists(hookPath)term in its skip condition —hasBash()only says a bash exists, not that it can see the hook script.bash-path.test.tsis in the same commit and not a separate one because it cannot survive the helper change alone: it pinned the Git-Bash-first order literally, and four assertions go red on Linux and macOS the moment the order becomes conditional. Splitting them would leave a commit that failsnpm testfor anyone not on Windows.What is not here, and why
You asked for this "minus the npiperelay-specific parts if they're separable". They are not:
findNpiperelay()does not exist on master —git grep npiperelay master -- src/cli/wmux.tsis empty. It arrives with the bridge, so itspath.delimiter/Path-vs-PATHfix has to ride with PR 3. Flagging rather than silently omitting.The
resources/cli/wmux.jsconflict you expectedIt is not there. Nothing in
8122b69..mastertouchessrc/cli/orresources/cli/— #153's timeout work (18940c0) is below the PR base, and #158 (1dd0d9e) is inclaude-context.tsand friends. So this PR had no artifact conflict to resolve.But while checking that, something else turned up, and it changes one test in this PR:
resources/cli/wmux.jsis 1519 diff lines behind a build of master's ownsrc/(wmux-hook.jsis 64). It is missingtimeoutMessageandbrowserDeadlinefrom #153,subcommandErrorfrom #156, andrename-surfacefrom #104. Nothing shipped is broken by this —package.json'sbin,electron-builder.json'sextraResourcesand step 7 of the release process all usedist/cli/*.js, so releases and installs get a fresh build. The stale copies are read by dev-mode Claude hooks (claude-context.ts:394) and by anything consuming the CLI out of a repo checkout.cli-config-path.test.tsas originally written spawnedresources/cli/wmux.js, on the stated grounds that it is "the file a released wmux actually runs" — which is the belief above, and it is wrong. It now spawnsdist/cli/wmux.jsand compiles it inbeforeAll(~3s;dist/is gitignored andnpm testruns ahead ofnpm run build:main). Building unconditionally is what makes the spawned CLI the working tree's rather than a leftover.I have deliberately not regenerated the artifact here. Doing so would put 1519 lines of unrelated catch-up into a three-file fix and bury the actual change. Same class of bug as the shell-integration drift in PR 2 — happy to send a fourth PR that regenerates both
resources/cli/*.js, or a CI check thatresources/matches a fresh build, whichever you prefer. Tell me which and I will open it.Verification
npm run typecheckclean (both tsconfigs).masterworktree with the samenode_modules: 6 failed, 988 passed, 2 skipped. The failing set is identical one-for-one — the Windows-path and live-Win32-probe cases (shell-context-menu×3,pty-ledger×2,pty-manager > resolveSpawnCwd). This PR adds 5 passing tests and takes nothing green to red.bashPathCandidates()still has a Windows branch a Linux run cannot exercise, andwmux config path/wmux locales pathwant a check from a native Windows shell — that half is yours.🤖 Generated with Claude Code