rename-cursor-plans.sh
1 #!/usr/bin/env bash 2 # Rename .cursor/plans/*.plan.md to convention: YYYY-MM-DD_<username>_<slug>.plan.md 3 # Usage: scripts/rename-cursor-plans.sh [--dry-run] 4 # Used by pre-commit to keep plan filenames consistent. 5 6 set -e 7 DRY_RUN=false 8 [ "${1:-}" = "--dry-run" ] && { 9 DRY_RUN=true 10 shift 11 } 12 if [ -n "${GIT_DIR}" ]; then 13 ROOT="$(cd "$(dirname "$GIT_DIR")" && pwd)" 14 else 15 ROOT="$(cd "$(dirname "$0")/.." && pwd)" 16 fi 17 PLANS_DIR="$ROOT/.cursor/plans" 18 19 # Valid name: YYYY-MM-DD_<username>_<slug>.plan.md (exactly two underscores, three segments) 20 valid_name() { 21 local base="$1" 22 case "$base" in 23 README.md) return 0 ;; 24 [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]_*_*.plan.md) return 0 ;; 25 *) return 1 ;; 26 esac 27 } 28 29 # Get default username: env CURSOR_PLAN_USER, or git user.name (lowercase, spaces -> dots) 30 get_username() { 31 if [ -n "${CURSOR_PLAN_USER:-}" ]; then 32 echo "$CURSOR_PLAN_USER" 33 return 34 fi 35 local name 36 name=$(git config user.name 2>/dev/null || echo "dotfiles") 37 echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/ */./g' | tr -cd 'a-z0-9._-' 38 } 39 40 # Derive slug from basename: strip .plan.md, strip leading YYYY-MM-DD_, remove UUID, normalize 41 to_slug() { 42 local base="$1" 43 base="${base%.plan.md}" 44 base="${base%.plan}" 45 # Strip leading date so "2026-02-27_nvim-improvements" -> "nvim-improvements" 46 base=$(echo "$base" | sed -E 's/^[0-9]{4}-[0-9]{2}-[0-9]{2}_//') 47 # Remove UUID-like suffix (e.g. -3e679162) 48 base=$(echo "$base" | sed -E 's/-[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$//i') 49 base=$(echo "$base" | sed -E 's/-[0-9a-f]{8}$//i') 50 # Lowercase, non-alphanumeric -> hyphen 51 base=$(echo "$base" | tr '[:upper:]' '[:lower:]' | tr -s ' _.' '---' | sed 's/[^a-z0-9-]/-/g' | sed 's/^-//;s/-$//') 52 [ -z "$base" ] && base="plan" 53 echo "$base" 54 } 55 56 # Use date from basename (YYYY-MM-DD_) if present, else today 57 get_date() { 58 local base="$1" 59 if echo "$base" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}_'; then 60 echo "$base" | sed -E 's/^([0-9]{4}-[0-9]{2}-[0-9]{2})_.*/\1/' 61 else 62 date +%Y-%m-%d 63 fi 64 } 65 66 renamed=0 67 username=$(get_username) 68 69 for f in "$PLANS_DIR"/*.plan.md; do 70 [ -e "$f" ] || continue 71 base=$(basename "$f") 72 # Skip README and already-valid names 73 if [ "$base" = "README.md" ]; then 74 continue 75 fi 76 if valid_name "$base"; then 77 continue 78 fi 79 slug=$(to_slug "$base") 80 plan_date=$(get_date "$base") 81 new_name="${plan_date}_${username}_${slug}.plan.md" 82 new_path="$PLANS_DIR/$new_name" 83 if [ -e "$new_path" ] && [ "$(realpath "$f" 2>/dev/null || echo "$f")" != "$(realpath "$new_path" 2>/dev/null || echo "$new_path")" ]; then 84 echo "rename-cursor-plans: skip $base (target exists: $new_name)" 1>&2 85 continue 86 fi 87 if [ "$DRY_RUN" = true ]; then 88 echo "Would rename: $base -> $new_name" 89 else 90 old_path="$f" 91 mv "$f" "$new_path" 92 echo "Renamed: $base -> $new_name" 93 if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then 94 git add -- "$new_path" 2>/dev/null || true 95 git add -- "$old_path" 2>/dev/null || true 96 fi 97 fi 98 renamed=$((renamed + 1)) 99 done 100 101 [ "$DRY_RUN" = true ] && [ "$renamed" -gt 0 ] && echo "Dry run: $renamed file(s) would be renamed." 102 exit 0