#!/usr/bin/env bash # gstack-gbrain-source-wireup — register the gstack brain repo as a gbrain # federated source via `git worktree`, run an initial sync, hook into # subsequent skill-end syncs. # # Replaces the v1.12.2.0 dead `consumers.json + ingest_url + /ingest-repo` # wireup which depended on a gbrain HTTP endpoint that never shipped. # # Usage: # gstack-gbrain-source-wireup [--strict] [--source-id ] [--no-pull] # gstack-gbrain-source-wireup --uninstall [--source-id ] # gstack-gbrain-source-wireup --probe # gstack-gbrain-source-wireup --help # # Exit codes: # 0 — success, OR benign skip without --strict # 1 — hard failure (gbrain or git op errored on a real call) # 2 — missing prereqs (no gbrain >= 0.18.0, no .git or remote-file) # 3 — source-id derivation failed in --uninstall, no fallback worked # # Env: # GSTACK_HOME — override ~/.gstack (test harness) # GSTACK_BRAIN_WORKTREE — override worktree path (default ~/.gstack-brain-worktree) # GSTACK_BRAIN_SOURCE_ID — id override; --source-id flag takes precedence # GSTACK_BRAIN_NO_SYNC — skip the gbrain sync step (tests; helper still # ensures source registration) # # Depends on: jq (transitive via gstack-gbrain-detect). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" CONFIG_BIN="$SCRIPT_DIR/gstack-config" GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}" WORKTREE="${GSTACK_BRAIN_WORKTREE:-$HOME/.gstack-brain-worktree}" REMOTE_FILE="$HOME/.gstack-brain-remote.txt" PLIST_PATH="$HOME/Library/LaunchAgents/com.gstack.brain-sync.plist" # ---- arg parse ---- MODE="wireup" STRICT=0 NO_PULL=0 SOURCE_ID="" while [ $# -gt 0 ]; do case "$1" in --uninstall) MODE="uninstall"; shift ;; --probe) MODE="probe"; shift ;; --strict) STRICT=1; shift ;; --no-pull) NO_PULL=1; shift ;; --source-id) SOURCE_ID="$2"; shift 2 ;; --help|-h) sed -n '2,28p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "Unknown flag: $1" >&2; exit 1 ;; esac done prefix() { sed 's/^/gstack-gbrain-source-wireup: /' >&2; } warn() { echo "$*" | prefix; } die() { warn "$*"; exit "${2:-1}"; } # ---- source-id derivation (D6 multi-fallback) ---- derive_source_id() { if [ -n "$SOURCE_ID" ]; then echo "$SOURCE_ID"; return 0 fi if [ -n "${GSTACK_BRAIN_SOURCE_ID:-}" ]; then echo "$GSTACK_BRAIN_SOURCE_ID"; return 0 fi local remote_url="" remote_url=$(git -C "$GSTACK_HOME" remote get-url origin 2>/dev/null) || true if [ -z "$remote_url" ] && [ -f "$REMOTE_FILE" ]; then remote_url=$(head -1 "$REMOTE_FILE" 2>/dev/null | tr -d '[:space:]') fi [ -z "$remote_url" ] && return 3 basename "$remote_url" .git \ | tr '[:upper:]' '[:lower:]' \ | tr -c 'a-z0-9-' '-' \ | sed 's/--*/-/g; s/^-//; s/-$//' \ | cut -c1-32 } # ---- gbrain version gate ---- gbrain_version_ok() { if ! command -v gbrain >/dev/null 2>&1; then return 1 fi local v v=$(gbrain --version 2>/dev/null | awk '{print $2}') [ -z "$v" ] && return 1 # 0.18.0 minimum (gbrain sources shipped here) [ "$(printf '%s\n0.18.0\n' "$v" | sort -V | head -1)" = "0.18.0" ] } # ---- worktree management ---- # A worktree is always created `--detach`ed at $GSTACK_HOME's HEAD. Detached # because a branch (main) can only be checked out in ONE worktree, and the # parent at $GSTACK_HOME already has it. To advance, we re-checkout the # parent's current HEAD into the detached worktree. _worktree_add_detached() { local sha sha=$(git -C "$GSTACK_HOME" rev-parse HEAD 2>/dev/null) || return 1 git -C "$GSTACK_HOME" worktree prune 2>/dev/null || true git -C "$GSTACK_HOME" worktree add --detach "$WORKTREE" "$sha" 2>/dev/null } ensure_worktree() { if [ ! -d "$GSTACK_HOME/.git" ]; then return 2 fi if [ -d "$WORKTREE/.git" ] || [ -f "$WORKTREE/.git" ]; then # already exists; advance the detached HEAD to parent's current HEAD if [ "$NO_PULL" = "0" ]; then local sha sha=$(git -C "$GSTACK_HOME" rev-parse HEAD 2>/dev/null) || return 1 ( cd "$WORKTREE" && git checkout --detach "$sha" 2>/dev/null ) || { warn "worktree at $WORKTREE could not advance to $sha; resetting via remove + re-add" git -C "$GSTACK_HOME" worktree remove --force "$WORKTREE" 2>/dev/null || rm -rf "$WORKTREE" _worktree_add_detached || return 1 } fi return 0 fi # Stray non-git dir? Remove first. [ -e "$WORKTREE" ] && rm -rf "$WORKTREE" _worktree_add_detached || return 1 } # ---- gbrain sources operations ---- # Returns 0 if source with id exists at expected path. 1 if exists but path differs. 2 if absent. check_source_state() { local id="$1" local existing_path existing_path=$(gbrain sources list --json 2>/dev/null \ | jq -r --arg id "$id" '.sources[] | select(.id==$id) | .local_path' 2>/dev/null) || existing_path="" if [ -z "$existing_path" ]; then return 2 fi if [ "$existing_path" = "$WORKTREE" ]; then return 0 fi return 1 } # ---- modes ---- do_probe() { local id worktree_status="absent" gbrain_status="missing" source_status="absent" id=$(derive_source_id 2>/dev/null) || id="(unknown)" [ -d "$WORKTREE/.git" ] || [ -f "$WORKTREE/.git" ] && worktree_status="present" if gbrain_version_ok; then gbrain_status="ok ($(gbrain --version 2>/dev/null | awk '{print $2}'))" if check_source_state "$id"; then source_status="registered ($WORKTREE)" elif [ $? = 1 ]; then source_status="registered (different path)" fi fi echo "source_id=$id" echo "worktree=$WORKTREE" echo "worktree_status=$worktree_status" echo "gbrain=$gbrain_status" echo "source_status=$source_status" } do_wireup() { local id id=$(derive_source_id) || die "cannot derive source id (no .git, no remote-file, no --source-id)" 2 if ! gbrain_version_ok; then if [ "$STRICT" = "1" ]; then die "gbrain not installed or < 0.18.0; install/upgrade gbrain and re-run" 2 fi warn "gbrain not installed or < 0.18.0; skipping wireup (benign skip)" exit 0 fi ensure_worktree || { if [ $? = 2 ]; then [ "$STRICT" = "1" ] && die "no $GSTACK_HOME/.git; run /setup-gbrain Step 7 (gstack-brain-init) first" 2 warn "no $GSTACK_HOME/.git; skipping (benign skip)" exit 0 fi die "git worktree creation failed at $WORKTREE" 1 } # Source registration: probe state, then act. set +e check_source_state "$id" local sstate=$? set -e case "$sstate" in 0) : ;; # already correctly registered 1) warn "source $id registered with different path; recreating (gbrain has no 'sources update')" gbrain sources remove "$id" --yes 2>&1 | prefix || die "gbrain sources remove failed" 1 gbrain sources add "$id" --path "$WORKTREE" --federated 2>&1 | prefix \ || die "gbrain sources add failed" 1 ;; 2) gbrain sources add "$id" --path "$WORKTREE" --federated 2>&1 | prefix \ || die "gbrain sources add failed" 1 ;; esac if [ "${GSTACK_BRAIN_NO_SYNC:-0}" = "1" ]; then echo "source_id=$id" echo "worktree=$WORKTREE" echo "pages_synced=skipped" exit 0 fi local sync_out sync_out=$(gbrain sync --repo "$WORKTREE" 2>&1) || die "gbrain sync failed: $sync_out" 1 echo "$sync_out" | tail -3 | prefix echo "source_id=$id" echo "worktree=$WORKTREE" echo "pages_synced=$(echo "$sync_out" | grep -oE '[0-9]+ pages? imported' | head -1 || echo 'incremental')" } do_uninstall() { local id id=$(derive_source_id) || die "cannot derive source id; pass --source-id explicitly" 3 if command -v gbrain >/dev/null 2>&1; then gbrain sources remove "$id" --yes 2>&1 | prefix || warn "gbrain sources remove failed (continuing)" fi if [ -d "$WORKTREE/.git" ] || [ -f "$WORKTREE/.git" ]; then git -C "$GSTACK_HOME" worktree remove --force "$WORKTREE" 2>/dev/null \ || rm -rf "$WORKTREE" fi # Cron-stub: future launchd plist (not created today; safety net for D9 future). rm -f "$PLIST_PATH" 2>/dev/null || true echo "uninstalled source=$id worktree=$WORKTREE" } case "$MODE" in probe) do_probe ;; wireup) do_wireup ;; uninstall) do_uninstall ;; esac