cloverleaf-larry/larry-rollback.sh
Bryan Johnson e08f030df5 v0.3.0: initial release of Larry-Anywhere
Portable AI agent for Cloverleaf integration work. Pure bash + curl + jq.
Zero dependency on v1 wrapper scripts or v2 cloverleaf-tools.pyz.

27 native Anthropic tools:

NetConfig parsing (read)
  nc_list_protocols, nc_list_processes, nc_protocol_block,
  nc_protocol_field, nc_protocol_nested, nc_protocol_summary,
  nc_destinations, nc_sources, nc_xlate_refs, nc_tclproc_refs

NetConfig modification (journal-backed writes with rollback)
  nc_insert_protocol, nc_add_route, larry_rollback_list

Workflows
  nc_find_inbound, nc_make_jump (3-thread jump pattern), nc_find
  (tbn/tbp/tbh/tbpr/where replacements), nc_document, nc_diff_interface,
  nc_regression

Messages
  hl7_field, nc_msgs (smat is SQLite!), hl7_diff (with --ignore MSH.7)

File system
  read_file, list_dir, grep_files, glob_files, write_file, bash_exec

Validated against a 22-site real Cloverleaf test install. Five worked
examples end-to-end: jump-thread generation, smat MRN search, system
documentation, interface+connected diff, HL7-aware regression diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 09:46:20 -07:00

130 lines
4.4 KiB
Bash
Executable File

#!/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/tty || ans=""
[[ "$ans" =~ ^[Yy]$ ]] || { echo "aborted"; exit 1; }
fi
printf '%s\n' "$ENTRIES" | while IFS=$'\t' read -r ts ses seq target action orig_sha new_sha backup diffp; do
[ -z "$target" ] && continue
if [ "$action" = "create" ]; then
if [ -e "$target" ]; then
cp -p "$target" "$target.larry-prerollback.$(date +%s)"
rm -f "$target" && printf ' %s✓ deleted%s %s (was newly created)\n' "$C_GREEN" "$C_RESET" "$target"
else
printf ' %s○ already absent%s %s\n' "$C_DIM" "$C_RESET" "$target"
fi
else
if [ -f "$backup" ]; then
cp -p "$target" "$target.larry-prerollback.$(date +%s)" 2>/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 <target>.larry-prerollback.<ts> in case you want to redo. Clean up at your leisure."