UNPKG

claude-code-checkpoint

Version:

Automatic project snapshots for Claude Code - never lose your work again

298 lines (247 loc) 8.7 kB
export function generateCheckpointCommand() { return `#!/bin/bash # Claude Code Checkpoint Command # Generated by claude-code-checkpoint CHECKPOINT_BASE_DIR="$HOME/.claude/checkpoint" DATA_DIR="$CHECKPOINT_BASE_DIR/data" DEBUG_LOG="$HOME/.claude/checkpoint-debug.log" # Colors RED='\\033[0;31m' GREEN='\\033[0;32m' YELLOW='\\033[1;33m' BLUE='\\033[0;34m' NC='\\033[0m' # No Color # Function to print colored output print_color() { echo -e "$1$2$NC" } # Function to get project name get_project_name() { if git rev-parse --git-dir > /dev/null 2>&1; then basename "$(git rev-parse --show-toplevel)" else echo "" fi } # Function to check if in git repo require_git_repo() { if ! git rev-parse --git-dir > /dev/null 2>&1; then print_color "$RED" "Error: Not in a git repository" exit 1 fi } # List checkpoints list_checkpoints() { require_git_repo PROJECT_NAME=$(get_project_name) METADATA_FILE="$DATA_DIR/$PROJECT_NAME/metadata.json" if [ ! -f "$METADATA_FILE" ]; then print_color "$YELLOW" "No checkpoints found for this project" return fi print_color "$BLUE" "\\nCheckpoints for $PROJECT_NAME:\\n" FIRST_LINE=true jq -r '.checkpoints | sort_by(.timestamp) | reverse | .[] | "\\(.id). \\(.description) (\\(.timestamp | split("T")[0]) at \\(.timestamp | split("T")[1] | split("Z")[0]))"' "$METADATA_FILE" | while IFS= read -r line; do if [ "$FIRST_LINE" = true ]; then print_color "$GREEN" "$line [LATEST]" FIRST_LINE=false else echo "$line" fi done } # Restore checkpoint restore_checkpoint() { require_git_repo PROJECT_NAME=$(get_project_name) PROJECT_ROOT=$(git rev-parse --show-toplevel) METADATA_FILE="$DATA_DIR/$PROJECT_NAME/metadata.json" if [ ! -f "$METADATA_FILE" ]; then print_color "$RED" "No checkpoints found for this project" exit 1 fi # Parse checkpoint ID CHECKPOINT_ID="$1" if [ "$CHECKPOINT_ID" = "last" ] || [ "$CHECKPOINT_ID" = "latest" ]; then CHECKPOINT_ID=$(jq -r '.checkpoints | sort_by(.timestamp) | reverse | .[0].id' "$METADATA_FILE") elif [ "$CHECKPOINT_ID" = "previous" ]; then CHECKPOINT_ID=$(jq -r '.checkpoints | sort_by(.timestamp) | reverse | .[1].id' "$METADATA_FILE") fi # Get checkpoint path CHECKPOINT_PATH=$(jq -r ".checkpoints[] | select(.id == $CHECKPOINT_ID) | .path" "$METADATA_FILE") if [ -z "$CHECKPOINT_PATH" ] || [ ! -d "$CHECKPOINT_PATH" ]; then print_color "$RED" "Checkpoint #$CHECKPOINT_ID not found" exit 1 fi # Get checkpoint description DESCRIPTION=$(jq -r ".checkpoints[] | select(.id == $CHECKPOINT_ID) | .description" "$METADATA_FILE") # Confirm restore print_color "$YELLOW" "\\nAbout to restore to checkpoint #$CHECKPOINT_ID: $DESCRIPTION" print_color "$YELLOW" "This will overwrite your current files!" echo -n "Continue? (y/N) " read -r response if [[ ! "$response" =~ ^[Yy]$ ]]; then print_color "$RED" "Restore cancelled" exit 0 fi # Create backup of current state print_color "$BLUE" "Creating backup of current state..." BACKUP_ID="backup-$(date +%s)" BACKUP_DIR="$DATA_DIR/$PROJECT_NAME/$BACKUP_ID" mkdir -p "$BACKUP_DIR" rsync -a \\ --exclude='.git' \\ --exclude='node_modules' \\ --exclude='dist' \\ --exclude='build' \\ --exclude='.next' \\ "$PROJECT_ROOT/" "$BACKUP_DIR/" # Restore checkpoint print_color "$BLUE" "Restoring checkpoint #$CHECKPOINT_ID..." # Remove current files (except .git and excluded) find "$PROJECT_ROOT" -mindepth 1 -maxdepth 1 \\ -not -name '.git' \\ -not -name 'node_modules' \\ -exec rm -rf {} + # Copy checkpoint files rsync -a "$CHECKPOINT_PATH/" "$PROJECT_ROOT/" print_color "$GREEN" "✓ Successfully restored to checkpoint #$CHECKPOINT_ID" print_color "$YELLOW" "Backup saved as: $BACKUP_ID" } # Create manual checkpoint create_checkpoint() { require_git_repo # Trigger the checkpoint hook manually DESCRIPTION="$1" if [ -z "$DESCRIPTION" ]; then DESCRIPTION="Manual checkpoint" fi # Set environment variable for manual description export CHECKPOINT_MANUAL_DESC="$DESCRIPTION" # Run the checkpoint hook if [ -f "$HOME/.claude/checkpoint/hooks/checkpoint-hook.sh" ]; then bash "$HOME/.claude/checkpoint/hooks/checkpoint-hook.sh" print_color "$GREEN" "✓ Checkpoint created: $DESCRIPTION" else print_color "$RED" "Error: Checkpoint hook not found" exit 1 fi } # Show checkpoint diff diff_checkpoints() { require_git_repo PROJECT_NAME=$(get_project_name) METADATA_FILE="$DATA_DIR/$PROJECT_NAME/metadata.json" if [ ! -f "$METADATA_FILE" ]; then print_color "$RED" "No checkpoints found for this project" exit 1 fi ID1="$1" ID2="$2" PATH1=$(jq -r ".checkpoints[] | select(.id == $ID1) | .path" "$METADATA_FILE") PATH2=$(jq -r ".checkpoints[] | select(.id == $ID2) | .path" "$METADATA_FILE") if [ -z "$PATH1" ] || [ -z "$PATH2" ]; then print_color "$RED" "One or both checkpoint IDs not found" exit 1 fi print_color "$BLUE" "\\nDifferences between checkpoint #$ID1 and #$ID2:\\n" # Use diff to compare diff -r -u --exclude='.git' --exclude='node_modules' "$PATH1" "$PATH2" | head -100 echo "\\n(Showing first 100 lines)" } # Show checkpoint details show_checkpoint() { require_git_repo PROJECT_NAME=$(get_project_name) METADATA_FILE="$DATA_DIR/$PROJECT_NAME/metadata.json" if [ ! -f "$METADATA_FILE" ]; then print_color "$RED" "No checkpoints found for this project" exit 1 fi CHECKPOINT_ID="$1" # Get checkpoint details CHECKPOINT=$(jq ".checkpoints[] | select(.id == $CHECKPOINT_ID)" "$METADATA_FILE") if [ -z "$CHECKPOINT" ]; then print_color "$RED" "Checkpoint #$CHECKPOINT_ID not found" exit 1 fi print_color "$BLUE" "\\nCheckpoint #$CHECKPOINT_ID Details:\\n" echo "$CHECKPOINT" | jq . # Show file count CHECKPOINT_PATH=$(echo "$CHECKPOINT" | jq -r '.path') if [ -d "$CHECKPOINT_PATH" ]; then FILE_COUNT=$(find "$CHECKPOINT_PATH" -type f | wc -l | tr -d ' ') print_color "$GREEN" "\\nFiles in checkpoint: $FILE_COUNT" fi } # Clear all checkpoints clear_checkpoints() { require_git_repo PROJECT_NAME=$(get_project_name) PROJECT_DATA_DIR="$DATA_DIR/$PROJECT_NAME" if [ ! -d "$PROJECT_DATA_DIR" ]; then print_color "$YELLOW" "No checkpoints to clear" return fi # Count checkpoints if [ -f "$PROJECT_DATA_DIR/metadata.json" ]; then COUNT=$(jq '.checkpoints | length' "$PROJECT_DATA_DIR/metadata.json") else COUNT=0 fi print_color "$YELLOW" "\\nAbout to delete $COUNT checkpoints for $PROJECT_NAME" echo -n "This cannot be undone. Continue? (y/N) " read -r response if [[ ! "$response" =~ ^[Yy]$ ]]; then print_color "$RED" "Clear cancelled" exit 0 fi # Remove project data directory rm -rf "$PROJECT_DATA_DIR" print_color "$GREEN" "✓ All checkpoints cleared for $PROJECT_NAME" } # Main command handler case "$1" in list|ls) list_checkpoints ;; restore) restore_checkpoint "$2" ;; create) create_checkpoint "$2" ;; diff) diff_checkpoints "$2" "$3" ;; show) show_checkpoint "$2" ;; clear) clear_checkpoints ;; *) echo "Claude Code Checkpoint System" echo "" echo "Usage: checkpoint <command> [options]" echo "" echo "Commands:" echo " list List all checkpoints" echo " restore <id> Restore to checkpoint (id/last/previous)" echo " create [desc] Create manual checkpoint" echo " diff <id1> <id2> Compare two checkpoints" echo " show <id> Show checkpoint details" echo " clear Remove all checkpoints" echo "" echo "Examples:" echo " checkpoint list" echo " checkpoint restore 5" echo " checkpoint restore last" echo " checkpoint create 'Before refactor'" echo " checkpoint diff 3 5" ;; esac `; }