688b64f3
fix: mux HOST finds a ~/.local/bin muxd on the remote
a73x 2026-08-18 18:43
Commit message
.gitignore
| Old | New | ||
|---|---|---|---|
| @@ -9,3 +9,4 @@ dist/ | |||
| 9 | 9 | ||
| 10 | # Cross-version gate artifacts: two versions built static, per version. | 10 | # Cross-version gate artifacts: two versions built static, per version. |
| 11 | .xversion/ | 11 | .xversion/ |
| 12 | .release/ | ||
README.md
| Old | New | ||
|---|---|---|---|
| @@ -53,7 +53,9 @@ mux HOST # attach; Ctrl-\ detaches, running it again reattaches | |||
| 53 | `muxd start` spawns the daemon detached and waits until it answers, so it | 53 | `muxd start` spawns the daemon detached and waits until it answers, so it |
| 54 | is safe to run every time — if one is already up it says so and exits 0. | 54 | is safe to run every time — if one is already up it says so and exits 0. |
| 55 | 55 | ||
| 56 | `mux HOST` runs `ssh HOST muxd endpoint` — ssh config (aliases, ports, | 56 | `mux HOST` runs `muxd endpoint` on HOST over ssh (finding a muxd in |
| 57 | `~/.local/bin` even though ssh's non-login shell leaves it off PATH) — | ||
| 58 | ssh config (aliases, ports, | ||
| 57 | ProxyJump) all keeps working, since `mux` never parses HOST. That ssh | 59 | ProxyJump) all keeps working, since `mux` never parses HOST. That ssh |
| 58 | fetches the daemon's QUIC port and key once and the session moves onto | 60 | fetches the daemon's QUIC port and key once and the session moves onto |
| 59 | QUIC, leaving no ssh process behind; the coordinates are cached under | 61 | QUIC, leaving no ssh process behind; the coordinates are cached under |
src/handoff.zig
| Old | New | ||
|---|---|---|---|
| @@ -187,7 +187,15 @@ pub const Recipe = struct { | |||
| 187 | /// commands. Building it here (rather than in client.zig) is what keeps | 187 | /// commands. Building it here (rather than in client.zig) is what keeps |
| 188 | /// the client free of XDG and of allocating a command line. | 188 | /// the client free of XDG and of allocating a command line. |
| 189 | pub fn recipeFor(alloc: std.mem.Allocator, host: []const u8) !Recipe { | 189 | pub fn recipeFor(alloc: std.mem.Allocator, host: []const u8) !Recipe { |
| 190 | const cmd = try std.fmt.allocPrint(alloc, "ssh {s} muxd endpoint", .{host}); | 190 | // The PATH prefix, single-quoted so the REMOTE shell expands it: sshd |
| 191 | // runs this through a non-login, non-interactive shell that never | ||
| 192 | // sources the profile putting ~/.local/bin (make install's target) on | ||
| 193 | // PATH — without it a muxd the user can run by hand is invisible here. | ||
| 194 | const cmd = try std.fmt.allocPrint( | ||
| 195 | alloc, | ||
| 196 | "ssh {s} 'PATH=\"$HOME/.local/bin:$PATH\" muxd endpoint'", | ||
| 197 | .{host}, | ||
| 198 | ); | ||
| 191 | errdefer alloc.free(cmd); | 199 | errdefer alloc.free(cmd); |
| 192 | return .{ .ssh_cmd = cmd, .cache_path = xdg.hostCachePath(alloc, host) catch null }; | 200 | return .{ .ssh_cmd = cmd, .cache_path = xdg.hostCachePath(alloc, host) catch null }; |
| 193 | } | 201 | } |
| @@ -390,6 +398,15 @@ test "announce: every shape of junk is a named error" { | |||
| 390 | } | 398 | } |
| 391 | } | 399 | } |
| 392 | 400 | ||
| 401 | test "recipeFor: the remote command carries ~/.local/bin itself — sshd's non-login shell never sources the profile that would" { | ||
| 402 | const r = try recipeFor(std.testing.allocator, "user@box"); | ||
| 403 | defer r.deinit(std.testing.allocator); | ||
| 404 | try std.testing.expectEqualStrings( | ||
| 405 | "ssh user@box 'PATH=\"$HOME/.local/bin:$PATH\" muxd endpoint'", | ||
| 406 | r.ssh_cmd, | ||
| 407 | ); | ||
| 408 | } | ||
| 409 | |||
| 393 | test "dialHost: the LAST @ wins, which is where ssh splits" { | 410 | test "dialHost: the LAST @ wins, which is where ssh splits" { |
| 394 | try std.testing.expectEqualStrings("box", dialHost("ubuntu@box")); | 411 | try std.testing.expectEqualStrings("box", dialHost("ubuntu@box")); |
| 395 | try std.testing.expectEqualStrings("box", dialHost("box")); | 412 | try std.testing.expectEqualStrings("box", dialHost("box")); |