From 81c4875ecf00cc3d0c86b01f42e3272734093f5d Mon Sep 17 00:00:00 2001 From: Bryan Johnson Date: Wed, 27 May 2026 17:25:00 -0700 Subject: [PATCH] v0.7.2: Gitea becomes primary auto-update origin; GitHub demoted to fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 — 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 --- VERSION | 2 +- install-larry.sh | 45 +++++++- larry.sh | 261 ++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 288 insertions(+), 20 deletions(-) diff --git a/VERSION b/VERSION index 39e898a..7486fdb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.1 +0.7.2 diff --git a/install-larry.sh b/install-larry.sh index 9744628..dc8e559 100755 --- a/install-larry.sh +++ b/install-larry.sh @@ -15,9 +15,16 @@ set -eu LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" -# Canonical hosting: bojj27/cloverleaf-larry on GitHub, branch main, raw view. -# Override via env if you fork or mirror elsewhere. -LARRY_BASE_URL="${LARRY_BASE_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main}" +# 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' @@ -69,12 +76,34 @@ ok "created $LARRY_HOME" # ───────────────────────────────────────────────────────────────────────────── 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" - curl -fsSL --max-time 30 "$LARRY_BASE_URL/$1" -o "$2" \ - && ok "$2" || die "failed to fetch $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 @@ -181,6 +210,12 @@ fi # ───────────────────────────────────────────────────────────────────────────── 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)" diff --git a/larry.sh b/larry.sh index d5c4d78..2ac9162 100755 --- a/larry.sh +++ b/larry.sh @@ -11,10 +11,17 @@ # # Env vars: # LARRY_HOME where to cache config/sessions (default: ~/.larry) -# LARRY_BASE_URL root URL of the bundle on the server (default: -# https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main) +# LARRY_BASE_URL primary root URL of the bundle on the server (default +# as of v0.7.2: https://git.bjnoela.com/bryan/cloverleaf-larry/raw/branch/main). # Self-update pulls VERSION + MANIFEST from here and # refreshes every file listed in MANIFEST. +# LARRY_BASE_URL_FALLBACK +# secondary root URL used if the primary is unreachable +# (default: https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main). +# v0.7.2: GitHub demoted from primary to fallback so the +# self-hosted Gitea origin (git.bjnoela.com) is the +# canonical source. Users can pin/override via the +# /origin slash command (see $LARRY_HOME/.origin). # LARRY_UPDATE_URL (legacy override) full URL of latest larry.sh # LARRY_AGENTS_URL (legacy override) base URL for agents/ # LARRY_MODEL Claude model (default: claude-sonnet-4-6) @@ -47,9 +54,59 @@ set -o pipefail # ───────────────────────────────────────────────────────────────────────────── # Config # ───────────────────────────────────────────────────────────────────────────── -LARRY_VERSION="0.7.1" +LARRY_VERSION="0.7.2" LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" -LARRY_BASE_URL="${LARRY_BASE_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main}" + +# ───────────────────────────────────────────────────────────────────────────── +# Origin defaults (v0.7.2). Gitea is primary; GitHub is fallback. +# --------------------------------------------------------------------------- +# The /origin slash-command family (see dispatcher below) lets the user pin +# either origin or supply a custom URL. Persisted pins live at +# $LARRY_HOME/.origin and are applied just below. Env-var overrides still win +# over the pinned file (mirroring how LARRY_HOME / LARRY_MODEL etc. behave). +# +# IMPORTANT: the Gitea repo (git.bjnoela.com/bryan/cloverleaf-larry) MUST be +# set to PUBLIC for unauthenticated raw-URL reads to succeed. Until then the +# fallback path (GitHub) carries all real auto-update traffic. +# ───────────────────────────────────────────────────────────────────────────── +LARRY_ORIGIN_DEFAULT_GITEA="https://git.bjnoela.com/bryan/cloverleaf-larry/raw/branch/main" +LARRY_ORIGIN_DEFAULT_GITHUB="https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main" + +# Resolve a pinned origin file BEFORE applying env-var defaults so the env +# overrides still take precedence. $LARRY_HOME may not exist yet on first +# launch — guard accordingly. +_larry_pin_primary="" +_larry_pin_fallback="" +if [ -r "$LARRY_HOME/.origin" ]; then + _larry_pin_raw=$(tr -d '[:space:]' < "$LARRY_HOME/.origin" 2>/dev/null) + case "$_larry_pin_raw" in + gitea) + _larry_pin_primary="$LARRY_ORIGIN_DEFAULT_GITEA" + _larry_pin_fallback="$LARRY_ORIGIN_DEFAULT_GITHUB" ;; + github) + # SWAP: github becomes primary, gitea fallback. + _larry_pin_primary="$LARRY_ORIGIN_DEFAULT_GITHUB" + _larry_pin_fallback="$LARRY_ORIGIN_DEFAULT_GITEA" ;; + https://*) + _larry_pin_primary="$_larry_pin_raw" + _larry_pin_fallback="$LARRY_ORIGIN_DEFAULT_GITEA" ;; + "") : ;; # empty file → treat as no pin + *) + printf 'warn: ignoring unrecognised value in %s/.origin: %s\n' "$LARRY_HOME" "$_larry_pin_raw" >&2 ;; + esac + unset _larry_pin_raw +fi + +LARRY_BASE_URL="${LARRY_BASE_URL:-${_larry_pin_primary:-$LARRY_ORIGIN_DEFAULT_GITEA}}" +LARRY_BASE_URL_FALLBACK="${LARRY_BASE_URL_FALLBACK:-${_larry_pin_fallback:-$LARRY_ORIGIN_DEFAULT_GITHUB}}" +unset _larry_pin_primary _larry_pin_fallback + +# Tracks which origin actually served the most recent self_update phase (set +# by sync_from_manifest / phase-B fetch). Read by /origin and the status +# line's optional origin badge. Values: "" (none yet), "primary", "fallback". +_LARRY_LAST_ORIGIN="" +_LARRY_LAST_ORIGIN_URL="" + LARRY_UPDATE_URL="${LARRY_UPDATE_URL:-${LARRY_BASE_URL}/larry.sh}" LARRY_AGENTS_URL="${LARRY_AGENTS_URL:-${LARRY_BASE_URL}/agents}" LARRY_MODEL="${LARRY_MODEL:-claude-sonnet-4-6}" @@ -268,7 +325,33 @@ fetch_agents_or_warn # pulls every other file matching the new version. # # Skip all of it via --no-update or LARRY_NO_UPDATE=1. +# +# v0.7.2: every network step (manifest fetch in phase A, VERSION + larry.sh +# fetch in phase B) tries $LARRY_BASE_URL first and transparently falls back +# to $LARRY_BASE_URL_FALLBACK when the primary fails (curl exit non-zero, +# HTTP error, timeout, DNS failure). The label of whichever origin actually +# served the last byte is recorded in $_LARRY_LAST_ORIGIN[_URL] so /origin +# and the optional status-line badge can display it. # ───────────────────────────────────────────────────────────────────────────── + +# _origin_label URL — short label ("gitea", "github", or the URL itself) used +# in human-facing log lines. Pure string match — no network. +_origin_label() { + case "$1" in + "$LARRY_ORIGIN_DEFAULT_GITEA") printf 'gitea' ;; + "$LARRY_ORIGIN_DEFAULT_GITHUB") printf 'github' ;; + *) printf '%s' "$1" ;; + esac +} + +# _record_origin SLOT URL — remember the origin (and its URL) that just +# served bytes. SLOT is "primary" or "fallback". Persists across the rest +# of the run so /origin and the status line can read it back. +_record_origin() { + _LARRY_LAST_ORIGIN="$1" + _LARRY_LAST_ORIGIN_URL="$2" +} + sync_from_manifest() { local base="$1" local manifest="$LARRY_HOME/.manifest.new" @@ -318,6 +401,45 @@ sync_from_manifest() { return 0 } +# sync_from_manifest_with_fallback — try primary then fallback. Returns 0 if +# either succeeded, non-zero if BOTH unreachable. Wraps sync_from_manifest. +sync_from_manifest_with_fallback() { + if sync_from_manifest "$LARRY_BASE_URL"; then + _record_origin primary "$LARRY_BASE_URL" + return 0 + fi + if [ -n "$LARRY_BASE_URL_FALLBACK" ] && [ "$LARRY_BASE_URL_FALLBACK" != "$LARRY_BASE_URL" ]; then + warn "$(_origin_label "$LARRY_BASE_URL") unreachable, falling back to $(_origin_label "$LARRY_BASE_URL_FALLBACK")" + if sync_from_manifest "$LARRY_BASE_URL_FALLBACK"; then + _record_origin fallback "$LARRY_BASE_URL_FALLBACK" + return 0 + fi + fi + warn "self-update skipped (both origins unreachable)" + return 1 +} + +# _fetch_with_fallback REL_PATH DEST [MAX_TIME] — curl with primary→fallback +# retry. Returns 0 if either pulled a non-empty file, non-zero otherwise. +# Records the winning origin slot in $_LARRY_LAST_ORIGIN. +_fetch_with_fallback() { + local rel="$1" dest="$2" mt="${3:-15}" + if curl -fsSL --max-time "$mt" "$LARRY_BASE_URL/$rel" -o "$dest" 2>/dev/null && [ -s "$dest" ]; then + _record_origin primary "$LARRY_BASE_URL" + return 0 + fi + rm -f "$dest" + if [ -n "$LARRY_BASE_URL_FALLBACK" ] && [ "$LARRY_BASE_URL_FALLBACK" != "$LARRY_BASE_URL" ]; then + if curl -fsSL --max-time "$mt" "$LARRY_BASE_URL_FALLBACK/$rel" -o "$dest" 2>/dev/null && [ -s "$dest" ]; then + warn "$(_origin_label "$LARRY_BASE_URL") unreachable for $rel, served from $(_origin_label "$LARRY_BASE_URL_FALLBACK")" + _record_origin fallback "$LARRY_BASE_URL_FALLBACK" + return 0 + fi + rm -f "$dest" + fi + return 1 +} + self_update() { [ "$LARRY_NO_UPDATE" = "1" ] && return 0 [ -z "$LARRY_BASE_URL" ] && return 0 @@ -334,7 +456,7 @@ self_update() { if [ "$last_sync" != "$LARRY_VERSION" ]; then LARRY_SYNC_UPDATED_COUNT=0 LARRY_SYNC_FAILED_COUNT=0 - if sync_from_manifest "$LARRY_BASE_URL"; then + if sync_from_manifest_with_fallback; then printf '%s\n' "$LARRY_VERSION" > "$LARRY_HOME/.last-sync-version" 2>/dev/null || true if [ "${LARRY_JUST_UPDATED:-0}" = "1" ] && [ -n "${LARRY_PREV_VERSION:-}" ]; then # We came in via a phase-B self-replace; phase A then synced the rest. @@ -355,14 +477,20 @@ self_update() { [ "${LARRY_JUST_UPDATED:-0}" = "1" ] && return 0 [ -w "$self" ] || return 0 - local remote_ver - remote_ver=$(curl -fsSL --max-time 5 "$LARRY_BASE_URL/VERSION" 2>/dev/null | tr -d '[:space:]') + # VERSION fetch with primary→fallback. + local ver_tmp="$LARRY_HOME/.VERSION.new" remote_ver="" + if _fetch_with_fallback "VERSION" "$ver_tmp" 5; then + remote_ver=$(tr -d '[:space:]' < "$ver_tmp" 2>/dev/null) + fi + rm -f "$ver_tmp" [ -z "$remote_ver" ] && return 0 [ "$remote_ver" = "$LARRY_VERSION" ] && return 0 local tmp="$LARRY_HOME/larry.sh.new" - curl -fsSL --max-time 15 "$LARRY_BASE_URL/larry.sh" -o "$tmp" 2>/dev/null || { rm -f "$tmp"; return 0; } - [ -s "$tmp" ] || { rm -f "$tmp"; return 0; } + if ! _fetch_with_fallback "larry.sh" "$tmp" 15; then + rm -f "$tmp" + return 0 + fi if cmp -s "$self" "$tmp"; then rm -f "$tmp" return 0 @@ -370,7 +498,7 @@ self_update() { local new_ver new_ver=$(grep -m1 '^LARRY_VERSION=' "$tmp" | sed 's/.*"\(.*\)".*/\1/') [ -z "$new_ver" ] && { rm -f "$tmp"; return 0; } - log "update found: $LARRY_VERSION -> $new_ver — relaunching" + log "update found: $LARRY_VERSION -> $new_ver (via $(_origin_label "$_LARRY_LAST_ORIGIN_URL")) — relaunching" cp "$tmp" "$self" && chmod +x "$self" rm -f "$tmp" # Force phase A on the next launch by invalidating the sync stamp. @@ -1435,10 +1563,11 @@ _render_status_line_oauth() { "$seven_color" "$seven_pct" "$C_DIM" \ "$C_RESET") fi + local badge; badge=$(_origin_badge) if [ -n "$overall_pre" ]; then - printf '%s%s\n' "$overall_pre" "$line" + printf '%s%s%s\n' "$overall_pre" "$line" "$badge" else - printf '%s\n' "$line" + printf '%s%s\n' "$line" "$badge" fi } @@ -1446,8 +1575,39 @@ _render_status_line_apikey() { local ctx; ctx=$(_ctx_segment) # Session $ from current cost trackers. local dollars; dollars=$(_render_session_cost_dollars) - printf '%s─ %s ─ $%s session ─ %d turns ─%s\n' \ - "$C_DIM" "$ctx" "$dollars" "$_LARRY_TURNS" "$C_RESET" + local badge; badge=$(_origin_badge) + printf '%s─ %s ─ $%s session ─ %d turns ─%s%s\n' \ + "$C_DIM" "$ctx" "$dollars" "$_LARRY_TURNS" "$C_RESET" "$badge" +} + +# _origin_badge — light-touch v0.7.2 status-line segment. Returns a leading +# space + colored fragment when the origin is non-default, otherwise empty. +# Default = on defaults AND last-served by primary (or never served yet). +_origin_badge() { + # Suppress on default state. + local pinned="" + [ -r "$LARRY_HOME/.origin" ] && pinned=$(tr -d '[:space:]' < "$LARRY_HOME/.origin" 2>/dev/null) + if [ -z "$pinned" ] && { [ -z "$_LARRY_LAST_ORIGIN" ] || [ "$_LARRY_LAST_ORIGIN" = "primary" ]; }; then + return 0 + fi + # Pinned to github → " github" (dim). + if [ "$pinned" = "github" ]; then + printf ' %sgithub%s' "$C_DIM" "$C_RESET" + return 0 + fi + # Pinned to a custom URL → " custom". + case "$pinned" in + https://*) printf ' %scustom%s' "$C_DIM" "$C_RESET"; return 0 ;; + esac + # Failed over from gitea→github (or other primary→fallback). + if [ "$_LARRY_LAST_ORIGIN" = "fallback" ]; then + printf ' %s%s→%s%s' "$C_RED$C_DIM" \ + "$(_origin_label "$LARRY_BASE_URL")" \ + "$(_origin_label "$LARRY_BASE_URL_FALLBACK")" \ + "$C_RESET" + return 0 + fi + return 0 } # _render_session_cost_dollars — reuse the existing pricing logic. @@ -2418,6 +2578,17 @@ Slash commands: /hl7-fields print component breakdown for a field (e.g. /hl7-fields PID.5 → Family, Given, ...) + Auto-update origin (v0.7.2): + /origin show current primary + fallback + which + served the last self-update + /origin gitea pin to git.bjnoela.com (default primary) + /origin github pin to GitHub raw (swap: github→primary) + /origin auto clear the pin (revert to gitea primary, + github fallback) + /origin pin to an arbitrary HTTPS base URL + (useful for air-gapped mirrors / testing) + Pin is persisted to \$LARRY_HOME/.origin and re-read on next launch. + Mouse mode (v0.7.0): /mouse on|off toggle xterm mouse + bracketed-paste for the session. Status with /mouse (no arg). @@ -2570,6 +2741,7 @@ _LARRY_SLASH_CMDS=( /hl7 /hl7-fields /mouse + /origin ) # _LARRY_SLASH_CMDS_DESC — one-line descriptions for each slash command. @@ -2620,6 +2792,7 @@ _LARRY_SLASH_CMDS_DESC=( [/hl7]=" print full field list for an HL7 segment (e.g. /hl7 PID)" [/hl7-fields]=" print component breakdown (e.g. /hl7-fields PID.5)" [/mouse]="on|off toggle xterm mouse mode for this session" + [/origin]="show/pin auto-update origin (gitea|github|auto|)" ) # __larry_complete_slash — bound to TAB via `bind -x` (see _install_readline_tab). @@ -3360,6 +3533,66 @@ main_loop() { fi continue ;; # v0.7.0: mouse mode toggle (xterm SGR mouse + bracketed paste). + /origin|/origin\ *) + # v0.7.2: show/pin the auto-update origin. Pin is persisted + # to $LARRY_HOME/.origin and re-read on the NEXT launch (the + # current run keeps using the resolved $LARRY_BASE_URL). + local _arg; _arg=$(_slash_args "/origin" "$input") + case "${_arg:-status}" in + status) + printf '%sauto-update origin:%s\n' "$C_BOLD" "$C_RESET" + printf ' %sprimary :%s %s %s(%s)%s\n' \ + "$C_BOLD" "$C_RESET" "$LARRY_BASE_URL" \ + "$C_DIM" "$(_origin_label "$LARRY_BASE_URL")" "$C_RESET" + printf ' %sfallback:%s %s %s(%s)%s\n' \ + "$C_BOLD" "$C_RESET" "$LARRY_BASE_URL_FALLBACK" \ + "$C_DIM" "$(_origin_label "$LARRY_BASE_URL_FALLBACK")" "$C_RESET" + if [ -n "$_LARRY_LAST_ORIGIN" ]; then + printf ' %slast served by:%s %s %s(%s)%s\n' \ + "$C_BOLD" "$C_RESET" "$_LARRY_LAST_ORIGIN" \ + "$C_DIM" "$(_origin_label "$_LARRY_LAST_ORIGIN_URL")" "$C_RESET" + else + printf ' %slast served by:%s %s(self-update did not run this session)%s\n' \ + "$C_BOLD" "$C_RESET" "$C_DIM" "$C_RESET" + fi + if [ -r "$LARRY_HOME/.origin" ]; then + printf ' %spin file :%s %s/.origin → %s\n' \ + "$C_BOLD" "$C_RESET" "$LARRY_HOME" \ + "$(tr -d '[:space:]' < "$LARRY_HOME/.origin" 2>/dev/null)" + else + printf ' %spin file :%s %s(none — using defaults)%s\n' \ + "$C_BOLD" "$C_RESET" "$C_DIM" "$C_RESET" + fi + ;; + gitea) + printf 'gitea\n' > "$LARRY_HOME/.origin" 2>/dev/null \ + && larry_say "pinned origin: gitea ($LARRY_ORIGIN_DEFAULT_GITEA). Restart larry to apply." \ + || err "could not write $LARRY_HOME/.origin" + ;; + github) + printf 'github\n' > "$LARRY_HOME/.origin" 2>/dev/null \ + && larry_say "pinned origin: github ($LARRY_ORIGIN_DEFAULT_GITHUB) — SWAP, github becomes primary, gitea fallback. Restart larry to apply." \ + || err "could not write $LARRY_HOME/.origin" + ;; + auto) + if [ -f "$LARRY_HOME/.origin" ]; then + rm -f "$LARRY_HOME/.origin" \ + && larry_say "pin cleared. Restart larry to revert to defaults (gitea primary, github fallback)." \ + || err "could not remove $LARRY_HOME/.origin" + else + larry_say "no pin in place; already on defaults." + fi + ;; + https://*) + printf '%s\n' "$_arg" > "$LARRY_HOME/.origin" 2>/dev/null \ + && larry_say "pinned origin: $_arg (gitea kept as fallback). Restart larry to apply." \ + || err "could not write $LARRY_HOME/.origin" + ;; + *) + err "usage: /origin [gitea|github|auto|] (no arg → status)" + ;; + esac + continue ;; /mouse|/mouse\ *) local _arg; _arg=$(_slash_args "/mouse" "$input") case "${_arg:-status}" in