Files
state-hub/scripts/install_hooks.sh
tegwick 768a8ba9c7 fix(api): normalize trailing slashes — no slash on param routes
Rule: trailing slash only on collection roots (/). Any route containing
a path parameter {…} uses no trailing slash. Applies across all routers,
scripts, Makefile, and tests. Fixes 307-redirect fragility on POST/PATCH
from naive clients (curl, Codex HTTP calls).

Also adds POST /repos/{slug}/sync — runs ADR-001 consistency check with
--fix via HTTP, so non-MCP agents (Codex) can self-service DB sync without
operator intervention.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 15:13:01 +02:00

150 lines
4.9 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# install_hooks.sh — install a custodian post-commit sync hook into registered repos.
#
# Usage:
# ./install_hooks.sh --repo <slug> # install into one repo
# ./install_hooks.sh --all # install into all registered repos
# ./install_hooks.sh --repo <slug> --remove # remove hook from one repo
# ./install_hooks.sh --all --remove # remove hook from all repos
#
# The hook runs `make fix-consistency REPO=<slug>` 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 <slug> | --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/<user>/<slug>
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 <<BLOCK
${MARKER} — managed by custodian, do not edit this block
if curl -sf ${API_BASE}/state/health >/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."