162d5cdd10
install.sh promises "the running fleet == the repo, always; editing the live file IS editing the tracked file". Its bin/ loop globbed `bin/bullpen-*`, so five entrypoints were never deployed by it at all: `bp`, `room-ask`, `room_tail`, `news-de` and `deus-seize` — the commands a human types, plus the emergency stop. They had been hand-copied once and then drifted silently. Measured on 2026-08-02: the room host was running a `bp` from 2026-07-21 (61 lines behind the repo, which is how `bp --route` came back as "no reply from @--route"), and deus-seize had never reached any host, so tonight's portability work on it was live nowhere. The hole is invisible to inspection: the glob reads as "the bullpen commands", and every file it misses is sitting right beside the ones it catches. tests/test_install_covers_bin.py compares the two sets instead of reading the pattern — red under the old glob, green now. Directories and non-executables are skipped explicitly, so bin/__pycache__ (which appears the moment anything in bin/ is imported) cannot be symlinked into /usr/local/bin.
102 lines
5.0 KiB
Bash
Executable File
102 lines
5.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# deploy/install.sh — point this host's live bullpen AT this working copy, so the repo is the
|
|
# source of truth and `git pull` is the whole deploy.
|
|
#
|
|
# The drift problem: the fleet used to run hand-edited loose copies (scripts in ~/.local/bin,
|
|
# unit files copied into /etc/systemd/system), which silently diverged from the repo. This
|
|
# replaces those copies with SYMLINKS into the checked-out repo, so:
|
|
# * the running fleet == the repo, always; editing the live file IS editing the tracked file;
|
|
# * to update a host: `git pull` here (+ a daemon-reload / service restart to pick it up).
|
|
#
|
|
# Two layouts:
|
|
# install.sh user layout — bin/ -> ~/.local/bin, units -> ~/.config/systemd/user
|
|
# install.sh --system system layout — bin/ -> /usr/local/bin, lib/ -> /usr/local/lib/bullpen,
|
|
# contrib/lmcp-tool on PATH, units -> /etc/systemd/system (run as root)
|
|
#
|
|
# It deploys: bin/bullpen-* + the lurker, lib/*.py (--system), contrib/lmcp-tool (--system),
|
|
# and every systemd/ + lurker/ unit file. It does NOT touch per-nick lurker config dirs or the
|
|
# lmcp room tool (fleet state / load-bearing — deploy those deliberately).
|
|
#
|
|
# The units carry NO usernames and need no per-host editing. Because the SAME tracked file is
|
|
# symlinked into either layout, a literal ExecStart path is necessarily wrong in one of them —
|
|
# which is why it flip-flopped twice (6905f4e -> 933dd86) and crash-looped bullpen-triage. The
|
|
# units instead say `Environment=PATH=%h/.local/bin:/usr/local/bin:…` + `ExecStart=/usr/bin/env
|
|
# <prog>`: in user scope %h is the user's home and ~/.local/bin wins; in system scope %h is
|
|
# /root, that entry misses, and /usr/local/bin wins. tests/test_units_portable.py holds the line.
|
|
#
|
|
# Idempotent and reversible: a pre-existing NON-symlink file is moved to <name>.predeploy-bak
|
|
# before the symlink is created. Re-running is safe.
|
|
set -euo pipefail
|
|
|
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
SYSTEM=0
|
|
case "${1:-}" in
|
|
--system) SYSTEM=1 ;;
|
|
""|--user) SYSTEM=0 ;;
|
|
-h|--help) sed -n '2,20p' "$0"; exit 0 ;;
|
|
*) echo "usage: install.sh [--system]" >&2; exit 2 ;;
|
|
esac
|
|
|
|
if [ "$SYSTEM" = 1 ]; then
|
|
BINDEST="/usr/local/bin"; LIBDEST="/usr/local/lib/bullpen"; UNITDEST="/etc/systemd/system"
|
|
[ -w /usr/local/bin ] || { echo "install.sh --system needs write to /usr/local — run as root/sudo." >&2; exit 1; }
|
|
else
|
|
BINDEST="${HOME}/.local/bin"; LIBDEST=""; UNITDEST="${HOME}/.config/systemd/user"
|
|
fi
|
|
mkdir -p "$BINDEST" "$UNITDEST"; [ -n "$LIBDEST" ] && mkdir -p "$LIBDEST"
|
|
|
|
link() { # link <path-relative-to-repo> <dest-dir> <name>
|
|
local src="$REPO/$1" dest="$2" name="$3" tgt="$2/$3"
|
|
[ -f "$src" ] || { echo " skip $name (no $1 in repo)"; return; }
|
|
if [ -L "$tgt" ] && [ "$(readlink -f "$tgt")" = "$(readlink -f "$src")" ]; then
|
|
echo " ok $name (already linked)"; return
|
|
fi
|
|
if [ -e "$tgt" ] && [ ! -L "$tgt" ]; then
|
|
mv -- "$tgt" "$tgt.predeploy-bak"
|
|
echo " bak $name -> $name.predeploy-bak"
|
|
fi
|
|
ln -sfn "$src" "$tgt"
|
|
echo " link $name -> $src"
|
|
}
|
|
|
|
echo "bullpen deploy ($([ "$SYSTEM" = 1 ] && echo system || echo user)): symlinking -> $REPO"
|
|
# EVERY executable in bin/, not just bullpen-*. The old glob was `bin/bullpen-*`, which
|
|
# silently excluded `bp`, `room-ask`, `room_tail`, `news-de` and `deus-seize` — i.e. the
|
|
# commands a human actually types, plus the emergency stop. They were hand-copied once and
|
|
# then drifted: on 2026-08-02 hertz still ran a `bp` from 2026-07-21, and deus-seize had
|
|
# never been deployed at all. A deploy whose promise is "the running fleet == the repo"
|
|
# cannot have a name-shaped hole in it.
|
|
for f in "$REPO"/bin/*; do
|
|
[ -f "$f" ] || continue # skips __pycache__/ and any other directory
|
|
[ -x "$f" ] || continue # data files in bin/ are not entrypoints
|
|
link "bin/$(basename "$f")" "$BINDEST" "$(basename "$f")"
|
|
done
|
|
# … plus the lurker (lives under lurker/, not bin/)
|
|
link "lurker/bullpen-lurker" "$BINDEST" "bullpen-lurker"
|
|
|
|
if [ "$SYSTEM" = 1 ]; then
|
|
# the config/worker/participant libs the entrypoints import from /usr/local/lib/bullpen
|
|
for f in "$REPO"/lib/*.py; do
|
|
[ -e "$f" ] || continue
|
|
link "lib/$(basename "$f")" "$LIBDEST" "$(basename "$f")"
|
|
done
|
|
# the lmcp-tool client the coordinators call by name (room host)
|
|
link "contrib/lmcp-tool" "$BINDEST" "lmcp-tool"
|
|
fi
|
|
|
|
# systemd units — symlink service/timer files so `git pull` + daemon-reload deploys unit edits
|
|
units=0
|
|
for f in "$REPO"/systemd/*.service "$REPO"/systemd/*.timer "$REPO"/lurker/*.service; do
|
|
[ -e "$f" ] || continue
|
|
case "$f" in "$REPO"/systemd/*) rel="systemd/$(basename "$f")";; *) rel="lurker/$(basename "$f")";; esac
|
|
link "$rel" "$UNITDEST" "$(basename "$f")"; units=1
|
|
done
|
|
if [ "$units" = 1 ]; then
|
|
if [ "$SYSTEM" = 1 ]; then systemctl daemon-reload; else systemctl --user daemon-reload 2>/dev/null || true; fi
|
|
echo " systemd daemon-reload ($UNITDEST)"
|
|
fi
|
|
|
|
echo "done. scripts are byte-identical to what already ran (no restart needed);"
|
|
echo "changed unit files take effect after the daemon-reload above + a service restart."
|