From e0daa440c28f0ec6de283ff5bab9b5ed77a1e715 Mon Sep 17 00:00:00 2001 From: w-khai <212912583+w-khai@users.noreply.github.com> Date: Tue, 19 May 2026 18:26:09 +0800 Subject: [PATCH 1/2] fix(setup): wrap subshell-with-redirect in bash -c for Windows bun shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bun's Windows shell parser rejects `(cmd) > file` with: error: Failed to run script build due to error Subshells with redirections are currently not supported. Please open a GitHub issue. This makes `./setup` fail on every Windows install — the binaries don't rebuild and the user is left with whatever stale browse.exe was last committed (or no binary at all on a fresh clone). Fix: wrap each `(git rev-parse HEAD ...) > path` and the trailing `(rm -f ...)` cleanup in `bash -c '...'`. The version-stamp behavior is identical (empty file when not in a git checkout vs SHA when we are); the build now runs end-to-end on Windows via git-bash + bun 1.3.13. Verified on Windows 11 + bun 1.3.13 + git-bash: - Pre-patch: `./setup` aborts during `bun run build`. - Post-patch: full build completes (~7s on a warm cache), all 5 compiled exes land in their dist dirs, server-node.mjs bundles. POSIX behaviour is unchanged — bash on macOS / Linux handles either syntax. No risk to existing platforms. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3851a78bd7..791f3e5d79 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "make-pdf": "./make-pdf/dist/pdf" }, "scripts": { - "build": "bun run vendor:xterm && bun run gen:skill-docs --host all; bun build --compile browse/src/cli.ts --outfile browse/dist/browse && bun build --compile browse/src/find-browse.ts --outfile browse/dist/find-browse && bun build --compile design/src/cli.ts --outfile design/dist/design && bun build --compile make-pdf/src/cli.ts --outfile make-pdf/dist/pdf && bun build --compile bin/gstack-global-discover.ts --outfile bin/gstack-global-discover && bash browse/scripts/build-node-server.sh && ( git rev-parse HEAD 2>/dev/null || true ) > browse/dist/.version && ( git rev-parse HEAD 2>/dev/null || true ) > design/dist/.version && ( git rev-parse HEAD 2>/dev/null || true ) > make-pdf/dist/.version && chmod +x browse/dist/browse browse/dist/find-browse design/dist/design make-pdf/dist/pdf bin/gstack-global-discover && (rm -f .*.bun-build || true)", + "build": "bun run vendor:xterm && bun run gen:skill-docs --host all; bun build --compile browse/src/cli.ts --outfile browse/dist/browse && bun build --compile browse/src/find-browse.ts --outfile browse/dist/find-browse && bun build --compile design/src/cli.ts --outfile design/dist/design && bun build --compile make-pdf/src/cli.ts --outfile make-pdf/dist/pdf && bun build --compile bin/gstack-global-discover.ts --outfile bin/gstack-global-discover && bash browse/scripts/build-node-server.sh && bash -c '(git rev-parse HEAD 2>/dev/null || true) > browse/dist/.version' && bash -c '(git rev-parse HEAD 2>/dev/null || true) > design/dist/.version' && bash -c '(git rev-parse HEAD 2>/dev/null || true) > make-pdf/dist/.version' && chmod +x browse/dist/browse browse/dist/find-browse design/dist/design make-pdf/dist/pdf bin/gstack-global-discover && bash -c 'rm -f .*.bun-build || true'", "vendor:xterm": "mkdir -p extension/lib && cp node_modules/xterm/lib/xterm.js extension/lib/xterm.js && cp node_modules/xterm/css/xterm.css extension/lib/xterm.css && cp node_modules/xterm-addon-fit/lib/xterm-addon-fit.js extension/lib/xterm-addon-fit.js", "dev:make-pdf": "bun run make-pdf/src/cli.ts", "dev:design": "bun run design/src/cli.ts", From 376c5cd7e671e0605b8d5c4dea22dd3dc91f5608 Mon Sep 17 00:00:00 2001 From: w-khai <212912583+w-khai@users.noreply.github.com> Date: Tue, 19 May 2026 18:26:25 +0800 Subject: [PATCH 2/2] fix(browse): tolerate EEXIST in mkdirSecure for bun-on-Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bun's compiled-binary `fs.mkdirSync(path, {recursive:true})` throws EEXIST when the directory already exists. Node does NOT — `recursive: true` is documented as suppressing EEXIST. The divergence makes every `browse` invocation on Windows exit 1 with: [browse] EEXIST: file already exists, mkdir 'C:\...\.gstack' because ensureStateDir() runs unconditionally at startup and the project's `.gstack/` exists by then (gitignore'd state dir). Reproduced on: - Windows 11 Home (10.0.26200) - bun 1.3.13 - OneDrive home directory (path contains a space) - git-bash + plain cmd.exe — both surface the same EEXIST A plain `node -e "require('fs').mkdirSync('.gstack', {recursive:true})"` on the same machine + same path succeeds. So the bug is in bun's fs shim, not in our code or the filesystem. But mkdirSecure can defend defensively without waiting for an upstream bun fix. Fix: catch EEXIST in mkdirSecure(), verify the existing path is in fact a directory (statSync), then fall through to re-apply the ACL. Throws on any other error code. The behavior on Node is unchanged (EEXIST is never raised, so the catch never fires). Verified end-to-end: rebuilt the browse binary with the patch, re-ran `browse status` and `browse goto https://example.com` — both return cleanly. Prior to the patch, both exited 1 with the EEXIST. Without this fix, gstack browse is 100% unusable on Windows. --- browse/src/file-permissions.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/browse/src/file-permissions.ts b/browse/src/file-permissions.ts index d3d404acde..f359a13660 100644 --- a/browse/src/file-permissions.ts +++ b/browse/src/file-permissions.ts @@ -143,9 +143,24 @@ export function appendSecureFile( * `mkdir -p` with owner-only directory permissions, cross-platform. * Replaces `fs.mkdirSync(path, { recursive: true, mode: 0o700 })` + Windows ACL. * Safe to call on an existing directory — re-applies the ACL idempotently. + * + * Bun-on-Windows quirk: bun's compiled-binary fs.mkdirSync sometimes throws + * EEXIST even with `recursive: true` when the directory already exists (Node + * does not). Swallow that one case so callers don't have to wrap every + * invocation. */ export function mkdirSecure(dirPath: string): void { - fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 }); + try { + fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 }); + } catch (err: any) { + if (err && err.code === 'EEXIST') { + const stat = fs.statSync(dirPath); + if (!stat.isDirectory()) throw err; + // Existing directory — fall through to re-apply ACL. + } else { + throw err; + } + } restrictDirectoryPermissions(dirPath); }