/ scripts / rename-cursor-plans.sh
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