#!/usr/bin/env bash # gstack-brain-restore — bootstrap a new machine from an existing brain repo. # # Usage: # gstack-brain-restore [] # # If no URL is given, reads from ~/.gstack-brain-remote.txt (written by # gstack-brain-init on the original machine). Copy that file to the new # machine before running this command. # # Safety gates (refuses with clear message): # - ~/.gstack/.git already exists with a DIFFERENT remote # - ~/.gstack/ contains non-allowlisted, non-gitignored user files # that would be clobbered by restore # # What it does: # 1. Clone the remote to a staging directory # 2. Validate the repo is gstack-brain-shaped (.brain-allowlist, .gitattributes) # 3. rsync-copy tracked files into ~/.gstack/ with skip-if-same-hash # 4. Move staging's .git into ~/.gstack/.git # 5. Register local git config merge drivers (they don't clone from remote) # 6. Wire the cloned brain into gbrain via gstack-gbrain-source-wireup # (best-effort; restore continues even if gbrain wireup fails) # # Env: # GSTACK_HOME — override ~/.gstack # Heredoc delivery guard. bash 5.2+ writes a heredoc body <=64KiB through a # pipe in the forked child before exec, with no reader on the other end. On # macOS under pipe-KVA pressure a fresh pipe gets a 512-byte buffer, so any # body >=512B blocks write() forever and the script hangs at startup with no # output. Compat level 50 restores the tempfile path. These scripts are # bash-3.2-clean, so the compat level costs them nothing. Not exported: the # guard is per-script, and it survives `bash script.sh` call sites that # bypass the shebang. BASH_COMPAT=50 set -euo pipefail . "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/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" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" CONFIG_BIN="$SCRIPT_DIR/gstack-config" # Egress receipt helpers (_receipted_git): fail-open for user-directed # git ops against the user's own artifacts remote. . "$SCRIPT_DIR/gstack-egress-lib.sh" # v1.27.0.0+ canonical name; brain-remote is the legacy fallback during the # migration window. The migration script renames the file in place. if [ -f "$HOME/.gstack-artifacts-remote.txt" ]; then REMOTE_FILE="$HOME/.gstack-artifacts-remote.txt" else REMOTE_FILE="$HOME/.gstack-brain-remote.txt" fi REMOTE_URL="${1:-}" if [ -z "$REMOTE_URL" ]; then if [ -f "$REMOTE_FILE" ]; then REMOTE_URL=$(head -1 "$REMOTE_FILE" | tr -d '[:space:]') fi fi if [ -z "$REMOTE_URL" ]; then cat >&2 < or put the URL in $REMOTE_FILE (copy from the original machine) EOF exit 1 fi # ---- safety gates ---- if [ -d "$GSTACK_HOME/.git" ]; then EXISTING_REMOTE=$(git -C "$GSTACK_HOME" remote get-url origin 2>/dev/null || echo "") if [ -n "$EXISTING_REMOTE" ] && [ "$EXISTING_REMOTE" != "$REMOTE_URL" ]; then cat >&2 <&2 exit 1 fi # ---- clone to staging ---- STAGING=$(mktemp -d "${TMPDIR:-/tmp}/gstack-brain-restore.XXXXXX") trap 'rm -rf "$STAGING" 2>/dev/null; gstack_brain_lock_release' EXIT echo "Cloning $REMOTE_URL to staging..." RESTORE_HOST="${REMOTE_URL#*://}"; RESTORE_HOST="${RESTORE_HOST#*@}"; RESTORE_HOST="${RESTORE_HOST%%[/:]*}" if ! _receipted_git open brain-restore "${RESTORE_HOST:-unknown}" brain-restore-clone "user ran gstack-brain-restore" \ bash -c 'git clone --quiet "$1" "$2" 2>/dev/null' _ "$REMOTE_URL" "$STAGING/repo"; then echo "Clone failed. Check:" >&2 echo " - URL is correct: $REMOTE_URL" >&2 echo " - Auth: gh auth status (github) / glab auth status (gitlab)" >&2 exit 1 fi # ---- validate shape ---- if [ ! -f "$STAGING/repo/.brain-allowlist" ] || [ ! -f "$STAGING/repo/.gitattributes" ]; then cat >&2 < 5: print(f"...and {len(risks) - 5} more") sys.exit(0 if not risks else 2) PYEOF ) || true if [ -n "$CLOBBER_RISK" ]; then cat >&2 </dev/null 2>&1' _ "$GSTACK_HOME" || true else mv "$STAGING/repo/.git" "$GSTACK_HOME/.git" fi # ---- register merge drivers (local git config; don't survive clones) ---- git -C "$GSTACK_HOME" config merge.jsonl-append.driver "$SCRIPT_DIR/gstack-jsonl-merge %O %A %B" git -C "$GSTACK_HOME" config merge.jsonl-append.name "gstack JSONL append-only merger" git -C "$GSTACK_HOME" config merge.union.driver "cat %A %B > %A.merged && mv %A.merged %A" git -C "$GSTACK_HOME" config merge.union.name "union concat" # ---- install pre-commit hook (same as init) ---- HOOK="$GSTACK_HOME/.git/hooks/pre-commit" mkdir -p "$(dirname "$HOOK")" cp "$SCRIPT_DIR/gstack-brain-secret-scan.py" "$(dirname "$HOOK")/gstack-brain-secret-scan.py" cat > "$HOOK" <<'HOOK_EOF' #!/usr/bin/env bash # gstack-brain pre-commit hook: secret-scan defense-in-depth. It runs a copy of the # scanner gstack-brain-sync runs before staging (same families, the whole # staged diff, JSON string values decoded), written beside this hook by # gstack-artifacts-init and gstack-brain-restore, so a manual `git commit` against # the artifacts repo is held the same way. set -uo pipefail SCAN="$(dirname "$0")/gstack-brain-secret-scan.py" if [ ! -f "$SCAN" ]; then echo "gstack-brain pre-commit: refusing commit — the secret scanner $SCAN is missing; rerun gstack-brain-restore to reinstall this hook." >&2 exit 1 fi TEXT=$(mktemp) || { echo "gstack-brain pre-commit: refusing commit — could not create a temporary file for the secret scan." >&2; exit 1; } trap 'rm -f "$TEXT"' EXIT if ! git diff --cached --no-renames --no-color --no-ext-diff --text --no-textconv -U3 > "$TEXT"; then echo "gstack-brain pre-commit: refusing commit — git could not read the staged diff for the secret scan." >&2 exit 1 fi hits=$(git diff --cached --no-renames --no-color --no-ext-diff | python3 "$SCAN" gate - "$TEXT") rc=$? [ "$rc" -eq 0 ] && exit 0 if [ "$rc" -eq 3 ]; then echo "gstack-brain pre-commit: refusing commit — $hits detected in staged diff." >&2 else echo "gstack-brain pre-commit: refusing commit — the secret scan failed (exit $rc)." >&2 fi exit 1 HOOK_EOF chmod +x "$HOOK" # ---- write remote helper file if missing ---- if [ ! -f "$REMOTE_FILE" ]; then echo "$REMOTE_URL" > "$REMOTE_FILE" chmod 600 "$REMOTE_FILE" echo "" echo "Wrote $REMOTE_FILE for future skill-run auto-detection." fi # ---- wire the cloned brain into gbrain (best-effort) ---- WIREUP_BIN="$SCRIPT_DIR/gstack-gbrain-source-wireup" if [ -x "$WIREUP_BIN" ]; then "$WIREUP_BIN" || >&2 echo "WARNING: gbrain wireup failed; run $WIREUP_BIN manually after fixing prereqs" fi cat <