fix(windows): migrations resolve their directory with either path separator; state-root parity compares under the HOME Git Bash actually sees

This commit is contained in:
garrytan committed 2026-09-30 07:19:25 +00:00
1 parent fa81927f3f
commit d361f9a8b8
11 files changed
+39 -20

No files matched your search

+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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
+19 -10
View File
@@ -17,19 +17,28 @@ const BASH = Bun.which('bash') ?? '/bin/bash';
type Row = { name: string; env: Record<string, string>; 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<string, string> {
return { USERPROFILE: '', ...row.env };
function tsEnv(row: Row, childHome: string): Record<string, string> {
const env: Record<string, string> = { 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}`);
}
}
});