diff --git a/gstack-upgrade/migrations/v0.16.2.0.sh b/gstack-upgrade/migrations/v0.16.2.0.sh index 534db737b..1f3f73cbd 100755 --- a/gstack-upgrade/migrations/v0.16.2.0.sh +++ b/gstack-upgrade/migrations/v0.16.2.0.sh @@ -11,7 +11,8 @@ # Affected: users who ran /office-hours before this version set -euo pipefail -. "${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } +_gstack_migration_dir="${BASH_SOURCE[0]//\\//}"; _gstack_migration_dir="${_gstack_migration_dir%/*}" +. "${_gstack_migration_dir}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${_gstack_migration_dir}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } gstack_state_root_select; GSTACK_HOME="$_gstack_sr_root" PROFILE_FILE="$GSTACK_HOME/builder-profile.jsonl" diff --git a/gstack-upgrade/migrations/v1.0.0.0.sh b/gstack-upgrade/migrations/v1.0.0.0.sh index f43ea3f6b..91cc93173 100755 --- a/gstack-upgrade/migrations/v1.0.0.0.sh +++ b/gstack-upgrade/migrations/v1.0.0.0.sh @@ -13,7 +13,8 @@ # Affected: every user on v0.19.x and below who upgrades to v1.x set -euo pipefail -. "${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } +_gstack_migration_dir="${BASH_SOURCE[0]//\\//}"; _gstack_migration_dir="${_gstack_migration_dir%/*}" +. "${_gstack_migration_dir}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${_gstack_migration_dir}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } gstack_state_root_select; GSTACK_HOME="$_gstack_sr_root" PROMPTED_FLAG="$GSTACK_HOME/.writing-style-prompted" PENDING_FLAG="$GSTACK_HOME/.writing-style-prompt-pending" diff --git a/gstack-upgrade/migrations/v1.17.0.0.sh b/gstack-upgrade/migrations/v1.17.0.0.sh index 7048c7363..16c8ef69b 100755 --- a/gstack-upgrade/migrations/v1.17.0.0.sh +++ b/gstack-upgrade/migrations/v1.17.0.0.sh @@ -38,7 +38,8 @@ if [ "$SYNC_MODE" = "off" ] || [ -z "$SYNC_MODE" ]; then fi # Skip if no brain-sync git repo exists. -. "${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } +_gstack_migration_dir="${BASH_SOURCE[0]//\\//}"; _gstack_migration_dir="${_gstack_migration_dir%/*}" +. "${_gstack_migration_dir}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${_gstack_migration_dir}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } gstack_state_root_select if [ ! -d "$_gstack_sr_root/.git" ]; then exit 0 diff --git a/gstack-upgrade/migrations/v1.27.0.0.sh b/gstack-upgrade/migrations/v1.27.0.0.sh index e43466e38..182f6b654 100755 --- a/gstack-upgrade/migrations/v1.27.0.0.sh +++ b/gstack-upgrade/migrations/v1.27.0.0.sh @@ -44,7 +44,8 @@ fi # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- -. "${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } +_gstack_migration_dir="${BASH_SOURCE[0]//\\//}"; _gstack_migration_dir="${_gstack_migration_dir%/*}" +. "${_gstack_migration_dir}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${_gstack_migration_dir}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } gstack_state_root_select; GSTACK_HOME="$_gstack_sr_root" SKILLS_DIR="${HOME}/.claude/skills" BIN_DIR="${SKILLS_DIR}/gstack/bin" diff --git a/gstack-upgrade/migrations/v1.37.0.0.sh b/gstack-upgrade/migrations/v1.37.0.0.sh index c975e6bd5..3499c16fa 100755 --- a/gstack-upgrade/migrations/v1.37.0.0.sh +++ b/gstack-upgrade/migrations/v1.37.0.0.sh @@ -38,7 +38,8 @@ if [ -z "${HOME:-}" ]; then exit 0 fi -. "${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } +_gstack_migration_dir="${BASH_SOURCE[0]//\\//}"; _gstack_migration_dir="${_gstack_migration_dir%/*}" +. "${_gstack_migration_dir}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${_gstack_migration_dir}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } gstack_state_root_select; GSTACK_HOME="$_gstack_sr_root" MIGRATIONS_DIR="$GSTACK_HOME/.migrations" DONE_TOUCH="$MIGRATIONS_DIR/v1.37.0.0.done" diff --git a/gstack-upgrade/migrations/v1.38.1.0.sh b/gstack-upgrade/migrations/v1.38.1.0.sh index 1b23e9edd..148b68070 100755 --- a/gstack-upgrade/migrations/v1.38.1.0.sh +++ b/gstack-upgrade/migrations/v1.38.1.0.sh @@ -15,7 +15,8 @@ # still run. `set -u` is fine. set -u -. "${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } +_gstack_migration_dir="${BASH_SOURCE[0]//\\//}"; _gstack_migration_dir="${_gstack_migration_dir%/*}" +. "${_gstack_migration_dir}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${_gstack_migration_dir}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } gstack_state_root_select; GSTACK_HOME="$_gstack_sr_root" ALLOWLIST="${GSTACK_HOME}/.brain-allowlist" PRIVACY="${GSTACK_HOME}/.brain-privacy-map.json" diff --git a/gstack-upgrade/migrations/v1.40.0.0.sh b/gstack-upgrade/migrations/v1.40.0.0.sh index 177705d7f..83de735ce 100755 --- a/gstack-upgrade/migrations/v1.40.0.0.sh +++ b/gstack-upgrade/migrations/v1.40.0.0.sh @@ -22,7 +22,8 @@ set -u -. "${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } +_gstack_migration_dir="${BASH_SOURCE[0]//\\//}"; _gstack_migration_dir="${_gstack_migration_dir%/*}" +. "${_gstack_migration_dir}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${_gstack_migration_dir}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } gstack_state_root_select; GSTACK_HOME="$_gstack_sr_root" ALLOWLIST="${GSTACK_HOME}/.brain-allowlist" PRIVACY="${GSTACK_HOME}/.brain-privacy-map.json" diff --git a/gstack-upgrade/migrations/v1.58.0.0.sh b/gstack-upgrade/migrations/v1.58.0.0.sh index bd5adc24f..de119f0f6 100755 --- a/gstack-upgrade/migrations/v1.58.0.0.sh +++ b/gstack-upgrade/migrations/v1.58.0.0.sh @@ -20,7 +20,8 @@ set -u -. "${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } +_gstack_migration_dir="${BASH_SOURCE[0]//\\//}"; _gstack_migration_dir="${_gstack_migration_dir%/*}" +. "${_gstack_migration_dir}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${_gstack_migration_dir}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } gstack_state_root_select; GSTACK_HOME="$_gstack_sr_root" MIGRATION_DIR="${GSTACK_HOME}/.migrations" DONE="${MIGRATION_DIR}/v1.58.0.0.done" diff --git a/gstack-upgrade/migrations/v1.65.0.0.sh b/gstack-upgrade/migrations/v1.65.0.0.sh index 44e698a58..8c84a543c 100755 --- a/gstack-upgrade/migrations/v1.65.0.0.sh +++ b/gstack-upgrade/migrations/v1.65.0.0.sh @@ -34,7 +34,8 @@ set -u -. "${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } +_gstack_migration_dir="${BASH_SOURCE[0]//\\//}"; _gstack_migration_dir="${_gstack_migration_dir%/*}" +. "${_gstack_migration_dir}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${_gstack_migration_dir}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } gstack_state_root_select; GSTACK_HOME="$_gstack_sr_root" MIGRATION_DIR="${GSTACK_HOME}/.migrations" DONE="${MIGRATION_DIR}/v1.65.0.0.done" diff --git a/gstack-upgrade/migrations/v1.78.0.0.sh b/gstack-upgrade/migrations/v1.78.0.0.sh index 28c230aaa..29d293020 100755 --- a/gstack-upgrade/migrations/v1.78.0.0.sh +++ b/gstack-upgrade/migrations/v1.78.0.0.sh @@ -15,7 +15,8 @@ set -u INSTALL_DIR="${GSTACK_INSTALL_DIR:-$HOME/.claude/skills/gstack}" -. "${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${BASH_SOURCE[0]%/*}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } +_gstack_migration_dir="${BASH_SOURCE[0]//\\//}"; _gstack_migration_dir="${_gstack_migration_dir%/*}" +. "${_gstack_migration_dir}/../../bin/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: ${_gstack_migration_dir}/../../bin/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } gstack_state_root_select; GH="$_gstack_sr_root" mkdir -p "$GH" 2>/dev/null || exit 0 diff --git a/test/state-root-parity.test.ts b/test/state-root-parity.test.ts index 295411129..c53902c1e 100644 --- a/test/state-root-parity.test.ts +++ b/test/state-root-parity.test.ts @@ -17,19 +17,28 @@ const BASH = Bun.which('bash') ?? '/bin/bash'; type Row = { name: string; env: Record; win?: boolean }; -function bashEval(row: Row, body: string): string { +// On a Windows host the MSYS runtime fills HOME before Git Bash starts, so a +// row that leaves HOME unset or empty reaches the bash twin with the runtime's +// HOME. Those rows compare both twins under the HOME the child actually saw; +// the unset-HOME branches themselves are covered on POSIX hosts. +const HOST_FILLS_HOME = process.platform === 'win32'; + +function bashEval(row: Row, body: string): { out: string; home: string } { const ostype = row.win ? 'msys' : 'linux-gnu'; - const r = spawnSync(BASH, ['-c', `OSTYPE=${ostype}; . "${TWIN}" && ${body}`], { + const r = spawnSync(BASH, ['-c', `OSTYPE=${ostype}; printf '%s\\0' "\${HOME-}"; . "${TWIN}" && ${body}`], { env: { USERPROFILE: '', ...row.env, PATH: '' }, encoding: 'utf-8', timeout: 10_000, }); if (r.status !== 0) throw new Error(`bash twin failed for ${row.name}: ${r.stderr}`); - return r.stdout; + const split = r.stdout.indexOf('\0'); + return { home: r.stdout.slice(0, split), out: r.stdout.slice(split + 1) }; } -function tsEnv(row: Row): Record { - return { USERPROFILE: '', ...row.env }; +function tsEnv(row: Row, childHome: string): Record { + const env: Record = { USERPROFILE: '', ...row.env }; + if (HOST_FILLS_HOME && !row.env.HOME) env.HOME = childHome; + return env; } const A = '/state/a', B = '/state/b', C = '/state/c'; @@ -63,8 +72,8 @@ const rootRows: Row[] = [ describe('state root parity (bash twin vs lib/state-root.ts)', () => { for (const row of rootRows) { test(row.name, () => { - const bash = bashEval(row, 'gstack_state_root; printf x').replace(/x$/, ''); - expect(bash).toBe(resolveStateRoot(tsEnv(row), row.win ? 'win32' : 'linux')); + const bash = bashEval(row, 'gstack_state_root; printf x'); + expect(bash.out.replace(/x$/, '')).toBe(resolveStateRoot(tsEnv(row, bash.home), row.win ? 'win32' : 'linux')); }); } @@ -126,9 +135,9 @@ describe('config key parity (gstack_read_config_key vs readConfigKey)', () => { ]; for (const row of envs) { for (const key of keys) { - const bash = bashEval(row, `gstack_read_config_key ${key}; printf x`).replace(/x$/, ''); - const ts = readConfigKey(key, tsEnv(row), row.win ? 'win32' : 'linux') ?? ''; - expect(`${row.name}/${key}=${bash}`).toBe(`${row.name}/${key}=${ts}`); + const bash = bashEval(row, `gstack_read_config_key ${key}; printf x`); + const ts = readConfigKey(key, tsEnv(row, bash.home), row.win ? 'win32' : 'linux') ?? ''; + expect(`${row.name}/${key}=${bash.out.replace(/x$/, '')}`).toBe(`${row.name}/${key}=${ts}`); } } });