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.sh123
1 files changed, 73 insertions, 50 deletions
diff --git a/scripts/sync-language-bundle.sh b/scripts/sync-language-bundle.sh
index 8858f74..b2db8cb 100755
--- a/scripts/sync-language-bundle.sh
+++ b/scripts/sync-language-bundle.sh
@@ -1,19 +1,26 @@
#!/usr/bin/env bash
-# Per-project language-bundle freshness check, designed to run at startup.
+# Per-project 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
+# Covers two kinds of bundle copied into a project's .claude/:
+# - LANGUAGE bundles (languages/<lang>/) — generic rules + language rules +
+# hooks + githooks, with settings.json surfaced (not auto-fixed).
+# - TEAM overlays (teams/<team>/) — only the overlay's own rule file(s);
+# no generic rules, hooks, githooks, or settings.
+#
+# Detection is by fingerprint (no marker file): a project "has" a bundle iff
+# one of that bundle's own rule files is present in the project's
+# .claude/rules/. For each detected bundle it reconciles against the canonical
+# rulesets source:
+# - AUTO-FIX (rulesets-owned, safe to overwrite): .claude/rules/*.md, and
+# for language bundles also .claude/hooks/* and githooks/*
+# - SURFACE (project may customize — reported, never written): 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)
+# 3 manual action recommended (settings.json drift)
# 1 usage / path error
#
# Quiet when there is nothing to report. Resolves the canonical source
@@ -25,6 +32,7 @@ PROJECT="${1:-$PWD}"
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
LANG_ROOT="$REPO_ROOT/languages"
+TEAM_ROOT="$REPO_ROOT/teams"
GENERIC_RULES="$REPO_ROOT/claude-rules"
if [ ! -d "$PROJECT" ]; then
@@ -33,8 +41,6 @@ if [ ! -d "$PROJECT" ]; then
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
@@ -78,64 +84,81 @@ surface() {
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
+# process_bundle KIND SRC_DIR — reconcile one bundle (KIND is language|team).
+# A team overlay owns only its own rule files; a language bundle also owns the
+# shared generic rules, its hooks/githooks, and surfaces settings.json.
+process_bundle() {
+ local kind="$1" src="${2%/}"
+ local name rules rf f rel manual_before
+ name="$(basename "$src")"
+ rules="$src/claude/rules"
+ [ -d "$rules" ] || return 0
+
+ # Fingerprint: project has this bundle iff one of its own rule files is
+ # present in .claude/rules/.
+ local detected=0
+ for rf in "$rules"/*.md; do
[ -f "$rf" ] || continue
if [ -f "$PROJECT/.claude/rules/$(basename "$rf")" ]; then
detected=1
break
fi
done
- [ "$detected" -eq 1 ] || continue
+ [ "$detected" -eq 1 ] || return 0
- CUR_HEADER="language bundle '$lang' — $PROJECT:"
+ CUR_HEADER="$kind bundle '$name' — $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
+ # AUTO-FIX: language bundles carry the shared generic rules; team overlays
+ # carry only their own rule(s).
+ if [ "$kind" = language ]; then
+ for f in "$GENERIC_RULES"/*.md; do
+ [ -f "$f" ] || continue
+ fix "$f" "$PROJECT/.claude/rules/$(basename "$f")"
+ done
+ fi
+ for f in "$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)
+ # Language bundles also own hooks, githooks, and settings; teams don't.
+ if [ "$kind" = language ]; then
+ if [ -d "$src/claude/hooks" ]; then
+ while IFS= read -r f; do
+ rel="${f#"$src"/claude/}"
+ fix "$f" "$PROJECT/.claude/$rel" exec
+ done < <(find "$src/claude/hooks" -type f)
+ fi
+ if [ -d "$src/githooks" ]; then
+ while IFS= read -r f; do
+ rel="${f#"$src"/githooks/}"
+ fix "$f" "$PROJECT/githooks/$rel" exec
+ done < <(find "$src/githooks" -type f)
+ fi
+ surface "$src/claude/settings.json" "$PROJECT/.claude/settings.json"
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'
+ if [ "$kind" = team ]; then
+ OUT+=" → reconcile: make install-team TEAM=$name PROJECT=."$'\n'
+ else
+ OUT+=" → reconcile: make install-$name PROJECT=."$'\n'
+ fi
fi
-done
+}
+
+if [ -d "$LANG_ROOT" ]; then
+ for d in "$LANG_ROOT"/*/; do
+ [ -d "$d" ] && process_bundle language "$d"
+ done
+fi
+if [ -d "$TEAM_ROOT" ]; then
+ for d in "$TEAM_ROOT"/*/; do
+ [ -d "$d" ] && process_bundle team "$d"
+ done
+fi
[ -n "$OUT" ] && printf '%s' "$OUT"
[ "$MANUAL" -gt 0 ] && exit 3