aboutsummaryrefslogtreecommitdiff
path: root/scripts/sweep-gitignore-tooling.sh
blob: 63fc06601735df02c07f0704fe3ecdd15229cead (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
#!/usr/bin/env bash
# sweep-gitignore-tooling.sh — backfill the personal-tooling gitignore set
# across existing gitignore-mode AI projects.
#
# install-ai.sh sets up the ignore set at bootstrap, but a project bootstrapped
# before the set grew (or one that only ever ignored .ai/) is left exposed — an
# accidental `git add` or a /codify run can commit a personal CLAUDE.md, the
# private rule copies under .claude/, or an AGENTS.md. This sweep backfills the
# full set into every gitignore-mode project that's behind.
#
# For each AI project (a directory with .ai/protocols.org) under the search
# roots, if it's a git checkout in gitignore mode (.ai/ already appears in its
# .gitignore), ensure .ai/, .claude/, CLAUDE.md, and AGENTS.md are all ignored.
# Append only the missing lines, so a re-run is a no-op.
#
# Track-mode projects (.ai/ NOT in .gitignore) are skipped by design: they
# track their tooling on purpose — team repos sharing config with teammates who
# don't run rulesets, or private-remote personal repos where the history IS the
# project.
#
# A line added here only stops *future* commits. If a target path is already
# tracked, the ignore has no effect until it's untracked; the sweep warns so
# the path can be `git rm --cached`-ed by hand.
#
# Usage: sweep-gitignore-tooling.sh [--dry-run] [SEARCH_ROOT ...]
#   --dry-run        Report what would change without writing.
#   SEARCH_ROOT ...  Directories to scan (default: ~/code ~/projects ~/.emacs.d).

set -euo pipefail

IGNORE_SET=('.ai/' '.claude/' 'CLAUDE.md' 'AGENTS.md')

dry_run=0
roots=()
for arg in "$@"; do
  case "$arg" in
    --dry-run) dry_run=1 ;;
    -h|--help)
      sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'
      exit 0
      ;;
    -*)
      echo "ERROR: unknown flag: $arg (use --help for usage)" >&2
      exit 2
      ;;
    *) roots+=("$arg") ;;
  esac
done

if [ "${#roots[@]}" -eq 0 ]; then
  roots=("$HOME/code" "$HOME/projects" "$HOME/.emacs.d")
fi

# Discover AI projects: a directory holding .ai/protocols.org.
mapfile -t projects < <(
  for root in "${roots[@]}"; do
    [ -d "$root" ] || continue
    find "$root" -maxdepth 3 -type f -path '*/.ai/protocols.org' 2>/dev/null \
      | sed 's|/\.ai/protocols\.org$||'
  done | sort -u
)

swept=0 skipped=0 complete=0

for project in "${projects[@]}"; do
  name="$(basename "$project")"
  gi="$project/.gitignore"

  if [ ! -d "$project/.git" ]; then
    echo "skip  $name — not a git checkout"
    skipped=$((skipped + 1))
    continue
  fi

  # Gitignore mode iff .ai/ is already ignored. Otherwise track-mode: leave it.
  if [ ! -f "$gi" ] || ! grep -qFx '.ai/' "$gi"; then
    echo "skip  $name — track-mode (.ai/ not gitignored)"
    skipped=$((skipped + 1))
    continue
  fi

  needed=()
  for pat in "${IGNORE_SET[@]}"; do
    grep -qFx "$pat" "$gi" || needed+=("$pat")
  done

  if [ "${#needed[@]}" -eq 0 ]; then
    echo "ok    $name — already complete"
    complete=$((complete + 1))
    continue
  fi

  if [ "$dry_run" -eq 1 ]; then
    echo "DRY   $name — would add: ${needed[*]}"
  else
    {
      echo ""
      echo "# Claude Code per-project tooling (swept $(date +%Y-%m-%d))"
      printf '%s\n' "${needed[@]}"
    } >> "$gi"
    echo "swept $name — added: ${needed[*]}"
  fi
  swept=$((swept + 1))

  # Warn on any newly-ignored path that's already tracked — the ignore won't
  # untrack it.
  for pat in "${needed[@]}"; do
    path="${pat%/}"
    if git -C "$project" ls-files --error-unmatch "$path" >/dev/null 2>&1; then
      echo "  WARN  $name: $path is currently tracked — 'git -C $project rm --cached -r $path' to untrack"
    fi
  done
done

echo
echo "Summary: $swept swept, $complete already complete, $skipped skipped (of ${#projects[@]} projects)."
[ "$dry_run" -eq 1 ] && echo "(dry-run — no files written)"
exit 0