diff --git a/CHANGELOG.md b/CHANGELOG.md index cf89b49b2..0736f4d17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,49 @@ # Changelog +## [1.39.2.0] - 2026-05-15 + +## **`bun run build` now actually parses on Windows.** +## **The `.version` writes route through a bash script so no Bun-shell-hostile pattern remains in `package.json`.** + +The v1.39.1.0 fix swapped bash brace groups for subshells in the build script's `.version` redirects to "match the v1.38.0.0 invariant." But subshells-with-redirect (`( cmd ) > file`) are *also* rejected by Bun's bundled shell on Windows — different parser error from the brace-group form, same outcome. The compat test gated on the wrong property (subshell prefix present), so the regression green-lit. The third option — `cmd 2>/dev/null > file` with dual redirection on one command — is rejected too. No single-line inline form of "swallow stderr, swallow exit code, write stdout to a file" parses under Bun shell on Windows. + +The fix delegates `.version` writes to `scripts/write-versions.sh`, mirroring the existing precedent of `browse/scripts/build-node-server.sh`. The build script now calls `bash scripts/write-versions.sh` for all three files; the bash script handles git-missing cleanly, and `setup`'s safety net still backfills empty `.version` files at install time. + +### The numbers that matter + +Source: `bun test test/build-script-shell-compat.test.ts` — 4 passing tests covering all three Bun-shell-hostile patterns plus the delegation property. Verified end-to-end on Windows 11 + Git Bash + bun 1.3.13: `bun run build` exits 0; all three `.version` files contain the correct HEAD SHA. + +| Surface | Before | After | +|---|---|---| +| `( cmd ) > file` subshell-with-redirect in `scripts.build` | 3 occurrences (v1.39.1.0 form) | 0 | +| `{ cmd; } > file` brace-group-with-redirect | 0 (caught by existing test) | 0 | +| `cmd 2>X > Y` dual-redirect single command | 0 (no observed) | 0; new test prevents introduction | +| Compat-test assertions | 2 (no braces; subshell present) | 4 (no braces; no subshell-with-redirect; no dual-redirect; `.version` delegated to script) | +| `bun run build` on Windows + bun 1.3.13 | crashes with "Subshells with redirections are currently not supported" | exits 0 | + +### What this means for Windows users + +Fresh installs (`./setup`) and `/gstack-upgrade` from any older version both complete the build step cleanly. No manual workarounds, no `bun run build` interventions. The `bin/gstack-global-discover` binary, the three `.version` files, and the Node-compatible server bundle all materialize as expected. Run `/gstack-upgrade` once and you're caught up. + +### Itemized changes + +#### Fixed + +- **`package.json`** `scripts.build` — three `( git rev-parse HEAD 2>/dev/null || true ) > path/.version` subshells replaced with one `bash scripts/write-versions.sh` call. Bun shell on Windows rejected the subshell-with-redirect form with `error: Subshells with redirections are currently not supported`. The same parser also rejects bash brace groups (the v1.38.0.0 → v1.39.1.0 ping-pong) and dual-redirect single commands, so all single-line inline forms are unsafe. + +#### Added + +- **`scripts/write-versions.sh`** — bash script that writes `git rev-parse HEAD` to all three `.version` files. Mirrors the precedent of `browse/scripts/build-node-server.sh`. Empty file is acceptable when git is unavailable; `setup`'s safety net backfills. +- **`test/build-script-shell-compat.test.ts`** — strengthened from 2 to 4 assertions: + - `no bash brace groups in any npm script` — unchanged. + - `no subshell-with-redirection patterns in any npm script` — new; would have caught the v1.39.1.0 regression. + - `no command with both stderr-suppress and stdout-redirect on one line` — new; locks out the third Bun-shell-hostile form. + - `.version writes are delegated to a bash script (not inline)` — replaces the prior `must be a subshell` assertion. Gates on the right property (delegation), not the previously-wrong property (subshell prefix). + +#### For contributors + +- A complementary defense beyond the regex tests would be to execute `bun run build` (or a parse-only probe) under `windows-free-tests.yml`. Regex assertions catch only currently-known hostile patterns; an execution check would surface any future Bun shell limitation. Flagged as a follow-up in #1530, not included in this PR. + ## [1.39.1.0] - 2026-05-15 ## **Plan-mode reviews now enforce a blocking ExitPlanMode gate.** diff --git a/VERSION b/VERSION index 57fdbd724..939a56892 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.39.1.0 +1.39.2.0 diff --git a/package.json b/package.json index 601eb963c..c8d36437d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.39.1.0", + "version": "1.39.2.0", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", @@ -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 scripts/write-versions.sh && 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)", "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", diff --git a/scripts/write-versions.sh b/scripts/write-versions.sh new file mode 100755 index 000000000..c46e669e6 --- /dev/null +++ b/scripts/write-versions.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Write the current git HEAD SHA to dist/.version files for each compiled +# binary. An empty file is acceptable if git is unavailable; the setup script +# has a safety net that overwrites missing/empty .version files at install +# time (see setup, around the BROWSE_BIN build block). +# +# Why this is a separate script instead of inline in package.json's build +# script: Bun's bundled shell on Windows rejects subshells-with-redirection +# (`( cmd ) > file`), bash brace groups (`{ cmd; } > file`), and multiple +# redirections on one command (`cmd 2>/dev/null > file`). All three are +# needed to compose the previous inline form, so we delegate to bash which +# handles every variant. + +HEAD=$(git rev-parse HEAD 2>/dev/null || true) +printf '%s\n' "$HEAD" > browse/dist/.version +printf '%s\n' "$HEAD" > design/dist/.version +printf '%s\n' "$HEAD" > make-pdf/dist/.version diff --git a/test/build-script-shell-compat.test.ts b/test/build-script-shell-compat.test.ts index ee13fb709..16d8cd0dc 100644 --- a/test/build-script-shell-compat.test.ts +++ b/test/build-script-shell-compat.test.ts @@ -13,9 +13,14 @@ function stripSingleQuoted(s: string): string { return s.replace(/'[^']*'/g, "''"); } -describe('package.json build scripts — POSIX shell compat (D-1460)', () => { - // Bun's Windows shell parser doesn't grok bash brace groups `{ cmd; }`. - // Subshells `( cmd )` are POSIX-universal. This test prevents regression. +describe('package.json build scripts — Bun shell compat (D-1460)', () => { + // Bun's Windows shell parser rejects several patterns that are POSIX-valid: + // - bash brace groups: `{ cmd; }` + // - subshells with redirection: `( cmd ) > file` + // - multiple redirections on one command: `cmd 2>/dev/null > file` + // The safe approach is to keep these out of npm scripts entirely and + // delegate to bash scripts when needed. See scripts/write-versions.sh. + test('no bash brace groups in any npm script', () => { const offending: { script: string; pattern: string }[] = []; for (const [name, body] of Object.entries(PKG.scripts)) { @@ -28,13 +33,41 @@ describe('package.json build scripts — POSIX shell compat (D-1460)', () => { expect(offending).toEqual([]); }); - test('every `> path/.version` redirect is preceded by a subshell, not a brace group', () => { - // The original PR #1460 target: package.json line 12 had three of these. - const build = PKG.scripts.build ?? ''; - const versionRedirects = [...build.matchAll(/(\([^)]*\)|\{[^}]*\})\s*>\s*\S+\/\.version/g)]; - expect(versionRedirects.length).toBeGreaterThan(0); - for (const m of versionRedirects) { - expect(m[1].startsWith('(')).toBe(true); + test('no subshell-with-redirection patterns in any npm script', () => { + // `( cmd ) > file` parses on POSIX shells but throws on Bun shell (Windows): + // error: Subshells with redirections are currently not supported. + const offending: { script: string; pattern: string }[] = []; + for (const [name, body] of Object.entries(PKG.scripts)) { + const stripped = stripSingleQuoted(body); + const match = stripped.match(/\([^)]*\)\s*[12]?>/); + if (match) { + offending.push({ script: name, pattern: match[0] }); + } + } + expect(offending).toEqual([]); + }); + + test('no command with both stderr-suppress and stdout-redirect on one line', () => { + // `cmd 2>/dev/null > file` parses on POSIX shells but throws on Bun shell: + // error: expected a command or assignment but got: "Redirect" + const offending: { script: string; pattern: string }[] = []; + for (const [name, body] of Object.entries(PKG.scripts)) { + const stripped = stripSingleQuoted(body); + const dualRedirect = stripped.match(/\d>\S+\s+>\s*\S+|>\s*\S+\s+\d>\S+/); + if (dualRedirect) { + offending.push({ script: name, pattern: dualRedirect[0] }); + } } + expect(offending).toEqual([]); + }); + + test('.version writes are delegated to a bash script (not inline)', () => { + // .version writing must NOT be inlined into the build script — every + // safe inline form requires a Bun-shell-hostile pattern. It must go + // through scripts/write-versions.sh instead. + const build = PKG.scripts.build ?? ''; + expect(build).not.toMatch(/>\s*\S*\/\.version/); + expect(build).toContain('bash scripts/write-versions.sh'); + expect(fs.existsSync(path.join(ROOT, 'scripts/write-versions.sh'))).toBe(true); }); });