#!/usr/bin/env bash # install_hooks.sh — install a custodian post-commit sync hook into registered repos. # # Usage: # ./install_hooks.sh --repo # install into one repo # ./install_hooks.sh --all # install into all registered repos # ./install_hooks.sh --repo --remove # remove hook from one repo # ./install_hooks.sh --all --remove # remove hook from all repos # # The hook runs `make fix-consistency REPO=` in the state-hub after each # commit, keeping the hub in sync with workplan file changes automatically. # # Idempotent: the hook block is guarded by a marker comment. Running twice is safe. set -euo pipefail STATEHUB_DIR="$(cd "$(dirname "$0")/.." && pwd)" API_BASE="${STATE_HUB_API:-http://127.0.0.1:8000}" MARKER="# custodian-sync-hook" usage() { echo "Usage: $0 --repo | --all [--remove]" exit 1 } # ── Arg parsing ─────────────────────────────────────────────────────────────── REPO_SLUG="" DO_ALL=false REMOVE=false while [[ $# -gt 0 ]]; do case "$1" in --repo) REPO_SLUG="$2"; shift 2 ;; --all) DO_ALL=true; shift ;; --remove) REMOVE=true; shift ;; -h|--help) usage ;; *) echo "Unknown argument: $1"; usage ;; esac done if [[ -z "$REPO_SLUG" && "$DO_ALL" == false ]]; then usage; fi # ── Helper: resolve local path for a repo slug ─────────────────────────────── resolve_path() { local slug="$1" # Try the registered local_path first local api_path api_path=$(curl -sf "${API_BASE}/repos/${slug}/" | python3 -c \ "import json,sys; d=json.load(sys.stdin); print(d.get('local_path') or '')" 2>/dev/null || true) if [[ -n "$api_path" && -d "$api_path" ]]; then echo "$api_path" return fi # Fall back to convention: /home// local conventional="/home/$(whoami)/${slug}" if [[ -d "$conventional" ]]; then echo "$conventional" return fi echo "" } # ── Helper: install hook into one repo ─────────────────────────────────────── install_hook() { local slug="$1" local repo_path repo_path=$(resolve_path "$slug") if [[ -z "$repo_path" ]]; then echo " ⚠ $slug: no local path found — skipping" return fi if [[ ! -d "$repo_path/.git" ]]; then echo " ⚠ $slug: $repo_path is not a git repo — skipping" return fi local hook_file="$repo_path/.git/hooks/post-commit" local hook_block hook_block=$(cat </dev/null 2>&1; then (cd "${STATEHUB_DIR}" && make fix-consistency REPO=${slug} >/dev/null 2>&1 &) fi ${MARKER}-end BLOCK ) if [[ -f "$hook_file" ]] && grep -q "$MARKER" "$hook_file"; then echo " ✓ $slug: hook already present at $hook_file" return fi if [[ -f "$hook_file" ]]; then # Prepend to existing hook local existing existing=$(cat "$hook_file") printf '#!/usr/bin/env bash\n%s\n\n%s\n' "$hook_block" "$existing" > "$hook_file" else printf '#!/usr/bin/env bash\n%s\n' "$hook_block" > "$hook_file" fi chmod +x "$hook_file" echo " ✅ $slug: hook installed at $hook_file" } # ── Helper: remove hook from one repo ──────────────────────────────────────── remove_hook() { local slug="$1" local repo_path repo_path=$(resolve_path "$slug") if [[ -z "$repo_path" || ! -f "$repo_path/.git/hooks/post-commit" ]]; then echo " – $slug: no hook file found — skipping" return fi local hook_file="$repo_path/.git/hooks/post-commit" if ! grep -q "$MARKER" "$hook_file"; then echo " – $slug: custodian marker not found in hook — skipping" return fi # Remove the marked block (between MARKER and MARKER-end inclusive) python3 - "$hook_file" <<'PY' import sys, re path = sys.argv[1] text = open(path).read() cleaned = re.sub( r'# custodian-sync-hook.*?# custodian-sync-hook-end\n?', '', text, flags=re.DOTALL, ) open(path, 'w').write(cleaned) PY echo " 🗑 $slug: hook block removed from $hook_file" } # ── Collect repo slugs ──────────────────────────────────────────────────────── if $DO_ALL; then mapfile -t SLUGS < <(curl -sf "${API_BASE}/repos/" | python3 -c \ "import json,sys; [print(r['slug']) for r in json.load(sys.stdin) if r.get('status') == 'active']") else SLUGS=("$REPO_SLUG") fi echo "Custodian sync hook — $( $REMOVE && echo 'removing' || echo 'installing' ) for ${#SLUGS[@]} repo(s)" for slug in "${SLUGS[@]}"; do if $REMOVE; then remove_hook "$slug"; else install_hook "$slug"; fi done echo "Done."