diff options
| author | Aman Kumar <145339995+amankumarsingh77@users.noreply.github.com> | 2026-07-12 22:03:43 +0530 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-12 22:03:43 +0530 |
| commit | cf92c10eda651cdad882aa050439c80d2f40b4b1 (patch) | |
| tree | 789e211f90c8548bac7b149ddfecc4312a6ba037 /src | |
| parent | f6bab02b6b93c3804ec83b853e333fba417a0988 (diff) | |
fix: make browser daemon setup work on Windows (#7) (#12)
The Unix socket daemon introduced in v0.6.0 made several POSIX-only
assumptions that break setup on Windows:
- protocol.ts hardcoded a Unix socket path (/tmp/pi-browser-daemon.sock).
Node IPC on Windows requires a named pipe (\\.\pipe\<name>); a /tmp
path is not a valid net listen/connect target there.
- spawn.ts spawned the bare tsx binary / `npx` and probed the non-Windows
.bin/tsx shim. On Windows npm binaries are .cmd shims that must run via a
shell, and spawning them directly fails with EINVAL / ENOENT.
- isDaemonRunning() pre-checked the socket with fs.access(), which always
fails for a named pipe and would report a live daemon as dead.
- stale-socket unlink() would throw on a named pipe path.
Changes:
- DAEMON_SOCKET_PATH is now platform-aware: named pipe on win32, Unix
socket elsewhere.
- spawnDaemon() resolves tsx.cmd / npx.cmd on Windows, runs via shell with
per-token quoting (paths with spaces) and windowsVerbatimArguments.
- isDaemonRunning() skips the fs.access pre-check on Windows and probes the
pipe directly.
- stale-socket cleanup is a no-op on Windows (named pipes self-clean).
- setup.ts daemon-failure message uses the actual DAEMON_SOCKET_PATH.
Closes #7
Diffstat (limited to 'src')
| -rw-r--r-- | src/daemon/protocol.ts | 14 | ||||
| -rw-r--r-- | src/daemon/spawn.ts | 59 | ||||
| -rw-r--r-- | src/setup.ts | 3 |
3 files changed, 60 insertions, 16 deletions
diff --git a/src/daemon/protocol.ts b/src/daemon/protocol.ts index 9d777bc..4fa4a2a 100644 --- a/src/daemon/protocol.ts +++ b/src/daemon/protocol.ts @@ -133,8 +133,18 @@ export const deserialize = (line: string): WireMessage | null => { // ── Constants ────────────────────────────────────────────────────────────────── -/** Filesystem path for the daemon's Unix domain socket. */ -export const DAEMON_SOCKET_PATH = "/tmp/pi-browser-daemon.sock"; +/** + * IPC endpoint for the daemon. + * + * On POSIX platforms this is a Unix domain socket path. On Windows, Node's IPC + * requires a named pipe of the form `\\.\pipe\<name>` — a Unix path like + * `/tmp/…` is not a valid `net` listen/connect target there and causes setup + * to fail. See https://nodejs.org/api/net.html#ipc-support. + */ +export const DAEMON_SOCKET_PATH = + process.platform === "win32" + ? "\\\\.\\pipe\\pi-browser-daemon" + : "/tmp/pi-browser-daemon.sock"; /** * How long the daemon stays alive with zero connected clients before exiting diff --git a/src/daemon/spawn.ts b/src/daemon/spawn.ts index 0a2670c..d051fcc 100644 --- a/src/daemon/spawn.ts +++ b/src/daemon/spawn.ts @@ -19,6 +19,19 @@ import { DAEMON_SOCKET_PATH, serialize, type WireControl } from "./protocol"; const moduleDir = dirname(fileURLToPath(import.meta.url)); +const isWindows = process.platform === "win32"; + +/** + * Remove a stale POSIX socket file. On Windows the daemon endpoint is a named + * pipe, which is not a filesystem entry and is torn down automatically when the + * owning process exits — so there is nothing to unlink, and calling unlink on a + * pipe path throws. This is a no-op on Windows. + */ +const cleanupStaleSocket = (): void => { + if (isWindows) return; + unlink(DAEMON_SOCKET_PATH).catch(() => {}); +}; + /** * Check whether the daemon is running by probing its Unix socket. * A file-existence check is unreliable — a crashed daemon leaves a stale socket. @@ -26,11 +39,15 @@ const moduleDir = dirname(fileURLToPath(import.meta.url)); * On failure, clean up the stale socket so ensureDaemon can spawn a fresh one. */ export const isDaemonRunning = async (timeoutMs = 2_000): Promise<boolean> => { - // If the socket file doesn't exist, the daemon definitely isn't running. - try { - await access(DAEMON_SOCKET_PATH); - } catch { - return false; + // On POSIX, a missing socket file means the daemon definitely isn't running, + // so we can short-circuit. Windows named pipes are not filesystem entries — + // access() always fails there — so skip the pre-check and probe directly. + if (!isWindows) { + try { + await access(DAEMON_SOCKET_PATH); + } catch { + return false; + } } return new Promise((resolve) => { @@ -45,7 +62,7 @@ export const isDaemonRunning = async (timeoutMs = 2_000): Promise<boolean> => { const timer = setTimeout(() => { sock.destroy(); // ponytail: stale socket cleanup; ENOENT = already gone, fine - unlink(DAEMON_SOCKET_PATH).catch(() => {}); + cleanupStaleSocket(); done(false); }, timeoutMs); @@ -70,7 +87,7 @@ export const isDaemonRunning = async (timeoutMs = 2_000): Promise<boolean> => { clearTimeout(timer); sock.destroy(); // ponytail: connection refused = daemon dead, clean up stale socket - unlink(DAEMON_SOCKET_PATH).catch(() => {}); + cleanupStaleSocket(); done(false); }); @@ -96,18 +113,34 @@ export const isDaemonRunning = async (timeoutMs = 2_000): Promise<boolean> => { export const spawnDaemon = (): ChildProcess | null => { // Resolve the daemon entry point and tsx binary. const daemonScript = join(moduleDir, "index.ts"); - const tsxBin = join(moduleDir, "..", "..", "node_modules", ".bin", "tsx"); - // Use tsx to run the daemon TypeScript source directly. - // The spawed process inherits the parent's env so module resolution works. + // On Windows, npm installs binaries as `.cmd` shims, not bare executables, + // and the npx fallback is `npx.cmd`. A `.cmd` file can only be launched + // through a shell (spawning it directly yields EINVAL), so set shell: true + // whenever we invoke one. + const tsxBinBase = join(moduleDir, "..", "..", "node_modules", ".bin", "tsx"); + const tsxBin = isWindows ? `${tsxBinBase}.cmd` : tsxBinBase; + const hasLocalTsx = existsSync(tsxBin); + + // Use the local tsx binary to run the daemon TypeScript source directly. + // The spawned process inherits the parent's env so module resolution works. // Fall back to `npx tsx` if the local binary isn't available. - const cmd = existsSync(tsxBin) ? tsxBin : "npx"; - const args = existsSync(tsxBin) ? [daemonScript] : ["tsx", daemonScript]; + const cmd = hasLocalTsx ? tsxBin : isWindows ? "npx.cmd" : "npx"; + const args = hasLocalTsx ? [daemonScript] : ["tsx", daemonScript]; + + // With shell: true, Node passes the command line to the shell verbatim + // without quoting, so any path containing spaces (e.g. C:\Users\Some Name) + // would be split into multiple tokens. Wrap each token in double quotes on + // Windows to keep space-containing paths intact. + const quote = (s: string): string => (isWindows ? `"${s}"` : s); - const child = spawn(cmd, args, { + const child = spawn(quote(cmd), args.map(quote), { detached: true, stdio: "ignore", env: { ...process.env }, + // `.cmd` shims (local tsx or npx.cmd on Windows) must run via a shell. + shell: isWindows, + windowsVerbatimArguments: isWindows, }); child.unref(); diff --git a/src/setup.ts b/src/setup.ts index 561a343..1ff7c36 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -8,6 +8,7 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { execSync } from "node:child_process"; import type { BrowserClient } from "./client"; import { ensureDaemon } from "./daemon/spawn"; +import { DAEMON_SOCKET_PATH } from "./daemon/protocol"; // ── Public: register the /browser-setup slash command ────────────────────── @@ -41,7 +42,7 @@ export async function performSetup(client: BrowserClient): Promise<SetupResult> // Step 2: Start the browser daemon (spawns if not running, silently reuses if alive) const daemonReady = await ensureDaemon(); if (!daemonReady) { - return { success: false, error: "Could not start the browser daemon. Check /tmp/pi-browser-daemon.sock." }; + return { success: false, error: `Could not start the browser daemon. Check ${DAEMON_SOCKET_PATH}.` }; } // Step 3: Connect to Chrome DevTools |
