From e307f38c29d36e1f69d53875ad93261e615811f8 Mon Sep 17 00:00:00 2001 From: Wolf Date: Sun, 16 Aug 2026 03:31:08 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20Windows=20support=20=E2=80=94=20Node.js?= =?UTF-8?q?=20CLI=20fallback=20for=20Smart=20App=20Control?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `bun build --compile` emits an executable with no Authenticode signature, and Windows Smart App Control in enforcement mode refuses to load unsigned binaries. Every locally compiled browse.exe is unique to the machine that built it, so it can never accumulate the file reputation SAC accepts either. The result is that browse.exe cannot launch at all: PS> browse.exe status Program 'browse.exe' failed to run: An Application Control policy has blocked this file Every $B command dies, taking /browse, /qa, /design-review, /benchmark and /canary with it. SAC has no per-file allowlist and cannot be re-enabled once disabled without a clean Windows reinstall, so there is no user-side workaround. Measured on the affected machine: bun.exe Valid CN=Codeblog CORP allowed node.exe Valid CN=OpenJS Foundation allowed browse.exe NotSigned - BLOCKED bun and node both run fine. Only the compiled artifact is the problem. The server half already exploits that: build-node-server.sh plus the IS_WINDOWS branch in src/cli.ts spawns `node server-node.mjs` (added in #255 for the Playwright/Chromium issue). So Windows was already "signed Node runs the server" while an unsigned binary was still required to start it. This adds the missing other half. - browse/scripts/build-node-cli.sh mirrors build-node-server.sh: bundles src/cli.ts to dist/cli-node.mjs with the same externals, rewrites the Bun-ism import.meta.dir, and injects bun-polyfill.cjs plus a Bun.stdin shim. The polyfill already covers Bun.spawn, Bun.spawnSync and Bun.sleep, which is everything the CLI's import graph uses except Bun.stdin (reached only by `chain` when reading stdin). - On Windows it also writes dist/browse and dist/browse.cmd launchers. dist/browse keeps the SETUP block in browse/SKILL.md working unchanged, since it resolves $B to that path and gates on -x. The wrapper writes are Windows-gated because on macOS and Linux build.sh compiles the real binary to that same path. - Wired into scripts/build.sh next to the server bundle. browse/dist/ is gitignored, so build-node-cli.sh is the only new tracked file. `bun test` on this branch matches main exactly on Windows (5241 pass / 450 skip / 1435 fail / 12 errors, both). Co-Authored-By: Claude Opus 5 --- browse/scripts/build-node-cli.sh | 129 +++++++++++++++++++++++++++++++ scripts/build.sh | 1 + 2 files changed, 130 insertions(+) create mode 100644 browse/scripts/build-node-cli.sh diff --git a/browse/scripts/build-node-cli.sh b/browse/scripts/build-node-cli.sh new file mode 100644 index 0000000000..67c954ae41 --- /dev/null +++ b/browse/scripts/build-node-cli.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# Build a Node.js-compatible CLI bundle for Windows. +# +# `bun build --compile` emits an executable with no Authenticode signature, and +# Windows Smart App Control in enforcement mode refuses to load unsigned +# binaries. Every locally compiled browse.exe is unique to the machine that +# built it, so it can never accumulate the reputation SAC accepts either. The +# result is that browse.exe cannot launch at all: every $B command dies, taking +# /browse, /qa, /design-review, /benchmark and /canary with it. SAC has no +# per-file allowlist and cannot be re-enabled once disabled without a clean +# Windows reinstall. +# +# bun.exe and node.exe are both signed (Codeblog CORP and OpenJS Foundation), +# so they run fine. The server half already exploits that: build-node-server.sh +# plus the IS_WINDOWS branch in src/cli.ts spawns `node server-node.mjs`. This +# script builds the missing other half, so no unsigned binary has to launch. + +set -e + +GSTACK_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +SRC_DIR="$GSTACK_DIR/browse/src" +DIST_DIR="$GSTACK_DIR/browse/dist" + +echo "Building Node-compatible CLI bundle..." + +# Step 1: Transpile cli.ts to a single .mjs bundle (externalize runtime deps). +# Same externals as build-node-server.sh. +bun build "$SRC_DIR/cli.ts" \ + --target=node \ + --outfile "$DIST_DIR/cli-node.mjs" \ + --external playwright \ + --external playwright-core \ + --external diff \ + --external "bun:sqlite" \ + --external "@ngrok/ngrok" + +# Step 2: Post-process +# Replace import.meta.dir with a resolvable reference +perl -pi -e 's/import\.meta\.dir/__browseNodeSrcDir/g' "$DIST_DIR/cli-node.mjs" + +# Step 3: Create the final file with polyfill header injected after the first line. +# bun-polyfill.cjs already covers Bun.spawn, Bun.spawnSync and Bun.sleep, which is +# everything the CLI's import graph uses except Bun.stdin (reached only by `chain` +# when it reads from stdin), so shim that one here. +{ + head -1 "$DIST_DIR/cli-node.mjs" + echo '// ── Windows Node.js compatibility (auto-generated) ──' + echo 'import { fileURLToPath as _ftp } from "node:url";' + echo 'import { dirname as _dn } from "node:path";' + echo 'import { createRequire as _cr } from "node:module";' + echo 'const __browseNodeSrcDir = _dn(_dn(_ftp(import.meta.url))) + "/src";' + echo '{' + echo ' const _r = _cr(import.meta.url);' + echo ' _r("./bun-polyfill.cjs");' + echo ' if (globalThis.Bun && !globalThis.Bun.stdin) {' + echo ' globalThis.Bun.stdin = {' + echo ' text() {' + echo ' return new Promise((resolve, reject) => {' + echo ' let data = "";' + echo ' process.stdin.setEncoding("utf8");' + echo ' process.stdin.on("data", (chunk) => { data += chunk; });' + echo ' process.stdin.on("end", () => resolve(data));' + echo ' process.stdin.on("error", reject);' + echo ' });' + echo ' },' + echo ' };' + echo ' }' + echo '}' + echo '// ── end compatibility ──' + tail -n +2 "$DIST_DIR/cli-node.mjs" +} > "$DIST_DIR/cli-node.tmp.mjs" + +mv "$DIST_DIR/cli-node.tmp.mjs" "$DIST_DIR/cli-node.mjs" + +# Step 4: Copy polyfill to dist/ +cp "$SRC_DIR/bun-polyfill.cjs" "$DIST_DIR/bun-polyfill.cjs" + +echo "Node CLI bundle ready: $DIST_DIR/cli-node.mjs" + +# Step 5: Launcher wrappers, Windows only. +# +# On macOS and Linux, build.sh compiles the real binary to browse/dist/browse, +# so writing a wrapper there would clobber it. On Windows that same step emits +# browse.exe instead, leaving the extensionless path free — which is what keeps +# the SETUP block in browse/SKILL.md working unchanged, since it resolves $B to +# browse/dist/browse and gates on -x. +case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*|Windows_NT) + cat > "$DIST_DIR/browse" <<'WRAPPER' +#!/usr/bin/env bash +# Node launcher for gstack browse (Windows / Smart App Control). +# +# dist/browse.exe is unsigned, so Smart App Control blocks it. node.exe is +# signed, so running the CLI as a Node bundle works. +# +# Rebuild with: bash browse/scripts/build-node-cli.sh +set -e + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CLI="$DIR/cli-node.mjs" + +if [ ! -f "$CLI" ]; then + echo "cli-node.mjs missing. Run: bash browse/scripts/build-node-cli.sh" >&2 + exit 1 +fi + +# node.exe is a native Windows binary and cannot read MSYS-style /c/... paths. +if command -v cygpath >/dev/null 2>&1; then + CLI="$(cygpath -w "$CLI")" +fi + +exec node "$CLI" "$@" +WRAPPER + + cat > "$DIST_DIR/browse.cmd" <<'WRAPPER' +@echo off +REM Node launcher for gstack browse (Windows / Smart App Control). +REM dist\browse.exe is unsigned and blocked by Smart App Control; node.exe is signed. +REM Rebuild with: bash browse/scripts/build-node-cli.sh +node "%~dp0cli-node.mjs" %* +WRAPPER + + chmod +x "$DIST_DIR/browse" + echo "Launcher wrappers written: $DIST_DIR/browse, $DIST_DIR/browse.cmd" + ;; + *) + echo "Skipping launcher wrappers (non-Windows: dist/browse is the compiled binary)." + ;; +esac diff --git a/scripts/build.sh b/scripts/build.sh index 67acf6dc06..037de932e2 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -30,6 +30,7 @@ esac "$BUN_CMD" build --compile make-pdf/src/cli.ts --outfile make-pdf/dist/pdf "$BUN_CMD" build --compile bin/gstack-global-discover.ts --outfile bin/gstack-global-discover bash browse/scripts/build-node-server.sh +bash browse/scripts/build-node-cli.sh bash scripts/write-version-files.sh browse/dist/.version design/dist/.version 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