claude-code-checkpoint
Version:
Automatic project snapshots for Claude Code - never lose your work again
298 lines (247 loc) • 8.7 kB
JavaScript
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
`;
}