aboutsummaryrefslogtreecommitdiff
path: root/scripts/sync-language-bundle.sh
blob: 8858f749f32569cb2cbd03e0db2c05229ea5a369 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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