cloverleaf-larry/uninstall-larry.sh
Bryan Johnson 7606a535c9 v0.8.33: uninstall command + --no-api deterministic-only mode
Two operator-requested features:

1. `larry uninstall` / uninstall-larry.sh — there was no uninstaller before.
   Reverses install-larry.sh exactly: removes $LARRY_HOME (bundle + bin/jq +
   optional phi-venv + all runtime artifacts incl. log/headers.log, sessions,
   journal, lessons, creds) and the `larry` PATH shim. DRY-RUN by default;
   --yes to delete, --keep-data to preserve user data. Removes ONLY what the
   installer created (shim removed only if it carries our auto-gen header;
   shell rc / Cloverleaf sites / $HCIROOT never touched). Stops running PHI
   sidecar / tunnel via their own pidfiles. Shipped by the installer +
   manifest-synced; dispatched early like `larry tools` so it works offline.

2. --no-api (env LARRY_NO_API=1) — deterministic-only mode making ZERO LLM API
   calls (zero cost). REPL + all local/deterministic commands still work; a
   free-text prompt is routed to the matching `larry tools <name>` instead of
   the model. No API key required (first-run auth prompt skipped). call_api /
   call_api_stream hard-refuse as defense in depth.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 09:43:51 -07:00

207 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# uninstall-larry.sh — cleanly remove everything install-larry.sh put down.
#
# Reverses the installer EXACTLY: it removes only what Larry-Anywhere created
# (its install dir, the `larry` PATH shim, the optional PHI venv, the jq
# fallback binary, and all runtime artifacts under $LARRY_HOME) and NOTHING the
# installer didn't create. It does NOT touch your shell rc, your Cloverleaf
# sites, your $HCIROOT, or any file outside the install footprint.
#
# Default is a DRY-RUN preview. You must pass --yes (or confirm at the prompt)
# to actually delete. Every path removed is printed.
#
# Usage:
# uninstall-larry.sh # dry-run: list exactly what WOULD be removed
# uninstall-larry.sh --dry-run # (explicit) same as above
# uninstall-larry.sh --yes # remove without the interactive prompt
# uninstall-larry.sh --keep-data # remove program files but KEEP user data
# # (sessions/, journal/, lessons/,
# # knowledge/, sanitize/, log/, creds)
# uninstall-larry.sh --help
#
# Also reachable as a subcommand: larry uninstall [--dry-run|--yes|--keep-data]
#
# Env vars (mirror the installer so it targets the SAME footprint):
# LARRY_HOME install location (default: $HOME/.larry)
# LARRY_BIN_DIR where the `larry` shim was symlinked (default: $HOME/bin)
set -eu
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
LARRY_BIN_DIR="${LARRY_BIN_DIR:-$HOME/bin}"
C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'; C_GREEN=$'\033[32m'
C_YELLOW=$'\033[33m'; C_RED=$'\033[31m'; C_CYAN=$'\033[36m'; C_DIM=$'\033[2m'
say() { printf '%s%suninstall-larry>%s %s\n' "$C_CYAN" "$C_BOLD" "$C_RESET" "$*"; }
ok() { printf ' %s✓%s %s\n' "$C_GREEN" "$C_RESET" "$*"; }
warn() { printf ' %s!%s %s\n' "$C_YELLOW" "$C_RESET" "$*"; }
die() { printf '%serror:%s %s\n' "$C_RED" "$C_RESET" "$*" >&2; exit 1; }
# ─────────────────────────────────────────────────────────────────────────────
# Args
# ─────────────────────────────────────────────────────────────────────────────
DRY_RUN=1 # default: preview only
ASSUME_YES=0
KEEP_DATA=0
for arg in "${@:-}"; do
case "$arg" in
--yes|-y) DRY_RUN=0; ASSUME_YES=1 ;;
--dry-run|-n) DRY_RUN=1 ;;
--keep-data) KEEP_DATA=1 ;;
--help|-h) sed -n '2,38p' "$0"; exit 0 ;;
"") : ;;
*) die "unknown flag: $arg (try --help)" ;;
esac
done
# ─────────────────────────────────────────────────────────────────────────────
# Stop background processes Larry may have started, BEFORE deleting their files.
# Both PID files live under $LARRY_HOME. Best-effort: a stale/dead PID is fine.
# We only kill a PID we can read from Larry's own pidfile — never a guessed one.
# ─────────────────────────────────────────────────────────────────────────────
stop_bg_proc() {
# $1 = human label, $2 = pidfile path
local label="$1" pidfile="$2" pid=""
[ -f "$pidfile" ] || return 0
pid="$(tr -d '[:space:]' < "$pidfile" 2>/dev/null || echo "")"
case "$pid" in
''|*[!0-9]*) return 0 ;; # empty or non-numeric → nothing safe to do
esac
if kill -0 "$pid" 2>/dev/null; then
if [ "$DRY_RUN" = "1" ]; then
printf ' %s•%s would stop %s (PID %s)\n' "$C_YELLOW" "$C_RESET" "$label" "$pid"
else
kill "$pid" 2>/dev/null && ok "stopped $label (PID $pid)" || warn "could not stop $label (PID $pid) — kill it manually"
fi
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# Build the removal list. We separate PROGRAM artifacts (always removed) from
# USER DATA (removed unless --keep-data). Everything here was created by the
# installer or by Larry at runtime — we never list anything else.
# ─────────────────────────────────────────────────────────────────────────────
PROGRAM_TARGETS=() # files/dirs that ARE the install footprint
DATA_TARGETS=() # user-generated data under $LARRY_HOME
# The shim the installer drops onto PATH. We only remove it if it is OUR shim
# (auto-generated header), so we never delete an unrelated `larry` someone else
# put there.
SHIM="$LARRY_BIN_DIR/larry"
if [ -f "$SHIM" ] && grep -q 'Auto-generated by install-larry.sh' "$SHIM" 2>/dev/null; then
PROGRAM_TARGETS+=("$SHIM")
elif [ -e "$SHIM" ]; then
warn "$SHIM exists but is NOT our auto-generated shim — leaving it untouched."
fi
# The entire install dir is ours. Rather than enumerate every dotfile, we list
# $LARRY_HOME itself for the full uninstall. With --keep-data we instead remove
# the program files inside it and preserve the user-data subtrees.
if [ -d "$LARRY_HOME" ]; then
if [ "$KEEP_DATA" = "0" ]; then
PROGRAM_TARGETS+=("$LARRY_HOME")
else
# Program files (shipped bundle + caches + state markers + the jq fallback
# + the optional PHI venv). Anything not in this list under $LARRY_HOME is
# treated as user data and preserved.
for p in \
larry.sh larry-tunnel.sh larry-rollback.sh larry-auth.sh uninstall-larry.sh \
install-larry.sh VERSION MANUAL.md MANIFEST CHANGELOG.md README.md \
inbound-systems.tsv agents lib bin phi-venv \
.last-sync-version .origin .oauth-optin-warned \
.phi-notice-shown .b64-py3-notice-shown \
.last-curl-rc .last-curl-stderr .last-curl-headers \
.last-stream-headers .last-stream-curlerr \
.phi-sidecar.pid tunnel.pid tunnel.url tunnel.log .history; do
[ -e "$LARRY_HOME/$p" ] && PROGRAM_TARGETS+=("$LARRY_HOME/$p")
done
# User data preserved under --keep-data (listed for transparency).
for d in sessions journal lessons knowledge sanitize regression log \
.api-key .env .oauth.json .ssh-creds .ssh-sockets .ssh-hosts.tsv \
known_hosts .headers-sync-offset .headers-sync-state; do
[ -e "$LARRY_HOME/$d" ] && DATA_TARGETS+=("$LARRY_HOME/$d")
done
fi
fi
if [ "${#PROGRAM_TARGETS[@]}" -eq 0 ]; then
say "nothing to remove — no Larry-Anywhere install footprint found."
say " (looked for LARRY_HOME=$LARRY_HOME and shim $SHIM)"
exit 0
fi
# ─────────────────────────────────────────────────────────────────────────────
# Preview
# ─────────────────────────────────────────────────────────────────────────────
say "Larry-Anywhere uninstall — install footprint to remove:"
echo ""
printf '%sProgram files (always removed):%s\n' "$C_BOLD" "$C_RESET"
for t in "${PROGRAM_TARGETS[@]}"; do
if [ -d "$t" ]; then printf ' %s(dir) %s%s\n' "$C_DIM" "$t" "$C_RESET"
else printf ' %s\n' "$t"; fi
done
if [ "$KEEP_DATA" = "1" ]; then
echo ""
printf '%sUser data PRESERVED (--keep-data):%s\n' "$C_BOLD" "$C_RESET"
if [ "${#DATA_TARGETS[@]}" -eq 0 ]; then
printf ' %s(none found)%s\n' "$C_DIM" "$C_RESET"
else
for d in "${DATA_TARGETS[@]}"; do printf ' %skept%s %s\n' "$C_GREEN" "$C_RESET" "$d"; done
fi
fi
echo ""
say "NOT touched: your shell rc (any PATH export you added stays — remove it by hand),"
say " your Cloverleaf sites / \$HCIROOT, and anything outside the list above."
echo ""
# Stop background procs (preview prints "would stop …"; real run kills them).
stop_bg_proc "PHI Presidio sidecar" "$LARRY_HOME/.phi-sidecar.pid"
stop_bg_proc "reverse SSH tunnel" "$LARRY_HOME/tunnel.pid"
echo ""
if [ "$DRY_RUN" = "1" ]; then
say "${C_BOLD}DRY-RUN${C_RESET} — nothing was removed. Re-run with ${C_CYAN}--yes${C_RESET} to delete the above."
exit 0
fi
# ─────────────────────────────────────────────────────────────────────────────
# Confirm (skipped with --yes... but --yes already set ASSUME_YES; this is the
# belt-and-suspenders path if someone sets DRY_RUN=0 by other means).
# ─────────────────────────────────────────────────────────────────────────────
if [ "$ASSUME_YES" != "1" ]; then
printf '%sProceed with removal? This cannot be undone. [y/N]: %s' "$C_YELLOW" "$C_RESET"
ans=""
if [ -t 0 ]; then read -r ans </dev/tty || ans=""; else read -r ans || ans=""; fi
case "$ans" in
y|Y|yes|YES) : ;;
*) say "aborted — nothing removed."; exit 0 ;;
esac
fi
# ─────────────────────────────────────────────────────────────────────────────
# Remove. Print every path as it goes. rm -rf is scoped strictly to the list we
# built above — we never expand globs or touch a parent dir.
# ─────────────────────────────────────────────────────────────────────────────
removed=0
for t in "${PROGRAM_TARGETS[@]}"; do
if rm -rf "$t" 2>/dev/null; then
ok "removed $t"
removed=$((removed+1))
else
warn "could not remove $t (check permissions; remove by hand)"
fi
done
echo ""
say "removed $removed item(s)."
if [ "$KEEP_DATA" = "1" ] && [ "${#DATA_TARGETS[@]}" -gt 0 ]; then
say "your data is preserved under $LARRY_HOME — delete it manually if you want a full wipe."
fi
# Reminder: the installer never EDITS your shell rc; it only WARNS you to add a
# PATH export. If you added one, it's still there — this is the one manual step.
case ":$PATH:" in
*":$LARRY_BIN_DIR:"*)
say "reminder: you may have added 'export PATH=\"$LARRY_BIN_DIR:\$PATH\"' to your shell rc. Remove that line by hand if $LARRY_BIN_DIR is now empty." ;;
esac
say "uninstall complete."