From 7a4b401520c53fb201cf3b3844f6a0a456b94452 Mon Sep 17 00:00:00 2001 From: Bikash Joshi Date: Mon, 20 Apr 2026 01:21:43 -0700 Subject: [PATCH] fix(make-pdf): resolveBrowseBin() now works on Windows without BROWSE_BIN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The binary resolution chain in `make-pdf/src/browseClient.ts` hardcoded the Unix binary name `browse` and the Unix lookup tool `which`. On Windows the compiled binary is `browse.exe`, so every fallback (sibling paths, global install at `~/.claude/skills/gstack/browse/dist/browse`, PATH lookup) missed, and users hit a cryptic "browse binary not found" error unless they knew to set `BROWSE_BIN` manually. Changes: - Branch binary name on `process.platform === "win32"` → append `.exe`. - Use `where` on Windows, `which` on Unix; parse first line of `where` output (can be multi-line across PATHEXT hits). - `isExecutable()` checks `F_OK` on Windows because NTFS has no execute bit and `X_OK` is unreliable (Node delegates to `AccessCheck` which can false-negative on .exe files depending on ACLs). - Error message now shows the actual binary name being searched and suggests `setx BROWSE_BIN ...` on Windows instead of `export`. Verified on Windows 11 + Git Bash: - Before patch: `pdf.exe generate ...` fails with "browse binary not found" unless `BROWSE_BIN` is set explicitly via `setx`. - After patch: succeeds with empty `BROWSE_BIN`, finds `~/.claude/skills/gstack/browse/dist/browse.exe` via the global path branch. Rendered a 51KB smoke-test PDF end-to-end. No behavior change on macOS/Linux (suffix is empty string, lookup tool stays `which`, permission check stays `X_OK`). --- make-pdf/src/browseClient.ts | 37 +++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/make-pdf/src/browseClient.ts b/make-pdf/src/browseClient.ts index 9284590731..75bbcc79d0 100644 --- a/make-pdf/src/browseClient.ts +++ b/make-pdf/src/browseClient.ts @@ -62,13 +62,17 @@ export function resolveBrowseBin(): string { const envOverride = process.env.BROWSE_BIN; if (envOverride && isExecutable(envOverride)) return envOverride; + const isWin = process.platform === "win32"; + const exeSuffix = isWin ? ".exe" : ""; + const binName = `browse${exeSuffix}`; + // Sibling: look relative to this process's binary // (for when make-pdf and browse live next to each other in dist/) const selfDir = path.dirname(process.argv[0]); const siblingCandidates = [ - path.resolve(selfDir, "../browse/dist/browse"), - path.resolve(selfDir, "../../browse/dist/browse"), - path.resolve(selfDir, "../browse"), + path.resolve(selfDir, `../browse/dist/${binName}`), + path.resolve(selfDir, `../../browse/dist/${binName}`), + path.resolve(selfDir, `../${binName}`), ]; for (const candidate of siblingCandidates) { if (isExecutable(candidate)) return candidate; @@ -76,15 +80,18 @@ export function resolveBrowseBin(): string { // Global install const home = os.homedir(); - const globalPath = path.join(home, ".claude/skills/gstack/browse/dist/browse"); + const globalPath = path.join(home, ".claude/skills/gstack/browse/dist", binName); if (isExecutable(globalPath)) return globalPath; - // PATH lookup + // PATH lookup — Windows uses `where`, Unix uses `which` + const lookupCmd = isWin ? "where" : "which"; try { - const which = execFileSync("which", ["browse"], { encoding: "utf8" }).trim(); - if (which && isExecutable(which)) return which; + const out = execFileSync(lookupCmd, [binName], { encoding: "utf8" }).trim(); + // `where` can return multiple lines; take first + const first = out.split(/\r?\n/)[0]?.trim(); + if (first && isExecutable(first)) return first; } catch { - // `which` exited non-zero; fall through to error + // lookup exited non-zero; fall through to error } throw new BrowseClientError( @@ -94,24 +101,32 @@ export function resolveBrowseBin(): string { "browse binary not found.", "", "make-pdf needs browse (the gstack Chromium daemon) to render PDFs.", + `Platform: ${process.platform} (looking for "${binName}")`, "Tried:", ` - $BROWSE_BIN (${envOverride || "unset"})`, ` - sibling: ${siblingCandidates.join(", ")}`, ` - global: ${globalPath}`, - " - PATH: `browse`", + ` - PATH: \`${lookupCmd} ${binName}\``, "", "To fix: run gstack setup from the gstack repo:", " cd ~/.claude/skills/gstack && ./setup", "", "Or set BROWSE_BIN explicitly:", - " export BROWSE_BIN=/path/to/browse", + isWin + ? ' setx BROWSE_BIN "C:\\path\\to\\browse.exe"' + : " export BROWSE_BIN=/path/to/browse", ].join("\n"), ); } function isExecutable(p: string): boolean { try { - fs.accessSync(p, fs.constants.X_OK); + // Windows: NTFS has no execute bit. X_OK is unreliable — use F_OK + // (file exists) and let the OS decide at execFile time. + const mode = process.platform === "win32" + ? fs.constants.F_OK + : fs.constants.X_OK; + fs.accessSync(p, mode); return true; } catch { return false;