cloverleaf-larry/install-larry.sh
Bryan Johnson 81c4875ecf v0.7.2: Gitea becomes primary auto-update origin; GitHub demoted to fallback
Switches the canonical $LARRY_BASE_URL default from raw.githubusercontent.com
to the self-hosted Gitea mirror at git.bjnoela.com.  GitHub stays in the
loop as $LARRY_BASE_URL_FALLBACK and is used automatically when the primary
fails (DNS, timeout, HTTP error, private repo).

What's new
- Origin defaults split into LARRY_BASE_URL (Gitea) +
  LARRY_BASE_URL_FALLBACK (GitHub).  Env vars still override either side.
- Every network call in self_update tries primary first, then fallback.
  Emits "warn: gitea unreachable, falling back to github" on switch and
  "warn: self-update skipped (both origins unreachable)" if both fail.
- New /origin slash-command family:
    /origin           — show current primary/fallback + which served last
    /origin gitea     — pin to Gitea (default state)
    /origin github    — swap so GitHub is primary, Gitea fallback
    /origin auto      — clear pin, revert to defaults
    /origin <https://...> — pin to an arbitrary HTTPS base URL
  Pin is persisted to $LARRY_HOME/.origin and re-read on next launch.
- Status line picks up a light origin badge when state is non-default
  ("github" pinned, "custom" pinned, or "gitea→github" on failover).
- install-larry.sh mirrors the same primary→fallback fetch logic so
  first-contact installs still work even if Gitea is unreachable.

ACTION REQUIRED — Bryan, before this commit's auto-update path becomes
live you must set git.bjnoela.com/bryan/cloverleaf-larry repo visibility
to PUBLIC.  Gitea defaults to private; until you toggle it, every client
will silently fall back to GitHub.  Verify by running, from any box:
  curl -fsSI https://git.bjnoela.com/bryan/cloverleaf-larry/raw/branch/main/VERSION
A 200 with the published VERSION means clients hit Gitea; a 404/403 means
they still ride the GitHub fallback.

Don't break
- v0.7.1 status-line position (between turns)
- v0.7.0 HL7 completion, mouse mode
- v0.6.9 status line state tracking, header capture
- v0.6.7 streaming, @file, slash completion, persistent history
- v0.6.6 CR-strip + slash TAB

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 17:25:00 -07:00

229 lines
13 KiB
Bash
Executable File

