aboutsummaryrefslogtreecommitdiff
path: root/scripts/rename-ai-artifact.sh
blob: 9af6326da4bbd2ac3ad7d1bc5c48b0ee5039d897 (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
#!/usr/bin/env bash
# Rename an .ai artifact (a workflow or a script) across the canonical + mirror
# trees, rewriting every reference to it and leaving archived session records
# alone. Encodes the rename discipline so it isn't re-derived by hand each time:
#
#   - canonical (claude-templates/.ai/) and mirror (.ai/) move in lockstep
#   - every reference to the artifact's stem is rewritten, repo-wide
#   - .ai/sessions/ (both trees) is history — never edited
#   - references match on a token boundary, so renaming `foo` can't corrupt
#     `foobar` or `foo-bar`
#   - the underscore module-name variant is rewritten too (a hyphenated script
#     imported as `foo_bar` via importlib), not just the hyphenated path
#   - workflow-integrity.py + sync-check.sh run at the end to prove no drift
#
# Usage: rename-ai-artifact.sh OLD-BASENAME NEW-BASENAME
#   e.g. rename-ai-artifact.sh old-workflow.org new-workflow.org
#        rename-ai-artifact.sh old-helper.py    new-helper.py
#
# Renames ONE artifact per call; run it once per file in a family. Order within
# a family doesn't matter — token-boundary matching keeps shared prefixes apart.
#
# Exit: 0 renamed (verify may still warn); 1 usage / not-found / target-exists.

set -euo pipefail

OLD="${1:-}"
NEW="${2:-}"

if [ -z "$OLD" ] || [ -z "$NEW" ]; then
  echo "usage: rename-ai-artifact.sh OLD-BASENAME NEW-BASENAME" >&2
  exit 1
fi
if [ "$OLD" = "$NEW" ]; then
  echo "rename: OLD and NEW are identical ($OLD)" >&2
  exit 1
fi

REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -z "$REPO_ROOT" ]; then
  echo "rename: not inside a git repository" >&2
  exit 1
fi
CANON="$REPO_ROOT/claude-templates/.ai"
MIRROR="$REPO_ROOT/.ai"

# Locate OLD under the canonical tree (the source of truth). Exactly one match.
mapfile -t hits < <(find "$CANON" -type f -name "$OLD" 2>/dev/null)
if [ "${#hits[@]}" -eq 0 ]; then
  echo "rename: artifact not found under canonical tree: $OLD" >&2
  exit 1
fi
if [ "${#hits[@]}" -gt 1 ]; then
  echo "rename: $OLD is ambiguous (${#hits[@]} matches under $CANON):" >&2
  printf '  %s\n' "${hits[@]}" >&2
  exit 1
fi
canon_old="${hits[0]}"
relpath="${canon_old#"$CANON"/}"                 # e.g. workflows/foo.org
reldir="$(dirname "$relpath")"
canon_new="$CANON/$reldir/$NEW"
mirror_old="$MIRROR/$relpath"
mirror_new="$MIRROR/$reldir/$NEW"

if [ ! -f "$mirror_old" ]; then
  echo "rename: canonical has $relpath but the mirror copy is missing: $mirror_old" >&2
  exit 1
fi
for n in "$canon_new" "$mirror_new"; do
  if [ -e "$n" ]; then
    echo "rename: target already exists: $n" >&2
    exit 1
  fi
done

# Stems drive reference rewriting: foo.org -> foo, foo-helper.py -> foo-helper,
# a no-extension script keeps its whole name. The stem replacement also covers
# the extensioned form (foo.org), since the "." after the stem is a boundary.
old_stem="${OLD%.*}"
new_stem="${NEW%.*}"
# Python imports a hyphenated script under an underscored module name
# (importlib.spec_from_file_location("foo_bar", "foo-bar.py")). Rewrite that
# variant too so the module-name reference isn't left behind.
old_us="${old_stem//-/_}"
new_us="${new_stem//-/_}"

echo "rename: $relpath"
echo "  $OLD  ->  $NEW   (stem: $old_stem -> $new_stem)"

git -C "$REPO_ROOT" mv "$canon_old" "$canon_new"
git -C "$REPO_ROOT" mv "$mirror_old" "$mirror_new"
echo "  moved in canonical and mirror"

# Rewrite references repo-wide across tracked files, except archived session
# records (history) and the two files just renamed (already carry the new name
# in their path; their bodies get the same stem rewrite as everything else).
rewritten=0
while IFS= read -r f; do
  case "$f" in
    */sessions/*) continue ;;                    # history — never edit
  esac
  # Token-boundary replace: not preceded/followed by an identifier char or '-'.
  # Rewrite the hyphenated stem, then the underscored variant when it differs.
  PERL_OLD="$old_stem" PERL_NEW="$new_stem" perl -i -pe \
    's/(?<![A-Za-z0-9_-])\Q$ENV{PERL_OLD}\E(?![A-Za-z0-9_-])/$ENV{PERL_NEW}/g' "$f" 2>/dev/null || true
  if [ "$old_us" != "$old_stem" ]; then
    PERL_OLD="$old_us" PERL_NEW="$new_us" perl -i -pe \
      's/(?<![A-Za-z0-9_-])\Q$ENV{PERL_OLD}\E(?![A-Za-z0-9_-])/$ENV{PERL_NEW}/g' "$f" 2>/dev/null || true
  fi
done < <(git -C "$REPO_ROOT" grep -lI --untracked -e "$old_stem" -e "$old_us" 2>/dev/null || true)

# Count files that still changed (git sees them as modified).
rewritten="$(git -C "$REPO_ROOT" status --porcelain | grep -c '^ *M' || true)"
echo "  rewrote references (${rewritten} file(s) modified, sessions/ left as history)"

# Verify — best-effort; skips cleanly when the checkers aren't present (tests).
status=0
if [ -f "$REPO_ROOT/scripts/workflow-integrity.py" ]; then
  echo "  verify: workflow-integrity"
  python3 "$REPO_ROOT/scripts/workflow-integrity.py" || status=$?
fi
if [ -f "$REPO_ROOT/scripts/sync-check.sh" ]; then
  echo "  verify: sync-check"
  bash "$REPO_ROOT/scripts/sync-check.sh" || status=$?
fi
if [ "$status" -ne 0 ]; then
  echo "rename: done, but a verify step reported drift — review before committing." >&2
fi
echo "rename: done."
exit 0