Skip to content

Commit fa01f3c

Browse files
MrFlounderclaude
andcommitted
feat: add time travel - automatic snapshots and workspace rewind
- `crab rewind <N>` lists snapshots with relative timestamps - `crab rewind <N> 3` restores 3rd most recent snapshot - `crab rewind <N> 2h` restores from ~2 hours ago - `crab rewind <N> save` creates manual snapshot - `crab snapshot` quick snapshot from current workspace - Auto-saves current state before rewind (undo-able) - Keeps last 50 snapshots per workspace Enables "what did my code look like at 3pm?" workflow. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0c41a14 commit fa01f3c

1 file changed

Lines changed: 327 additions & 0 deletions

File tree

src/crabcode

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4833,6 +4833,310 @@ handle_handoff_command() {
48334833
esac
48344834
}
48354835

4836+
# =============================================================================
4837+
# Time Travel - Automatic snapshots and workspace rewind
4838+
# =============================================================================
4839+
4840+
SNAPSHOT_DIR="$CONFIG_DIR/snapshots"
4841+
SNAPSHOT_INTERVAL="${CRAB_SNAPSHOT_INTERVAL:-900}" # Default: 15 minutes
4842+
4843+
# Create a snapshot of workspace state
4844+
create_snapshot() {
4845+
local num="$1"
4846+
local label="${2:-auto}"
4847+
local dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$num"
4848+
4849+
if [ ! -d "$dir" ]; then
4850+
return 1
4851+
fi
4852+
4853+
mkdir -p "$SNAPSHOT_DIR/ws-$num"
4854+
4855+
local timestamp=$(date +%s)
4856+
local snapshot_name="$timestamp-$label"
4857+
local snapshot_path="$SNAPSHOT_DIR/ws-$num/$snapshot_name"
4858+
4859+
mkdir -p "$snapshot_path"
4860+
4861+
cd "$dir"
4862+
4863+
# Save git state
4864+
local branch=$(git branch --show-current 2>/dev/null || echo "main")
4865+
local commit=$(git rev-parse HEAD 2>/dev/null || echo "")
4866+
local diff=$(git diff HEAD 2>/dev/null || echo "")
4867+
local staged=$(git diff --cached HEAD 2>/dev/null || echo "")
4868+
local stash_count=$(git stash list 2>/dev/null | wc -l | tr -d ' ')
4869+
4870+
echo "$branch" > "$snapshot_path/branch"
4871+
echo "$commit" > "$snapshot_path/commit"
4872+
[ -n "$diff" ] && echo "$diff" > "$snapshot_path/unstaged.patch"
4873+
[ -n "$staged" ] && echo "$staged" > "$snapshot_path/staged.patch"
4874+
4875+
# Save metadata
4876+
cat > "$snapshot_path/metadata.json" << EOF
4877+
{
4878+
"timestamp": $timestamp,
4879+
"label": "$label",
4880+
"branch": "$branch",
4881+
"commit": "$commit",
4882+
"has_unstaged": $([ -n "$diff" ] && echo "true" || echo "false"),
4883+
"has_staged": $([ -n "$staged" ] && echo "true" || echo "false"),
4884+
"stash_count": $stash_count,
4885+
"created_at": "$(date -Iseconds)"
4886+
}
4887+
EOF
4888+
4889+
# Prune old snapshots (keep last 50)
4890+
local snapshot_count=$(ls -1d "$SNAPSHOT_DIR/ws-$num"/*/ 2>/dev/null | wc -l | tr -d ' ')
4891+
if [ "$snapshot_count" -gt 50 ]; then
4892+
ls -1d "$SNAPSHOT_DIR/ws-$num"/*/ 2>/dev/null | head -n -50 | xargs rm -rf 2>/dev/null || true
4893+
fi
4894+
4895+
echo "$snapshot_name"
4896+
}
4897+
4898+
# Manual snapshot with label
4899+
save_snapshot() {
4900+
local num="$1"
4901+
local label="${2:-manual}"
4902+
4903+
echo -e "${CYAN}Creating snapshot...${NC}"
4904+
local snapshot_name=$(create_snapshot "$num" "$label")
4905+
4906+
if [ -n "$snapshot_name" ]; then
4907+
success "Snapshot saved: $label"
4908+
echo -e " ${GRAY}Restore with: crab rewind $num $snapshot_name${NC}"
4909+
else
4910+
error "Failed to create snapshot"
4911+
fi
4912+
}
4913+
4914+
# List snapshots for a workspace
4915+
list_snapshots() {
4916+
local num="$1"
4917+
local limit="${2:-20}"
4918+
local snapshot_base="$SNAPSHOT_DIR/ws-$num"
4919+
4920+
if [ ! -d "$snapshot_base" ]; then
4921+
echo -e "${YELLOW}No snapshots for workspace $num${NC}"
4922+
return
4923+
fi
4924+
4925+
echo -e "${CYAN}╭────────────────────────────────────────────────────╮${NC}"
4926+
echo -e "${CYAN}${NC} ${BOLD}🕐 Time Travel - Workspace $num${NC} ${CYAN}${NC}"
4927+
echo -e "${CYAN}╰────────────────────────────────────────────────────╯${NC}"
4928+
echo ""
4929+
4930+
local count=0
4931+
for snapshot_dir in $(ls -1dr "$snapshot_base"/*/ 2>/dev/null); do
4932+
[ -d "$snapshot_dir" ] || continue
4933+
count=$((count + 1))
4934+
[ $count -gt $limit ] && break
4935+
4936+
local snapshot_name=$(basename "$snapshot_dir")
4937+
local timestamp=$(echo "$snapshot_name" | cut -d'-' -f1)
4938+
local label=$(echo "$snapshot_name" | cut -d'-' -f2-)
4939+
4940+
# Format time relative to now
4941+
local now=$(date +%s)
4942+
local diff=$((now - timestamp))
4943+
local time_ago=""
4944+
4945+
if [ $diff -lt 60 ]; then
4946+
time_ago="${diff}s ago"
4947+
elif [ $diff -lt 3600 ]; then
4948+
time_ago="$((diff / 60))m ago"
4949+
elif [ $diff -lt 86400 ]; then
4950+
time_ago="$((diff / 3600))h ago"
4951+
else
4952+
time_ago="$((diff / 86400))d ago"
4953+
fi
4954+
4955+
# Read metadata
4956+
local branch=""
4957+
local has_changes="clean"
4958+
if [ -f "$snapshot_dir/metadata.json" ] && command_exists jq; then
4959+
branch=$(jq -r ".branch // \"\"" "$snapshot_dir/metadata.json" 2>/dev/null)
4960+
local has_unstaged=$(jq -r ".has_unstaged // false" "$snapshot_dir/metadata.json" 2>/dev/null)
4961+
local has_staged=$(jq -r ".has_staged // false" "$snapshot_dir/metadata.json" 2>/dev/null)
4962+
[ "$has_unstaged" = "true" ] || [ "$has_staged" = "true" ] && has_changes="changes"
4963+
else
4964+
[ -f "$snapshot_dir/branch" ] && branch=$(cat "$snapshot_dir/branch")
4965+
[ -f "$snapshot_dir/unstaged.patch" ] || [ -f "$snapshot_dir/staged.patch" ] && has_changes="changes"
4966+
fi
4967+
4968+
local status_icon=""
4969+
local status_color="${GREEN}"
4970+
if [ "$has_changes" = "changes" ]; then
4971+
status_icon=""
4972+
status_color="${YELLOW}"
4973+
fi
4974+
4975+
printf " ${BOLD}%2d.${NC} %-12s ${GRAY}%-10s${NC} ${status_color}%s${NC} %s\n" \
4976+
"$count" "$time_ago" "$label" "$status_icon" "$branch"
4977+
done
4978+
4979+
echo ""
4980+
echo -e "${GRAY}Usage: crab rewind $num <N> (restore snapshot #N)${NC}"
4981+
echo -e "${GRAY} crab rewind $num 1h (restore from ~1 hour ago)${NC}"
4982+
echo ""
4983+
}
4984+
4985+
# Rewind to a specific snapshot
4986+
rewind_to_snapshot() {
4987+
local num="$1"
4988+
local target="$2"
4989+
local dir="$WORKSPACE_BASE/$WORKSPACE_PREFIX-$num"
4990+
local snapshot_base="$SNAPSHOT_DIR/ws-$num"
4991+
4992+
if [ ! -d "$dir" ]; then
4993+
error "Workspace $num does not exist"
4994+
exit 1
4995+
fi
4996+
4997+
if [ ! -d "$snapshot_base" ]; then
4998+
error "No snapshots for workspace $num"
4999+
exit 1
5000+
fi
5001+
5002+
local snapshot_dir=""
5003+
5004+
# Handle different target formats
5005+
if [[ "$target" =~ ^[0-9]+$ ]]; then
5006+
# Target is a snapshot number (1, 2, 3...)
5007+
snapshot_dir=$(ls -1dr "$snapshot_base"/*/ 2>/dev/null | sed -n "${target}p")
5008+
elif [[ "$target" =~ ^[0-9]+[mhd]$ ]]; then
5009+
# Target is a time offset (30m, 2h, 1d)
5010+
local amount=${target%?}
5011+
local unit=${target: -1}
5012+
local seconds=0
5013+
5014+
case "$unit" in
5015+
"m") seconds=$((amount * 60)) ;;
5016+
"h") seconds=$((amount * 3600)) ;;
5017+
"d") seconds=$((amount * 86400)) ;;
5018+
esac
5019+
5020+
local target_time=$(($(date +%s) - seconds))
5021+
5022+
# Find snapshot closest to target time
5023+
for sdir in $(ls -1dr "$snapshot_base"/*/ 2>/dev/null); do
5024+
local snap_time=$(basename "$sdir" | cut -d'-' -f1)
5025+
if [ "$snap_time" -le "$target_time" ]; then
5026+
snapshot_dir="$sdir"
5027+
break
5028+
fi
5029+
done
5030+
else
5031+
# Target is a snapshot name
5032+
snapshot_dir="$snapshot_base/$target"
5033+
fi
5034+
5035+
if [ -z "$snapshot_dir" ] || [ ! -d "$snapshot_dir" ]; then
5036+
error "Snapshot not found: $target"
5037+
echo ""
5038+
echo "Use 'crab rewind $num' to see available snapshots"
5039+
exit 1
5040+
fi
5041+
5042+
local snapshot_name=$(basename "$snapshot_dir")
5043+
local timestamp=$(echo "$snapshot_name" | cut -d'-' -f1)
5044+
local label=$(echo "$snapshot_name" | cut -d'-' -f2-)
5045+
local time_str=$(date -r "$timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || date -d "@$timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "unknown")
5046+
5047+
echo -e "${CYAN}Rewinding workspace $num to: $label ($time_str)${NC}"
5048+
echo ""
5049+
5050+
# First, save current state as a snapshot
5051+
echo " 💾 Saving current state before rewind..."
5052+
create_snapshot "$num" "pre-rewind" >/dev/null
5053+
5054+
cd "$dir"
5055+
5056+
# Restore branch
5057+
if [ -f "$snapshot_dir/branch" ]; then
5058+
local branch=$(cat "$snapshot_dir/branch")
5059+
echo " 🔀 Switching to branch: $branch"
5060+
git checkout "$branch" 2>/dev/null || git checkout -b "$branch" 2>/dev/null || true
5061+
fi
5062+
5063+
# Restore commit
5064+
if [ -f "$snapshot_dir/commit" ]; then
5065+
local commit=$(cat "$snapshot_dir/commit")
5066+
echo " 📌 Resetting to commit: ${commit:0:8}"
5067+
git reset --hard "$commit" 2>/dev/null || true
5068+
fi
5069+
5070+
# Apply patches
5071+
if [ -f "$snapshot_dir/staged.patch" ] && [ -s "$snapshot_dir/staged.patch" ]; then
5072+
echo " 📄 Applying staged changes..."
5073+
git apply --index "$snapshot_dir/staged.patch" 2>/dev/null || true
5074+
fi
5075+
5076+
if [ -f "$snapshot_dir/unstaged.patch" ] && [ -s "$snapshot_dir/unstaged.patch" ]; then
5077+
echo " 📄 Applying unstaged changes..."
5078+
git apply "$snapshot_dir/unstaged.patch" 2>/dev/null || true
5079+
fi
5080+
5081+
echo ""
5082+
success "Rewound to: $label"
5083+
echo ""
5084+
echo -e " ${GRAY}Pre-rewind state saved. Undo with: crab rewind $num 1${NC}"
5085+
}
5086+
5087+
# Auto-snapshot daemon (background)
5088+
start_snapshot_daemon() {
5089+
local num="$1"
5090+
local interval="${2:-$SNAPSHOT_INTERVAL}"
5091+
5092+
echo -e "${CYAN}Starting snapshot daemon for workspace $num...${NC}"
5093+
echo " Interval: ${interval}s"
5094+
echo ""
5095+
5096+
while true; do
5097+
create_snapshot "$num" "auto" >/dev/null 2>&1
5098+
sleep "$interval"
5099+
done
5100+
}
5101+
5102+
# Handle time travel commands
5103+
handle_rewind_command() {
5104+
load_config
5105+
validate_config
5106+
5107+
local num="${1:-}"
5108+
5109+
# Auto-detect workspace if not specified
5110+
if [ -z "$num" ] || ! [[ "$num" =~ ^[0-9]+$ ]]; then
5111+
num=$(detect_workspace)
5112+
if [ -z "$num" ]; then
5113+
error "Specify workspace number: crab rewind <N>"
5114+
exit 1
5115+
fi
5116+
# First arg might be the target, shift it
5117+
if [ -n "$1" ] && ! [[ "$1" =~ ^[0-9]+$ ]]; then
5118+
set -- "$num" "$@"
5119+
fi
5120+
fi
5121+
5122+
local action="${2:-}"
5123+
5124+
case "$action" in
5125+
"")
5126+
list_snapshots "$num"
5127+
;;
5128+
"save"|"snapshot")
5129+
save_snapshot "$num" "${3:-manual}"
5130+
;;
5131+
"daemon"|"auto")
5132+
start_snapshot_daemon "$num" "${3:-$SNAPSHOT_INTERVAL}"
5133+
;;
5134+
*)
5135+
rewind_to_snapshot "$num" "$action"
5136+
;;
5137+
esac
5138+
}
5139+
48365140
# =============================================================================
48375141
# Help / Cheat Sheet
48385142
# =============================================================================
@@ -4970,6 +5274,16 @@ show_cheat() {
49705274
║ ║
49715275
╠═══════════════════════════════════════════════════════════════════════════════╣
49725276
║ ║
5277+
║ TIME TRAVEL (crab rewind ...) ║
5278+
║ ──────────────────────────────────────────────────────────────────────── ║
5279+
║ crab rewind <N> List snapshots for workspace N ║
5280+
║ crab rewind <N> 3 Restore 3rd most recent snapshot ║
5281+
║ crab rewind <N> 2h Restore from ~2 hours ago ║
5282+
║ crab rewind <N> save Create manual snapshot ║
5283+
║ crab snapshot Create snapshot of current workspace ║
5284+
║ ║
5285+
╠═══════════════════════════════════════════════════════════════════════════════╣
5286+
║ ║
49735287
║ TMUX KEYBINDINGS (Prefix = Ctrl+a) ║
49745288
║ ──────────────────────────────────────────────────────────────────────── ║
49755289
║ Option+1,2,3... Switch to workspace 1, 2, 3... ║
@@ -5330,6 +5644,19 @@ main() {
53305644
"handoff")
53315645
handle_handoff_command "${@:2}"
53325646
;;
5647+
"rewind"|"timetravel"|"tt")
5648+
handle_rewind_command "${@:2}"
5649+
;;
5650+
"snapshot")
5651+
load_config
5652+
validate_config
5653+
local num=$(detect_workspace)
5654+
if [ -z "$num" ]; then
5655+
error "Not in a workspace. Use: crab snapshot from workspace directory"
5656+
exit 1
5657+
fi
5658+
save_snapshot "$num" "${2:-manual}"
5659+
;;
53335660
"receive")
53345661
load_config
53355662
validate_config

0 commit comments

Comments
 (0)