#!/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. Rehydrate consumers.json endpoints; prompt for tokens # # Env: # GSTACK_HOME — override ~/.gstack set -euo pipefail GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" CONFIG_BIN="$SCRIPT_DIR/gstack-config" REMOTE_FILE="$HOME/.gstack-brain-remote.txt" 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 </dev/null' EXIT echo "Cloning $REMOTE_URL to staging..." if ! git clone --quiet "$REMOTE_URL" "$STAGING/repo" 2>/dev/null; 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 || 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")" cat > "$HOOK" <<'HOOK_EOF' #!/usr/bin/env bash set -uo pipefail python3 -c " import sys, re, subprocess try: out = subprocess.check_output(['git', 'diff', '--cached'], stderr=subprocess.DEVNULL).decode('utf-8', 'replace') except Exception: sys.exit(0) patterns = [ ('aws-access-key', re.compile(r'AKIA[0-9A-Z]{16}')), ('github-token', re.compile(r'\b(gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})')), ('openai-key', re.compile(r'\bsk-[A-Za-z0-9_-]{20,}')), ('pem-block', re.compile(r'-----BEGIN [A-Z ]{3,}-----')), ('jwt', re.compile(r'\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b')), ('bearer-token-json', re.compile(r'\"(authorization|api[_-]?key|apikey|token|secret|password)\"\s*:\s*\"[A-Za-z0-9_./+=-]{16,}\"', re.IGNORECASE)), ] for name, rx in patterns: if rx.search(out): sys.stderr.write(f'gstack-brain pre-commit: refusing commit — {name} detected.\n') sys.exit(1) sys.exit(0) " HOOK_EOF chmod +x "$HOOK" # ---- rehydrate consumers, prompt for tokens ---- if [ -f "$GSTACK_HOME/consumers.json" ]; then echo "" echo "Consumer registry restored. Tokens are machine-local and NOT synced." echo "Run these for each consumer to re-enter tokens:" python3 - "$GSTACK_HOME/consumers.json" <<'PYEOF' import sys, json try: with open(sys.argv[1]) as f: data = json.load(f) except Exception: sys.exit(0) for c in data.get("consumers", []): name = c.get("name", "") token_ref = c.get("token_ref", f"{name}_token") print(f" gstack-config set {token_ref} ") PYEOF fi # ---- 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 cat <