#!/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/(?/dev/null || true if [ "$old_us" != "$old_stem" ]; then PERL_OLD="$old_us" PERL_NEW="$new_us" perl -i -pe \ 's/(?/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