From 898b4af317d64e175e2e91775b7e2d5c77bf8857 Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Fri, 3 Jul 2026 13:24:50 -0400 Subject: [PATCH] fix(session-broker): detect Windows bunfs paths when auto-launching the daemon Bun single-file executables on Windows report argv[1] as a virtual B:\~BUN\root\.exe path. That misses the Unix /$bunfs/ check but matches the script-entrypoint pattern, so the daemon relaunched the binary with the virtual path as a bogus first argument and never came up, leaving 'hunk session' commands with no live sessions. Fixes #502 Co-Authored-By: Claude Fable 5 --- .changeset/windows-bunfs-daemon-argv.md | 5 +++++ src/session-broker/brokerLauncher.test.ts | 21 +++++++++++++++++++++ src/session-broker/brokerLauncher.ts | 22 +++++++++++++++++++--- 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 .changeset/windows-bunfs-daemon-argv.md diff --git a/.changeset/windows-bunfs-daemon-argv.md b/.changeset/windows-bunfs-daemon-argv.md new file mode 100644 index 00000000..73aeacbe --- /dev/null +++ b/.changeset/windows-bunfs-daemon-argv.md @@ -0,0 +1,5 @@ +--- +"hunkdiff": patch +--- + +Fix session daemon auto-launch on Windows: the compiled binary's virtual `B:\~BUN\...` entrypoint was mistaken for a script path and passed to the relaunched daemon as a bogus argument, so `hunk session` commands never found a live session. diff --git a/src/session-broker/brokerLauncher.test.ts b/src/session-broker/brokerLauncher.test.ts index 997d88f6..937b7bab 100644 --- a/src/session-broker/brokerLauncher.test.ts +++ b/src/session-broker/brokerLauncher.test.ts @@ -71,6 +71,27 @@ describe("session daemon launcher", () => { }); }); + test("uses execPath for Windows Bun-compiled binaries mounted on the virtual B: drive", () => { + // On Windows, Bun single-file executables report the bundle as B:\~BUN\root\.exe; + // treating it as a script entrypoint would pass the virtual path to the relaunched + // binary as a bogus argument (#502). Both separators appear depending on the shell. + const realBinary = + "C:\\Users\\dev\\AppData\\Roaming\\npm\\node_modules\\hunkdiff\\node_modules\\hunkdiff-windows-x64\\bin\\hunk.exe"; + + expect( + resolveDaemonLaunchCommand(["bun", "B:/~BUN/root/hunk.exe", "diff"], realBinary), + ).toEqual({ + command: realBinary, + args: ["daemon", "serve"], + }); + expect( + resolveDaemonLaunchCommand(["bun", "B:\\~BUN\\root\\hunk.exe", "diff"], realBinary), + ).toEqual({ + command: realBinary, + args: ["daemon", "serve"], + }); + }); + test("detects whether some process is already listening on the daemon port", async () => { const listener = Bun.serve({ hostname: "127.0.0.1", diff --git a/src/session-broker/brokerLauncher.ts b/src/session-broker/brokerLauncher.ts index 588583cc..4629b76c 100644 --- a/src/session-broker/brokerLauncher.ts +++ b/src/session-broker/brokerLauncher.ts @@ -69,6 +69,19 @@ export interface EnsureSessionBrokerAvailableOptions { /** Detect Bun's virtual filesystem prefix used inside compiled single-file executables. */ const BUNFS_PREFIX = "/$bunfs/"; +/** Bun's Windows equivalent mounts the compiled bundle on a virtual B: drive. */ +const BUNFS_WINDOWS_PREFIX = "b:/~bun/"; + +/** True when argv[1] is a Bun single-file-executable virtual path on any platform. */ +function isBunfsEntrypoint(entrypoint: string) { + if (entrypoint.startsWith(BUNFS_PREFIX)) { + return true; + } + + // Windows reports the virtual path with either separator depending on the shell, so + // normalize before comparing (e.g. "B:\\~BUN\\root\\hunk.exe" or "B:/~BUN/root/hunk.exe"). + return entrypoint.replaceAll("\\", "/").toLowerCase().startsWith(BUNFS_WINDOWS_PREFIX); +} function safeRuntimeToken(value: string) { return value.replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "") || "default"; @@ -246,10 +259,13 @@ export function resolveDaemonLaunchCommand( const entrypoint = argv[1]; // Bun-compiled single-file executables report argv as - // ["bun", "/$bunfs/root/", ...userArgs] + // ["bun", "/$bunfs/root/", ...userArgs] (Unix) + // ["bun", "B:/~BUN/root/.exe", ...userArgs] (Windows) // with execPath pointing to the real binary on disk. - // Detect the virtual $bunfs path and use execPath directly. - if (entrypoint && entrypoint.startsWith(BUNFS_PREFIX)) { + // Detect the virtual path and use execPath directly; letting the Windows form fall through + // to the script-entrypoint branch would relaunch the binary with the virtual path as a bogus + // first argument and the daemon would never start (#502). + if (entrypoint && isBunfsEntrypoint(entrypoint)) { return { command: execPath, args: ["daemon", "serve"],