Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.**
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.39.1.0
1.39.2.0
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions scripts/write-versions.sh
Original file line number Diff line number Diff line change
@@ -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
53 changes: 43 additions & 10 deletions test/build-script-shell-compat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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);
});
});
Loading