|
#!/usr/bin/env bash |
|
set -euo pipefail |
|
|
|
CLAUDE_DIR="${HOME}/.claude" |
|
|
|
usage() { |
|
echo "Usage: claude-rename <source> <dest>" |
|
echo "" |
|
echo "Rename a project folder and update Claude Code sessions to follow." |
|
echo "" |
|
echo "Arguments:" |
|
echo " source Current project folder path" |
|
echo " dest New project folder path" |
|
echo "" |
|
echo "Examples:" |
|
echo " claude-rename ~/projects/old-name ~/projects/new-name" |
|
echo " claude-rename /abs/path/old /abs/path/new" |
|
exit 1 |
|
} |
|
|
|
# --- helpers --- |
|
|
|
# Convert absolute path to Claude's encoded folder name (slashes → dashes) |
|
encode_path() { |
|
echo "$1" | sed 's|/|-|g' |
|
} |
|
|
|
log() { |
|
echo "→ $*" |
|
} |
|
|
|
warn() { |
|
echo "⚠ $*" >&2 |
|
} |
|
|
|
die() { |
|
echo "✗ $*" >&2 |
|
exit 1 |
|
} |
|
|
|
# Resolve to absolute path (works on both macOS and Linux, no python) |
|
resolve_path() { |
|
local target="$1" |
|
|
|
# Expand ~ manually |
|
if [[ "$target" == "~"* ]]; then |
|
target="${HOME}${target#"~"}" |
|
fi |
|
|
|
# If relative, prepend pwd |
|
if [[ "$target" != /* ]]; then |
|
target="$(pwd)/$target" |
|
fi |
|
|
|
# Normalize . and .. using pure bash |
|
# Split on /, rebuild skipping . and resolving .. |
|
local IFS='/' parts=() normalized=() |
|
read -ra parts <<< "$target" |
|
for part in "${parts[@]}"; do |
|
case "$part" in |
|
''|'.') ;; |
|
'..') [[ ${#normalized[@]} -gt 0 ]] && unset 'normalized[${#normalized[@]}-1]' ;; |
|
*) normalized+=("$part") ;; |
|
esac |
|
done |
|
|
|
local result="" |
|
for part in "${normalized[@]}"; do |
|
result="${result}/${part}" |
|
done |
|
echo "${result:-/}" |
|
} |
|
|
|
# --- main --- |
|
|
|
[[ $# -ne 2 ]] && usage |
|
|
|
src_raw="$1" |
|
dst_raw="$2" |
|
|
|
src="$(resolve_path "$src_raw")" |
|
dst="$(resolve_path "$dst_raw")" |
|
|
|
log "Source: $src" |
|
log "Dest: $dst" |
|
|
|
# Validate source exists |
|
[[ -d "$src" ]] || die "Source directory does not exist: $src" |
|
|
|
# Validate dest does not exist |
|
[[ -e "$dst" ]] && die "Destination already exists: $dst" |
|
|
|
# Validate dest parent exists |
|
dst_parent="$(dirname "$dst")" |
|
[[ -d "$dst_parent" ]] || die "Destination parent directory does not exist: $dst_parent" |
|
|
|
# Encode paths for Claude's project directory names |
|
src_encoded="$(encode_path "$src")" |
|
dst_encoded="$(encode_path "$dst")" |
|
|
|
log "Encoded source: $src_encoded" |
|
log "Encoded dest: $dst_encoded" |
|
|
|
# 1. Rename the actual folder |
|
log "Renaming folder: $src → $dst" |
|
mv "$src" "$dst" |
|
log "Folder renamed." |
|
|
|
# 2. Rename Claude's project directory if it exists |
|
projects_dir="${CLAUDE_DIR}/projects" |
|
if [[ -d "${projects_dir}/${src_encoded}" ]]; then |
|
log "Renaming Claude project dir: ${src_encoded} → ${dst_encoded}" |
|
mv "${projects_dir}/${src_encoded}" "${projects_dir}/${dst_encoded}" |
|
log "Claude project dir renamed." |
|
else |
|
warn "No Claude project directory found for: ${src_encoded} (skipping)" |
|
fi |
|
|
|
# 3. Update history.jsonl - replace project path references |
|
history_file="${CLAUDE_DIR}/history.jsonl" |
|
if [[ -f "$history_file" ]]; then |
|
count=$(grep -c "\"$src\"" "$history_file" 2>/dev/null || true) |
|
if [[ "$count" -gt 0 ]]; then |
|
log "Updating history.jsonl ($count entries)..." |
|
|
|
# Use a temp file for atomic replacement |
|
tmp_file="$(mktemp)" |
|
|
|
# Escape paths for sed (handle slashes) |
|
src_escaped="${src//\//\\/}" |
|
dst_escaped="${dst//\//\\/}" |
|
|
|
sed "s|\"project\":\"${src}\"|\"project\":\"${dst}\"|g" "$history_file" > "$tmp_file" |
|
mv "$tmp_file" "$history_file" |
|
log "history.jsonl updated." |
|
else |
|
log "No matching entries in history.jsonl (skipping)." |
|
fi |
|
else |
|
warn "history.jsonl not found (skipping)." |
|
fi |
|
|
|
echo "" |
|
echo "Done. Claude sessions for '$src' now point to '$dst'." |
This script fails if the source directory contains dots.
Claude encodes these with dashes as well.
Replace
encode_pathwithNot sure, whether there are more special characters that require such treatment.