#!/usr/bin/env bash
# install-larry.sh — bootstrap Larry-Anywhere on a fresh remote shell.
# No root, no package install, no sudo. Writes only into $LARRY_HOME.
#
# Usage:
# curl -fsSL <BASE_URL>/install-larry.sh | bash
#
# Or with explicit base URL:
# LARRY_BASE_URL=https://example.com/larry-anywhere bash install-larry.sh
#
# Env vars:
# LARRY_HOME install location (default: $HOME/.larry)
# LARRY_BASE_URL where to fetch files from (no trailing slash)
# LARRY_BIN_DIR where to symlink the `larry` command (default: $HOME/bin)
set -eu
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
# Canonical hosting (v0.7.2+): self-hosted Gitea at git.bjnoela.com is primary;
# bojj27/cloverleaf-larry on GitHub is the unauthenticated fallback used when
# Gitea is unreachable (DNS failure, repo set to private, server down, etc.).
# Override either via env if you fork or mirror elsewhere.
#
# IMPORTANT: the Gitea repo must be set to PUBLIC for unauthenticated raw-URL
# reads to succeed. Until Bryan toggles repo visibility, the installer (and
# auto-update) will silently fall back to GitHub.
LARRY_BASE_URL="${LARRY_BASE_URL:-https://git.bjnoela.com/bryan/cloverleaf-larry/raw/branch/main}"
LARRY_BASE_URL_FALLBACK="${LARRY_BASE_URL_FALLBACK:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main}"
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'
say() { printf '%s%sinstall-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; }
# ─────────────────────────────────────────────────────────────────────────────
# Detect platform
# ─────────────────────────────────────────────────────────────────────────────
UNAME_S="$(uname -s 2>/dev/null || echo unknown)"
PLATFORM=""
case "$UNAME_S" in
Linux*) PLATFORM="linux" ;;
Darwin*) PLATFORM="darwin" ;;
CYGWIN*|MINGW*|MSYS*) PLATFORM="windows-cygwin" ;; # MobaXterm lives here
*) PLATFORM="unknown" ;;
esac
ARCH="$(uname -m 2>/dev/null || echo unknown)"
case "$ARCH" in
x86_64|amd64) ARCH_NORM="amd64" ;;
aarch64|arm64) ARCH_NORM="arm64" ;;
i?86) ARCH_NORM="i386" ;;
*) ARCH_NORM="$ARCH" ;;
esac
say "platform: $PLATFORM/$ARCH_NORM • LARRY_HOME=$LARRY_HOME"
# ─────────────────────────────────────────────────────────────────────────────
# Check required commands
# ─────────────────────────────────────────────────────────────────────────────
command -v bash >/dev/null 2>&1 || die "bash not found"
command -v curl >/dev/null 2>&1 || die "curl not found"
# ─────────────────────────────────────────────────────────────────────────────
# Make dirs
# ─────────────────────────────────────────────────────────────────────────────
mkdir -p "$LARRY_HOME"/{agents,sessions,bin,lib} || die "cannot create $LARRY_HOME"
chmod 700 "$LARRY_HOME" 2>/dev/null || true
ok "created $LARRY_HOME"
# ─────────────────────────────────────────────────────────────────────────────
# Fetch the scripts. If LARRY_BASE_URL is not set, try to detect being run
# from a local checkout (sibling files present) — copy locally instead.
# ─────────────────────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd)" || SCRIPT_DIR=""
# v0.7.2: install-larry.sh is the FIRST contact with the origin — it runs
# before larry.sh exists, so it has to do its own primary→fallback dance.
# Mirrors larry.sh's _fetch_with_fallback behavior; on every fetch, try
# primary first, then fallback if the primary curl exits non-zero or the
# resulting file is empty. Records the winning origin in $LAST_FETCH_ORIGIN
# so the post-install summary can tell Bryan which side served the install.
LAST_FETCH_ORIGIN=""
fetch() {
# $1 = remote relative path, $2 = local destination
if [ -n "$LARRY_BASE_URL" ]; then
say "fetching $1"
if curl -fsSL --max-time 30 "$LARRY_BASE_URL/$1" -o "$2" 2>/dev/null && [ -s "$2" ]; then
LAST_FETCH_ORIGIN="primary"
ok "$2"
return 0
fi
rm -f "$2"
if [ -n "${LARRY_BASE_URL_FALLBACK:-}" ] && [ "$LARRY_BASE_URL_FALLBACK" != "$LARRY_BASE_URL" ]; then
warn "primary unreachable for $1 — trying fallback"
if curl -fsSL --max-time 30 "$LARRY_BASE_URL_FALLBACK/$1" -o "$2" 2>/dev/null && [ -s "$2" ]; then
LAST_FETCH_ORIGIN="fallback"
ok "$2 (via fallback)"
return 0
fi
rm -f "$2"
fi
die "failed to fetch $1 from primary or fallback"
elif [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/$1" ]; then
cp "$SCRIPT_DIR/$1" "$2" && ok "copied $1 (local)"
else
die "no LARRY_BASE_URL set and $1 not found in script dir"
fi
}
fetch larry.sh "$LARRY_HOME/larry.sh"
fetch larry-tunnel.sh "$LARRY_HOME/larry-tunnel.sh"
fetch agents/larry.md "$LARRY_HOME/agents/larry.md"
fetch agents/clover.md "$LARRY_HOME/agents/clover.md"
fetch agents/cloverleaf-cheatsheet.md "$LARRY_HOME/agents/cloverleaf-cheatsheet.md"
fetch agents/regress.md "$LARRY_HOME/agents/regress.md"
fetch larry-rollback.sh "$LARRY_HOME/larry-rollback.sh"
fetch larry-auth.sh "$LARRY_HOME/larry-auth.sh"
fetch lib/oauth.sh "$LARRY_HOME/lib/oauth.sh"
fetch lib/ssh-helper.sh "$LARRY_HOME/lib/ssh-helper.sh"
fetch lib/lessons.sh "$LARRY_HOME/lib/lessons.sh"
fetch lib/hl7-sanitize.sh "$LARRY_HOME/lib/hl7-sanitize.sh"
fetch lib/hl7-desanitize.sh "$LARRY_HOME/lib/hl7-desanitize.sh"
fetch lib/each.sh "$LARRY_HOME/lib/each.sh"
fetch lib/each-site.sh "$LARRY_HOME/lib/each-site.sh"
fetch lib/len2nl.sh "$LARRY_HOME/lib/len2nl.sh"
fetch lib/csv-to-table.sh "$LARRY_HOME/lib/csv-to-table.sh"
fetch lib/table-to-csv.sh "$LARRY_HOME/lib/table-to-csv.sh"
fetch lib/nc-engine.sh "$LARRY_HOME/lib/nc-engine.sh"
fetch lib/nc-status.sh "$LARRY_HOME/lib/nc-status.sh"
fetch lib/nc-table.sh "$LARRY_HOME/lib/nc-table.sh"
fetch lib/nc-xlate.sh "$LARRY_HOME/lib/nc-xlate.sh"
fetch lib/nc-smat-diff.sh "$LARRY_HOME/lib/nc-smat-diff.sh"
fetch lib/nc-create-thread.sh "$LARRY_HOME/lib/nc-create-thread.sh"
fetch lib/nc-tclgen.sh "$LARRY_HOME/lib/nc-tclgen.sh"
fetch lib/nc-parse.sh "$LARRY_HOME/lib/nc-parse.sh"
fetch lib/nc-inbound.sh "$LARRY_HOME/lib/nc-inbound.sh"
fetch lib/nc-make-jump.sh "$LARRY_HOME/lib/nc-make-jump.sh"
fetch lib/hl7-field.sh "$LARRY_HOME/lib/hl7-field.sh"
fetch lib/hl7-schema.sh "$LARRY_HOME/lib/hl7-schema.sh"
fetch lib/nc-msgs.sh "$LARRY_HOME/lib/nc-msgs.sh"
fetch lib/nc-document.sh "$LARRY_HOME/lib/nc-document.sh"
fetch lib/nc-diff-interface.sh "$LARRY_HOME/lib/nc-diff-interface.sh"
fetch lib/nc-find.sh "$LARRY_HOME/lib/nc-find.sh"
fetch lib/nc-insert-protocol.sh "$LARRY_HOME/lib/nc-insert-protocol.sh"
fetch lib/hl7-diff.sh "$LARRY_HOME/lib/hl7-diff.sh"
fetch lib/nc-regression.sh "$LARRY_HOME/lib/nc-regression.sh"
fetch lib/journal.sh "$LARRY_HOME/lib/journal.sh"
fetch VERSION "$LARRY_HOME/VERSION"
fetch MANUAL.md "$LARRY_HOME/MANUAL.md"
chmod +x "$LARRY_HOME/larry.sh" "$LARRY_HOME/larry-tunnel.sh" "$LARRY_HOME/larry-rollback.sh" "$LARRY_HOME/larry-auth.sh" "$LARRY_HOME/lib/"*.sh
# ─────────────────────────────────────────────────────────────────────────────
# jq fallback — download static binary into $LARRY_HOME/bin/ if missing
# ─────────────────────────────────────────────────────────────────────────────
if ! command -v jq >/dev/null 2>&1 && [ ! -x "$LARRY_HOME/bin/jq" ]; then
say "jq not found — fetching static binary into $LARRY_HOME/bin/jq"
JQ_BASE="https://github.com/jqlang/jq/releases/download/jq-1.7.1"
JQ_URL=""
case "$PLATFORM/$ARCH_NORM" in
linux/amd64) JQ_URL="$JQ_BASE/jq-linux-amd64" ;;
linux/arm64) JQ_URL="$JQ_BASE/jq-linux-arm64" ;;
linux/i386) JQ_URL="$JQ_BASE/jq-linux-i386" ;;
darwin/amd64) JQ_URL="$JQ_BASE/jq-macos-amd64" ;;
darwin/arm64) JQ_URL="$JQ_BASE/jq-macos-arm64" ;;
windows-cygwin/amd64) JQ_URL="$JQ_BASE/jq-windows-amd64.exe" ;;
windows-cygwin/i386) JQ_URL="$JQ_BASE/jq-windows-i386.exe" ;;
*) warn "no jq binary known for $PLATFORM/$ARCH_NORM — install jq manually" ;;
esac
if [ -n "$JQ_URL" ]; then
local_jq="$LARRY_HOME/bin/jq"
case "$PLATFORM" in windows-cygwin) local_jq="$LARRY_HOME/bin/jq.exe" ;; esac
if curl -fsSL --max-time 60 "$JQ_URL" -o "$local_jq"; then
chmod +x "$local_jq"
ok "jq -> $local_jq"
else
warn "could not download jq from $JQ_URL"
fi
fi
else
ok "jq available"
fi
# ─────────────────────────────────────────────────────────────────────────────
# Drop a `larry` shim onto PATH (best-effort)
# ─────────────────────────────────────────────────────────────────────────────
mkdir -p "$LARRY_BIN_DIR" 2>/dev/null || true
if [ -d "$LARRY_BIN_DIR" ] && [ -w "$LARRY_BIN_DIR" ]; then
cat > "$LARRY_BIN_DIR/larry" <<EOF
#!/usr/bin/env bash
# Auto-generated by install-larry.sh
export LARRY_HOME="${LARRY_HOME}"
exec "${LARRY_HOME}/larry.sh" "\$@"
EOF
chmod +x "$LARRY_BIN_DIR/larry"
ok "shim: $LARRY_BIN_DIR/larry"
case ":$PATH:" in
*":$LARRY_BIN_DIR:"*) : ;;
*) warn "$LARRY_BIN_DIR is not on PATH — add 'export PATH=\"$LARRY_BIN_DIR:\$PATH\"' to your shell rc" ;;
esac
else
warn "cannot write to $LARRY_BIN_DIR — invoke larry directly as: $LARRY_HOME/larry.sh"
fi
# ─────────────────────────────────────────────────────────────────────────────
# Done
# ─────────────────────────────────────────────────────────────────────────────
echo ""
say "install complete (no system changes were made; everything lives under $LARRY_HOME)"
if [ "$LAST_FETCH_ORIGIN" = "fallback" ]; then
warn "files were served from the FALLBACK origin (primary unreachable)."
warn " primary : $LARRY_BASE_URL"
warn " fallback: $LARRY_BASE_URL_FALLBACK"
warn " if you expected the primary to work, check repo visibility (Gitea repos default to private)."
fi
echo ""
echo "Next steps:"
echo " 1) export ANTHROPIC_API_KEY=sk-ant-... (or larry will prompt on first run)"
echo " 2) larry (or $LARRY_HOME/larry.sh)"
echo " 3) larry /path/to/cloverleaf/site_root (to start with a working dir)"
echo ""
echo "Reverse SSH tunnel (optional, run in another shell or backgrounded):"
echo " $LARRY_HOME/larry-tunnel.sh --serveo # zero-config"
echo " $LARRY_HOME/larry-tunnel.sh --hop=user@bjnoela.com:22 # your hop"
echo ""