diff options
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/install-team.sh | 49 | ||||
| -rwxr-xr-x | scripts/sync-language-bundle.sh | 123 | ||||
| -rw-r--r-- | scripts/tests/sync-language-bundle.bats | 39 |
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" ] +} |
