From c4516f642c911e343515578f97fc14683bc88246 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Mon, 18 May 2026 21:00:33 -0700 Subject: [PATCH] fix(windows): .exe glob in .gitignore + .exe extension resolution in find-browse (#1554) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bun build --compile on Windows appends .exe to the output filename, producing browse.exe instead of browse. find-browse's existsSync probe only checked the bare path and returned null on Windows even when the binary was correctly built. .gitignore similarly only excluded the bare bin/gstack-global-discover path, leaving the .exe variant tracked. This commit: - .gitignore: changes `bin/gstack-global-discover` → `bin/gstack-global-discover*` so the Windows .exe variant is ignored - browse/src/find-browse.ts: adds isExecutable + findExecutable helpers that fall back to .exe/.cmd/.bat probing on Windows, mirroring the same helper already in make-pdf/src/browseClient.ts and pdftotext.ts Contributed by @Mike-E-Log via #1554. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 2 +- browse/src/find-browse.ts | 37 ++++++++++++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 9e413bc56..9fde8011f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ dist/ browse/dist/ design/dist/ make-pdf/dist/ -bin/gstack-global-discover +bin/gstack-global-discover* .gstack/ .claude/skills/ .claude/scheduled_tasks.lock diff --git a/browse/src/find-browse.ts b/browse/src/find-browse.ts index 44138257c..9a8ed157a 100644 --- a/browse/src/find-browse.ts +++ b/browse/src/find-browse.ts @@ -5,7 +5,7 @@ * Outputs the absolute path to the browse binary on stdout, or exits 1 if not found. */ -import { existsSync } from 'fs'; +import { accessSync, constants } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; @@ -24,6 +24,35 @@ function getGitRoot(): string | null { } } +// Probe a path for executability. accessSync(X_OK) checks the executable +// bit on Linux/macOS and degrades to an existence check on Windows (no +// true execute bit). Mirrors make-pdf/src/browseClient.ts:159 / +// make-pdf/src/pdftotext.ts:117. +function isExecutable(p: string): boolean { + try { + accessSync(p, constants.X_OK); + return true; + } catch { + return false; + } +} + +// Resolve a bare binary path to the actual file on disk. On Windows, `bun +// build --compile` appends `.exe` to the output filename, so `browse` on +// disk is actually `browse.exe`. After a bare-path probe, try the Windows +// extensions. Linux/macOS behavior is unchanged. Mirrors the helper in +// make-pdf/src/browseClient.ts:89 and make-pdf/src/pdftotext.ts:52. +function findExecutable(base: string): string | null { + if (isExecutable(base)) return base; + if (process.platform === 'win32') { + for (const ext of ['.exe', '.cmd', '.bat']) { + const withExt = base + ext; + if (isExecutable(withExt)) return withExt; + } + } + return null; +} + export function locateBinary(): string | null { const root = getGitRoot(); const home = homedir(); @@ -33,14 +62,16 @@ export function locateBinary(): string | null { if (root) { for (const m of markers) { const local = join(root, m, 'skills', 'gstack', 'browse', 'dist', 'browse'); - if (existsSync(local)) return local; + const found = findExecutable(local); + if (found) return found; } } // Global fallback for (const m of markers) { const global = join(home, m, 'skills', 'gstack', 'browse', 'dist', 'browse'); - if (existsSync(global)) return global; + const found = findExecutable(global); + if (found) return found; } return null;