aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/sync-check.sh72
-rw-r--r--scripts/tests/sync-check.bats86
2 files changed, 158 insertions, 0 deletions
diff --git a/scripts/sync-check.sh b/scripts/sync-check.sh
new file mode 100755
index 0000000..8fb9ab0
--- /dev/null
+++ b/scripts/sync-check.sh
@@ -0,0 +1,72 @@
+#!/usr/bin/env bash
+#
+# sync-check.sh — verify canonical claude-templates/.ai/ matches the .ai/ mirror.
+#
+# Usage:
+# scripts/sync-check.sh # exit 0 if synced, 1 if drift
+# scripts/sync-check.sh --fix # rsync canonical → mirror, then re-check
+#
+# Runs as a pre-commit hook (githooks/pre-commit) to catch drift before
+# commit, and as a manual check via `make sync-check`.
+#
+# The three synced paths are:
+# claude-templates/.ai/protocols.org ↔ .ai/protocols.org
+# claude-templates/.ai/workflows/ ↔ .ai/workflows/
+# claude-templates/.ai/scripts/ ↔ .ai/scripts/
+#
+# Source of truth is the canonical (claude-templates/) side. The mirror
+# exists so rulesets-as-a-project has a working copy. Drift in either
+# direction is a defect — both have to land in the same commit.
+
+set -euo pipefail
+
+if ! repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || [ -z "$repo_root" ]; then
+ echo "sync-check: not inside a git checkout" >&2
+ exit 2
+fi
+
+canonical="$repo_root/claude-templates/.ai"
+mirror="$repo_root/.ai"
+
+if [ ! -d "$canonical" ] || [ ! -d "$mirror" ]; then
+ echo "sync-check: not a rulesets-shaped repo (missing claude-templates/.ai or .ai)" >&2
+ exit 2
+fi
+
+paths=(protocols.org workflows scripts)
+
+check_drift() {
+ local drift=0
+ for relpath in "${paths[@]}"; do
+ if ! diff -rq "$canonical/$relpath" "$mirror/$relpath" >/dev/null 2>&1; then
+ echo "drift: claude-templates/.ai/$relpath ↔ .ai/$relpath" >&2
+ diff -rq "$canonical/$relpath" "$mirror/$relpath" 2>&1 | head -20 >&2
+ drift=1
+ fi
+ done
+ return "$drift"
+}
+
+if check_drift; then
+ exit 0
+fi
+
+if [ "${1:-}" = "--fix" ]; then
+ echo "" >&2
+ echo "sync-check --fix: syncing canonical → mirror..." >&2
+ rsync -a "$canonical/protocols.org" "$mirror/protocols.org"
+ rsync -a --delete "$canonical/workflows/" "$mirror/workflows/"
+ rsync -a --delete "$canonical/scripts/" "$mirror/scripts/"
+ if check_drift; then
+ echo "sync-check --fix: resolved." >&2
+ echo "Re-stage the synced files and retry the commit." >&2
+ exit 0
+ else
+ echo "sync-check --fix: drift persists after sync. Inspect manually." >&2
+ exit 1
+ fi
+fi
+
+echo "" >&2
+echo "Run 'scripts/sync-check.sh --fix' (or 'make sync-check FIX=1') to resolve." >&2
+exit 1
diff --git a/scripts/tests/sync-check.bats b/scripts/tests/sync-check.bats
new file mode 100644
index 0000000..df775b3
--- /dev/null
+++ b/scripts/tests/sync-check.bats
@@ -0,0 +1,86 @@
+#!/usr/bin/env bats
+#
+# Tests for scripts/sync-check.sh
+#
+# Sandboxes a fake rulesets-shaped git repo under $BATS_TMPDIR, drops the
+# real script in via PATH override, and exercises clean / drift / --fix
+# behavior.
+
+setup() {
+ SANDBOX="$BATS_TEST_TMPDIR/repo"
+ SCRIPT_SRC="$BATS_TEST_DIRNAME/../sync-check.sh"
+ mkdir -p "$SANDBOX/scripts" "$SANDBOX/claude-templates/.ai/workflows" \
+ "$SANDBOX/claude-templates/.ai/scripts" "$SANDBOX/.ai/workflows" \
+ "$SANDBOX/.ai/scripts"
+ cp "$SCRIPT_SRC" "$SANDBOX/scripts/sync-check.sh"
+ chmod +x "$SANDBOX/scripts/sync-check.sh"
+ cd "$SANDBOX"
+ git init -q
+ git config user.email test@example.com
+ git config user.name test
+ echo "protocols content" > claude-templates/.ai/protocols.org
+ echo "protocols content" > .ai/protocols.org
+ echo "workflow a" > claude-templates/.ai/workflows/a.org
+ echo "workflow a" > .ai/workflows/a.org
+ echo "script b" > claude-templates/.ai/scripts/b.sh
+ echo "script b" > .ai/scripts/b.sh
+}
+
+@test "clean tree: exit 0" {
+ run scripts/sync-check.sh
+ [ "$status" -eq 0 ]
+}
+
+@test "drift in protocols.org: exit 1, names the file" {
+ echo "drifted" > .ai/protocols.org
+ run scripts/sync-check.sh
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"protocols.org"* ]]
+}
+
+@test "drift in workflows/: exit 1, names workflows" {
+ echo "workflow a modified" > .ai/workflows/a.org
+ run scripts/sync-check.sh
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"workflows"* ]]
+}
+
+@test "drift in scripts/: exit 1, names scripts" {
+ echo "script b modified" > .ai/scripts/b.sh
+ run scripts/sync-check.sh
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"scripts"* ]]
+}
+
+@test "drift with --fix: syncs and exits 0" {
+ echo "drifted" > .ai/protocols.org
+ run scripts/sync-check.sh --fix
+ [ "$status" -eq 0 ]
+ run cat .ai/protocols.org
+ [ "$output" = "protocols content" ]
+}
+
+@test "extra file in mirror: --delete removes it" {
+ echo "stale" > .ai/workflows/stale.org
+ run scripts/sync-check.sh --fix
+ [ "$status" -eq 0 ]
+ [ ! -f .ai/workflows/stale.org ]
+}
+
+@test "missing canonical: exit 2 with clear error" {
+ rm -rf claude-templates
+ run scripts/sync-check.sh
+ [ "$status" -eq 2 ]
+ [[ "$output" == *"not a rulesets-shaped repo"* ]]
+}
+
+@test "outside a git checkout: exit 2" {
+ cd "$BATS_TEST_TMPDIR"
+ mkdir not-a-repo
+ cp "$SCRIPT_SRC" not-a-repo/sync-check.sh
+ chmod +x not-a-repo/sync-check.sh
+ cd not-a-repo
+ run ./sync-check.sh
+ [ "$status" -eq 2 ]
+ [[ "$output" == *"not inside a git checkout"* ]]
+}