#!/usr/bin/env bash # larry-rollback.sh — restore files modified by Larry-Anywhere from journal backups. # # Usage: # larry-rollback.sh --list # show every journal entry, newest first # larry-rollback.sh --list --session SESSION # show entries for one session # larry-rollback.sh --session SESSION # roll back ALL entries in a session (newest first) # larry-rollback.sh --last N # roll back the N most recent entries # larry-rollback.sh --entry ENTRY_ID # roll back one specific entry (e.g. 2026-05-26-.../004) # larry-rollback.sh --target /path/to/file # roll back all entries that touched this file (newest first) # larry-rollback.sh --dry-run # show what would be rolled back without doing it # # Y/N confirm on every restoration unless --yes is passed. set -u set -o pipefail LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" JOURNAL_ROOT="$LARRY_HOME/journal" JOURNAL_INDEX="$JOURNAL_ROOT/index.tsv" DRY=0 YES=0 MODE="" SESSION="" N="" ENTRY_ID="" TARGET="" while [ $# -gt 0 ]; do case "$1" in --list) MODE="list" ;; --session) shift; SESSION="$1"; [ -z "$MODE" ] && MODE="session" ;; --last) shift; N="$1"; MODE="last" ;; --entry) shift; ENTRY_ID="$1"; MODE="entry" ;; --target) shift; TARGET="$1"; MODE="target" ;; --dry-run) DRY=1 ;; --yes|-y) YES=1 ;; -h|--help) sed -n '2,16p' "$0"; exit 0 ;; *) echo "unknown arg: $1" >&2; exit 2 ;; esac shift done [ -f "$JOURNAL_INDEX" ] || { echo "no journal (no writes have been made yet)" >&2; exit 1; } C_BOLD=$'\033[1m'; C_DIM=$'\033[2m'; C_RESET=$'\033[0m' C_RED=$'\033[31m'; C_GREEN=$'\033[32m' _list_filter() { local ses="$1" awk -F'\t' -v s="$ses" ' NR==1 { next } s=="" || $2==s { print } ' "$JOURNAL_INDEX" | tail -r 2>/dev/null || awk -F'\t' -v s="$ses" ' NR==1 { next } s=="" || $2==s { print } ' "$JOURNAL_INDEX" | awk '{a[NR]=$0} END{for(i=NR;i>=1;i--) print a[i]}' } if [ "$MODE" = "list" ]; then printf '%stimestamp session-id seq target%s\n' "$C_BOLD" "$C_RESET" _list_filter "$SESSION" | awk -F'\t' '{printf " %-26s %-28s %-4s %s\n", $1, $2, $3, $4}' exit 0 fi build_targets() { case "$MODE" in session) [ -n "$SESSION" ] || { echo "--session needs a value" >&2; exit 2; } _list_filter "$SESSION" ;; last) [ -n "$N" ] || { echo "--last needs N" >&2; exit 2; } _list_filter "" | head -n "$N" ;; entry) local ses seq ses="${ENTRY_ID%/*}"; seq="${ENTRY_ID##*/}"; seq="${seq%%_*}" awk -F'\t' -v s="$ses" -v sq="$seq" '$2==s && $3==sq' "$JOURNAL_INDEX" ;; target) awk -F'\t' -v t="$TARGET" '$4==t' "$JOURNAL_INDEX" | awk '{a[NR]=$0} END{for(i=NR;i>=1;i--) print a[i]}' ;; *) echo "specify --list, --session, --last N, --entry ID, or --target PATH" >&2 exit 2 ;; esac } ENTRIES=$(build_targets) [ -n "$ENTRIES" ] || { echo "(no matching journal entries)"; exit 0; } printf '%sWill roll back %s entr(y/ies):%s\n' "$C_BOLD" "$(printf '%s\n' "$ENTRIES" | wc -l | tr -d ' ')" "$C_RESET" printf '%s' "$ENTRIES" | awk -F'\t' '{printf " %s %-26s %s\n", $3, $1, $4}' echo "" if [ "$DRY" = "1" ]; then echo "(dry-run; no changes)" exit 0 fi if [ "$YES" != "1" ]; then printf '%sProceed?%s [y/N]: ' "$C_BOLD" "$C_RESET" read -r ans /dev/null || true cp -p "$backup" "$target" && printf ' %s✓ restored%s %s ← %s\n' "$C_GREEN" "$C_RESET" "$target" "$backup" else printf ' %s✗ backup missing for%s %s (looked at %s)\n' "$C_RED" "$C_RESET" "$target" "$backup" fi fi done echo "" echo "Pre-rollback copies left at .larry-prerollback. in case you want to redo. Clean up at your leisure."