Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/windows-bunfs-daemon-argv.md
Original file line number Diff line number Diff line change
@@ -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.
21 changes: 21 additions & 0 deletions src/session-broker/brokerLauncher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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\<name>.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",
Expand Down
22 changes: 19 additions & 3 deletions src/session-broker/brokerLauncher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -246,10 +259,13 @@ export function resolveDaemonLaunchCommand(
const entrypoint = argv[1];

// Bun-compiled single-file executables report argv as
// ["bun", "/$bunfs/root/<name>", ...userArgs]
// ["bun", "/$bunfs/root/<name>", ...userArgs] (Unix)
// ["bun", "B:/~BUN/root/<name>.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"],
Expand Down