aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/install-team.sh49
-rwxr-xr-xscripts/sync-language-bundle.sh123
-rw-r--r--scripts/tests/sync-language-bundle.bats39
3 files changed, 161 insertions, 50 deletions
diff --git a/scripts/install-team.sh b/scripts/install-team.sh
new file mode 100755
index 0000000..0e6e129
--- /dev/null
+++ b/scripts/install-team.sh
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+# Install a team publishing overlay into a single target project.
+# Usage: install-team.sh <team> <project-path>
+#
+# Copies the team overlay's rule file(s) into the project's .claude/rules/.
+# A team overlay carries only its own rules — no generic rules, hooks,
+# githooks, or settings (those belong to the global install or a language
+# bundle). Re-runnable; the authoritative source overwrites.
+#
+# The overlay is NOT a global rule: it lands in one project's .claude/rules/
+# and loads only there. The companion sync-language-bundle.sh keeps it fresh
+# at that project's startup.
+
+set -euo pipefail
+
+TEAM="${1:-}"
+PROJECT="${2:-}"
+
+if [ -z "$TEAM" ] || [ -z "$PROJECT" ]; then
+ echo "Usage: $0 <team> <project-path>" >&2
+ exit 1
+fi
+
+REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+SRC="$REPO_ROOT/teams/$TEAM"
+
+if [ ! -d "$SRC/claude/rules" ]; then
+ echo "ERROR: no team overlay rules for '$TEAM' (expected $SRC/claude/rules/)" >&2
+ exit 1
+fi
+
+if [ ! -d "$PROJECT" ]; then
+ echo "ERROR: project path does not exist: $PROJECT" >&2
+ exit 1
+fi
+PROJECT="$(cd "$PROJECT" && pwd)"
+
+echo "Installing team overlay '$TEAM' into $PROJECT"
+
+mkdir -p "$PROJECT/.claude/rules"
+count=0
+for f in "$SRC/claude/rules"/*.md; do
+ [ -f "$f" ] || continue
+ cp "$f" "$PROJECT/.claude/rules/$(basename "$f")"
+ count=$((count + 1))
+done
+
+echo " [ok] .claude/rules/ — $count overlay rule(s) from teams/$TEAM/"
+echo "Loads only in this project. Startup keeps it synced via sync-language-bundle.sh."
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
diff --git a/scripts/tests/sync-language-bundle.bats b/scripts/tests/sync-language-bundle.bats
index 9fd1108..e641646 100644
--- a/scripts/tests/sync-language-bundle.bats
+++ b/scripts/tests/sync-language-bundle.bats
@@ -44,6 +44,14 @@ matches_canonical() { # project-relative-path canonical-abs-path
diff -q "$PROJ/$1" "$2" >/dev/null 2>&1
}
+# Mirror install-team.sh: copy only the team overlay's own rule files into a
+# synthetic project (no generic rules, hooks, githooks, or settings).
+install_team_overlay() {
+ local team="$1" proj="$2"
+ mkdir -p "$proj/.claude/rules"
+ cp "$REAL_REPO/teams/$team/claude/rules/"*.md "$proj/.claude/rules/"
+}
+
# --- Normal: no bundle / clean ---
@test "sync: project with no bundle is a quiet no-op (exit 0)" {
@@ -151,3 +159,34 @@ matches_canonical() { # project-relative-path canonical-abs-path
[ "$status" -eq 1 ]
[[ "$output" == *"does not exist"* ]]
}
+
+# --- Team overlays ---
+
+@test "sync: clean team overlay is a quiet no-op (exit 0)" {
+ install_team_overlay deepsat "$PROJ"
+ run bash "$SCRIPT" "$PROJ"
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+}
+
+@test "sync: drifted team-overlay rule is auto-fixed and restored" {
+ install_team_overlay deepsat "$PROJ"
+ echo "junk drift" >> "$PROJ/.claude/rules/publishing.md"
+ run bash "$SCRIPT" "$PROJ"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"team bundle 'deepsat'"* ]]
+ [[ "$output" == *"publishing.md"* ]]
+ matches_canonical ".claude/rules/publishing.md" "$REAL_REPO/teams/deepsat/claude/rules/publishing.md"
+}
+
+@test "sync: team overlay does NOT pull in generic claude-rules" {
+ install_team_overlay deepsat "$PROJ" # only publishing.md present, no commits.md
+ echo "junk drift" >> "$PROJ/.claude/rules/publishing.md"
+ run bash "$SCRIPT" "$PROJ"
+ [ "$status" -eq 0 ]
+ # A team overlay owns only its own rule; the sync must not copy generic
+ # rules (commits.md, testing.md, ...) into the project the way a language
+ # bundle does.
+ [ ! -f "$PROJ/.claude/rules/commits.md" ]
+ [ ! -f "$PROJ/.claude/rules/testing.md" ]
+}