aboutsummaryrefslogtreecommitdiff
path: root/scripts/sync-language-bundle.sh
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/sync-language-bundle.sh')
-rwxr-xr-xscripts/sync-language-bundle.sh142
1 files changed, 142 insertions, 0 deletions
diff --git a/scripts/sync-language-bundle.sh b/scripts/sync-language-bundle.sh
new file mode 100755
index 0000000..8858f74
--- /dev/null
+++ b/scripts/sync-language-bundle.sh
@@ -0,0 +1,142 @@
+#!/usr/bin/env bash
+# Per-project language-bundle freshness check, designed to run at startup.
+#
+# Detects which language bundle(s) a project has by fingerprint (no marker
+# file), then reconciles them against the canonical rulesets source:
+# - AUTO-FIX (rulesets-owned, safe to overwrite): .claude/rules/*.md,
+# .claude/hooks/*, githooks/*
+# - SURFACE (project may customize — reported, never written):
+# .claude/settings.json
+# CLAUDE.md is intentionally not tracked: it is seed-only in install-lang
+# and project-owned afterward (diff-lang skips it for the same reason).
+#
+# Usage: sync-language-bundle.sh [project-path] (default: $PWD)
+#
+# Exit: 0 no bundle, or clean / rules+hooks auto-fixed (resolved)
+# 3 manual action recommended (settings.json / CLAUDE.md drift)
+# 1 usage / path error
+#
+# Quiet when there is nothing to report. Resolves the canonical source
+# relative to its own location, so it always reads the current checkout.
+
+set -u
+
+PROJECT="${1:-$PWD}"
+
+REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+LANG_ROOT="$REPO_ROOT/languages"
+GENERIC_RULES="$REPO_ROOT/claude-rules"
+
+if [ ! -d "$PROJECT" ]; then
+ echo "ERROR: project path does not exist: $PROJECT" >&2
+ exit 1
+fi
+PROJECT="$(cd "$PROJECT" && pwd)"
+
+[ -d "$LANG_ROOT" ] || exit 0 # no bundles available — nothing to do
+
+OUT="" # buffered report, printed once at the end
+FIXED=0
+MANUAL=0
+HEADER_DONE=0
+CUR_HEADER=""
+
+ensure_header() {
+ if [ "$HEADER_DONE" -eq 0 ]; then
+ OUT+="$CUR_HEADER"$'\n'
+ HEADER_DONE=1
+ fi
+}
+
+# fix SRC DST [exec] — overwrite DST from SRC if missing or differing.
+fix() {
+ local src="$1" dst="$2" want_exec="${3:-}"
+ [ -f "$src" ] || return 0
+ if [ ! -f "$dst" ] || ! diff -q "$src" "$dst" >/dev/null 2>&1; then
+ mkdir -p "$(dirname "$dst")"
+ cp "$src" "$dst"
+ [ "$want_exec" = exec ] && chmod +x "$dst"
+ ensure_header
+ OUT+=" fixed ${dst#"$PROJECT"/}"$'\n'
+ FIXED=$((FIXED + 1))
+ fi
+}
+
+# surface SRC DST — report DST drift without writing.
+surface() {
+ local src="$1" dst="$2" reason=""
+ [ -f "$src" ] || return 0
+ if [ ! -f "$dst" ]; then
+ reason="missing"
+ elif ! diff -q "$src" "$dst" >/dev/null 2>&1; then
+ reason="differs"
+ else
+ return 0
+ fi
+ ensure_header
+ OUT+=" drift ${dst#"$PROJECT"/} ($reason — not auto-fixed)"$'\n'
+ MANUAL=$((MANUAL + 1))
+}
+
+for src_lang in "$LANG_ROOT"/*/; do
+ [ -d "$src_lang" ] || continue
+ src_lang="${src_lang%/}"
+ lang="$(basename "$src_lang")"
+ lang_rules="$src_lang/claude/rules"
+ [ -d "$lang_rules" ] || continue
+
+ # Fingerprint: project has this bundle iff any of the language's own
+ # rule files is present in .claude/rules/.
+ detected=0
+ for rf in "$lang_rules"/*.md; do
+ [ -f "$rf" ] || continue
+ if [ -f "$PROJECT/.claude/rules/$(basename "$rf")" ]; then
+ detected=1
+ break
+ fi
+ done
+ [ "$detected" -eq 1 ] || continue
+
+ CUR_HEADER="language bundle '$lang' — $PROJECT:"
+ HEADER_DONE=0
+ manual_before=$MANUAL
+
+ # AUTO-FIX: generic rules (shared) + language rules
+ for f in "$GENERIC_RULES"/*.md; do
+ [ -f "$f" ] || continue
+ fix "$f" "$PROJECT/.claude/rules/$(basename "$f")"
+ done
+ for f in "$lang_rules"/*.md; do
+ [ -f "$f" ] || continue
+ fix "$f" "$PROJECT/.claude/rules/$(basename "$f")"
+ done
+
+ # AUTO-FIX: language hooks (executable)
+ if [ -d "$src_lang/claude/hooks" ]; then
+ while IFS= read -r f; do
+ rel="${f#"$src_lang"/claude/}"
+ fix "$f" "$PROJECT/.claude/$rel" exec
+ done < <(find "$src_lang/claude/hooks" -type f)
+ fi
+
+ # AUTO-FIX: githooks (executable)
+ if [ -d "$src_lang/githooks" ]; then
+ while IFS= read -r f; do
+ rel="${f#"$src_lang"/githooks/}"
+ fix "$f" "$PROJECT/githooks/$rel" exec
+ done < <(find "$src_lang/githooks" -type f)
+ fi
+
+ # SURFACE-ONLY: settings.json may carry project customization, so report
+ # rather than overwrite. (CLAUDE.md is intentionally untracked here — it's
+ # seed-only in install-lang and project-owned after, same as diff-lang skips it.)
+ surface "$src_lang/claude/settings.json" "$PROJECT/.claude/settings.json"
+
+ if [ "$MANUAL" -gt "$manual_before" ]; then
+ OUT+=" → reconcile: make install-$lang PROJECT=."$'\n'
+ fi
+done
+
+[ -n "$OUT" ] && printf '%s' "$OUT"
+[ "$MANUAL" -gt 0 ] && exit 3
+exit 0