Align the relay plugin/server metadata to 1.1.0 and add a version-track checker for Android, server/plugin, and desktop release surfaces.
378 lines
16 KiB
Bash
378 lines
16 KiB
Bash
#!/usr/bin/env bash
|
|
# Hermes-Relay — canonical uninstaller.
|
|
#
|
|
# Reverses every step of install.sh in the opposite order. Idempotent: safe
|
|
# to run twice; missing artifacts produce a warning, not an error. Never
|
|
# touches state shared with other tools (~/.hermes/.env, the gateway's
|
|
# state.db, the hermes-agent venv core, etc.).
|
|
#
|
|
# Usage:
|
|
# bash uninstall.sh # Standard uninstall (keeps QR secret)
|
|
# bash uninstall.sh --remove-secret # Also wipe ~/.hermes/hermes-relay-qr-secret
|
|
# bash uninstall.sh --keep-clone # Don't remove ~/.hermes/hermes-relay
|
|
# bash uninstall.sh --dry-run # Print what would be removed, don't touch anything
|
|
#
|
|
# What it removes (in reverse install order):
|
|
# [6] systemd user service — `systemctl --user disable --now`,
|
|
# unit file deletion, daemon-reload
|
|
# [5] shell shims — ~/.local/bin/hermes-pair,
|
|
# ~/.local/bin/hermes-status,
|
|
# ~/.local/bin/hermes-relay,
|
|
# ~/.local/bin/hermes-relay-update,
|
|
# ~/.local/bin/hermes-relay-tailscale
|
|
# [4] skills external_dirs entry — removes the relay's path from
|
|
# ~/.hermes/config.yaml (other
|
|
# entries preserved)
|
|
# [3] plugin symlink + legacy stales — ~/.hermes/plugins/hermes-relay
|
|
# and any deprecated names
|
|
# [2] bootstrap .pth + pip package — venv site-packages drop and
|
|
# `pip uninstall hermes-relay`
|
|
# [1] git clone — ~/.hermes/hermes-relay (skipped
|
|
# with --keep-clone)
|
|
#
|
|
# What it does NOT touch (ever):
|
|
# - ~/.hermes/.env (other tools authenticate against this)
|
|
# - ~/.hermes/state.db (sessions DB shared with the gateway)
|
|
# - ~/.hermes/hermes-agent/ (the agent itself)
|
|
# - ~/.hermes/hermes-agent/venv/ (venv core; we only remove our own .pth)
|
|
# - ~/.hermes/hermes-relay-qr-secret (kept by default — wipe with --remove-secret)
|
|
# - The phone's stored session token (becomes stale on relay restart anyway)
|
|
#
|
|
# Overrides (mirror install.sh):
|
|
# HERMES_HOME Hermes config home (default: ~/.hermes)
|
|
# HERMES_RELAY_HOME Target directory (default: $HERMES_HOME/hermes-relay)
|
|
# HERMES_VENV_PY Path to hermes-agent venv python
|
|
# (default: $HERMES_HOME/hermes-agent/venv/bin/python)
|
|
|
|
set -euo pipefail
|
|
|
|
# ── Argument parsing ───────────────────────────────────────────────────────
|
|
KEEP_CLONE=""
|
|
REMOVE_SECRET=""
|
|
DRY_RUN=""
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--keep-clone) KEEP_CLONE=1 ;;
|
|
--remove-secret) REMOVE_SECRET=1 ;;
|
|
--dry-run) DRY_RUN=1 ;;
|
|
-h|--help)
|
|
sed -n '2,40p' "$0"
|
|
exit 0
|
|
;;
|
|
*) echo "Unknown flag: $1" >&2; exit 1 ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# ── Config ─────────────────────────────────────────────────────────────────
|
|
HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
|
|
RELAY_HOME="${HERMES_RELAY_HOME:-$HERMES_HOME/hermes-relay}"
|
|
VENV_PY="${HERMES_VENV_PY:-$HERMES_HOME/hermes-agent/venv/bin/python}"
|
|
PLUGIN_LINK="$HERMES_HOME/plugins/hermes-relay"
|
|
HERMES_CONFIG="$HERMES_HOME/config.yaml"
|
|
QR_SECRET="$HERMES_HOME/hermes-relay-qr-secret"
|
|
SHIM_PATH="$HOME/.local/bin/hermes-pair"
|
|
STATUS_SHIM_PATH="$HOME/.local/bin/hermes-status"
|
|
RELAY_SHIM_PATH="$HOME/.local/bin/hermes-relay"
|
|
UPDATE_SHIM_PATH="$HOME/.local/bin/hermes-relay-update"
|
|
TS_SHIM_PATH="$HOME/.local/bin/hermes-relay-tailscale"
|
|
DOCTOR_SHIM_PATH="$HOME/.local/bin/hermes-relay-doctor"
|
|
SYSTEMD_USER_DIR="$HOME/.config/systemd/user"
|
|
SERVICE_DST="$SYSTEMD_USER_DIR/hermes-relay.service"
|
|
PTH_NAME="hermes_relay_bootstrap.pth"
|
|
|
|
# ── Helpers ────────────────────────────────────────────────────────────────
|
|
info() { echo " $*"; }
|
|
ok() { echo " [ok] $*"; }
|
|
warn() { echo " [skip] $*"; }
|
|
note() { echo " [note] $*"; }
|
|
|
|
run() {
|
|
if [ -n "$DRY_RUN" ]; then
|
|
echo " [dry-run] $*"
|
|
else
|
|
eval "$@"
|
|
fi
|
|
}
|
|
|
|
# ── Banner ─────────────────────────────────────────────────────────────────
|
|
echo ""
|
|
echo " Hermes-Relay Uninstaller"
|
|
echo " ------------------------"
|
|
echo " Hermes home: $HERMES_HOME"
|
|
echo " Clone: $RELAY_HOME"
|
|
echo " Venv python: $VENV_PY"
|
|
[ -n "$KEEP_CLONE" ] && echo " --keep-clone: yes (clone will be left in place)"
|
|
[ -n "$REMOVE_SECRET" ] && echo " --remove-secret: yes (QR secret will be wiped)"
|
|
[ -n "$DRY_RUN" ] && echo " --dry-run: yes (no filesystem changes)"
|
|
echo ""
|
|
|
|
# ── 6/6 Stop + remove systemd user service ────────────────────────────────
|
|
info "[6/6] Removing systemd user service..."
|
|
if command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1; then
|
|
if systemctl --user list-unit-files hermes-relay.service >/dev/null 2>&1 && \
|
|
systemctl --user list-unit-files hermes-relay.service 2>/dev/null | grep -q hermes-relay.service; then
|
|
run "systemctl --user disable --now hermes-relay.service >/dev/null 2>&1 || true"
|
|
ok "Stopped + disabled hermes-relay.service"
|
|
else
|
|
warn "hermes-relay.service was not registered with systemd — nothing to stop"
|
|
fi
|
|
|
|
if [ -f "$SERVICE_DST" ]; then
|
|
run "rm -f \"$SERVICE_DST\""
|
|
run "systemctl --user daemon-reload >/dev/null 2>&1 || true"
|
|
ok "Removed $SERVICE_DST"
|
|
else
|
|
warn "$SERVICE_DST does not exist"
|
|
fi
|
|
else
|
|
warn "systemd user session not available — nothing to stop"
|
|
# Catch a stray manually-launched relay if there is one
|
|
if pgrep -f "python -m plugin.relay" >/dev/null 2>&1; then
|
|
note "A manual 'python -m plugin.relay' is still running."
|
|
note "Stop it yourself with: pkill -f 'python -m plugin.relay'"
|
|
fi
|
|
fi
|
|
|
|
# ── 5/6 Remove shell shims ───────────────────────────────────────────────
|
|
info "[5/6] Removing shell shims..."
|
|
if [ -f "$SHIM_PATH" ] || [ -L "$SHIM_PATH" ]; then
|
|
run "rm -f \"$SHIM_PATH\""
|
|
ok "Removed $SHIM_PATH"
|
|
else
|
|
warn "$SHIM_PATH does not exist"
|
|
fi
|
|
if [ -f "$STATUS_SHIM_PATH" ] || [ -L "$STATUS_SHIM_PATH" ]; then
|
|
run "rm -f \"$STATUS_SHIM_PATH\""
|
|
ok "Removed $STATUS_SHIM_PATH"
|
|
else
|
|
warn "$STATUS_SHIM_PATH does not exist"
|
|
fi
|
|
if [ -f "$RELAY_SHIM_PATH" ] || [ -L "$RELAY_SHIM_PATH" ]; then
|
|
run "rm -f \"$RELAY_SHIM_PATH\""
|
|
ok "Removed $RELAY_SHIM_PATH"
|
|
else
|
|
warn "$RELAY_SHIM_PATH does not exist"
|
|
fi
|
|
if [ -f "$UPDATE_SHIM_PATH" ] || [ -L "$UPDATE_SHIM_PATH" ]; then
|
|
run "rm -f \"$UPDATE_SHIM_PATH\""
|
|
ok "Removed $UPDATE_SHIM_PATH"
|
|
else
|
|
warn "$UPDATE_SHIM_PATH does not exist"
|
|
fi
|
|
if [ -f "$TS_SHIM_PATH" ] || [ -L "$TS_SHIM_PATH" ]; then
|
|
run "rm -f \"$TS_SHIM_PATH\""
|
|
ok "Removed $TS_SHIM_PATH"
|
|
else
|
|
warn "$TS_SHIM_PATH does not exist"
|
|
fi
|
|
if [ -f "$DOCTOR_SHIM_PATH" ] || [ -L "$DOCTOR_SHIM_PATH" ]; then
|
|
run "rm -f \"$DOCTOR_SHIM_PATH\""
|
|
ok "Removed $DOCTOR_SHIM_PATH"
|
|
else
|
|
warn "$DOCTOR_SHIM_PATH does not exist"
|
|
fi
|
|
|
|
# ── 4/6 Remove skills external_dirs entry from config.yaml ────────────────
|
|
info "[4/6] Removing skills external_dirs entry..."
|
|
if [ -f "$HERMES_CONFIG" ] && [ -x "$VENV_PY" ]; then
|
|
if [ -n "$DRY_RUN" ]; then
|
|
echo " [dry-run] would scrub skills.external_dirs entry pointing at $RELAY_HOME/skills"
|
|
else
|
|
"$VENV_PY" - <<PY
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
try:
|
|
import yaml
|
|
except ImportError:
|
|
print(" [skip] pyyaml not available — config.yaml left untouched")
|
|
sys.exit(0)
|
|
|
|
cfg_path = Path("$HERMES_CONFIG")
|
|
target_str = "$RELAY_HOME/skills"
|
|
target = str(Path(target_str).expanduser().resolve()) if Path(target_str).expanduser().exists() else target_str
|
|
|
|
if not cfg_path.is_file():
|
|
print(" [skip] $HERMES_CONFIG not present")
|
|
sys.exit(0)
|
|
|
|
try:
|
|
data = yaml.safe_load(cfg_path.read_text(encoding="utf-8")) or {}
|
|
except Exception as exc:
|
|
print(f" [skip] Could not parse {cfg_path}: {exc}")
|
|
sys.exit(0)
|
|
|
|
if not isinstance(data, dict):
|
|
print(" [skip] config.yaml is not a mapping — nothing to remove")
|
|
sys.exit(0)
|
|
|
|
skills_section = data.get("skills")
|
|
if not isinstance(skills_section, dict):
|
|
print(" [skip] no skills section in config.yaml")
|
|
sys.exit(0)
|
|
|
|
external_dirs = skills_section.get("external_dirs")
|
|
if not isinstance(external_dirs, list):
|
|
print(" [skip] no external_dirs entry to clean")
|
|
sys.exit(0)
|
|
|
|
# Match by both raw string AND resolved path so we catch both forms
|
|
remaining = []
|
|
removed = 0
|
|
for entry in external_dirs:
|
|
if not isinstance(entry, str):
|
|
remaining.append(entry)
|
|
continue
|
|
try:
|
|
resolved = str(Path(entry).expanduser().resolve())
|
|
except Exception:
|
|
resolved = entry
|
|
if entry == target_str or resolved == target or entry.endswith("/hermes-relay/skills"):
|
|
removed += 1
|
|
else:
|
|
remaining.append(entry)
|
|
|
|
if removed == 0:
|
|
print(" [skip] no relay skills entry was registered")
|
|
sys.exit(0)
|
|
|
|
if remaining:
|
|
skills_section["external_dirs"] = remaining
|
|
else:
|
|
skills_section.pop("external_dirs", None)
|
|
if not skills_section:
|
|
data.pop("skills", None)
|
|
|
|
backup = cfg_path.with_suffix(cfg_path.suffix + ".bak")
|
|
backup.write_text(cfg_path.read_text(encoding="utf-8"), encoding="utf-8")
|
|
print(f" [ok] Backed up existing config to {backup}")
|
|
cfg_path.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8")
|
|
print(f" [ok] Removed {removed} relay skills entry from {cfg_path}")
|
|
PY
|
|
fi
|
|
else
|
|
warn "$HERMES_CONFIG missing or venv python unavailable — skipped"
|
|
fi
|
|
|
|
# ── 3/6 Remove plugin symlink (and legacy stales) ─────────────────────────
|
|
info "[3/6] Removing plugin symlink..."
|
|
removed_any=""
|
|
for path in "$PLUGIN_LINK" \
|
|
"$HERMES_HOME/plugins/hermes-android" \
|
|
"$HERMES_HOME/hermes-agent/plugins/hermes-android" \
|
|
"$HERMES_HOME/hermes-agent/plugins/hermes-relay"; do
|
|
if [ -L "$path" ] || [ -e "$path" ]; then
|
|
run "rm -rf \"$path\""
|
|
ok "Removed $path"
|
|
removed_any=1
|
|
fi
|
|
done
|
|
[ -z "$removed_any" ] && warn "No plugin symlinks were registered"
|
|
|
|
# Best-effort hermes-agent dashboard rescan so the relay tab disappears
|
|
# without requiring a dashboard restart. Walks the systemd unit's
|
|
# ExecStart for the real --host / --port when available, otherwise
|
|
# falls back to common bind addresses. Silent on failure.
|
|
if command -v curl >/dev/null 2>&1; then
|
|
_dash_hosts=""
|
|
_dash_port=""
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
_exec_line="$(systemctl --user cat hermes-dashboard 2>/dev/null | grep -m1 '^ExecStart=' || true)"
|
|
if [ -n "$_exec_line" ]; then
|
|
_dash_hosts="$(echo "$_exec_line" | sed -nE 's/.*--host[[:space:]=]+([^[:space:]]+).*/\1/p')"
|
|
_dash_port="$(echo "$_exec_line" | sed -nE 's/.*--port[[:space:]=]+([0-9]+).*/\1/p')"
|
|
fi
|
|
fi
|
|
[ -z "$_dash_port" ] && _dash_port="9119"
|
|
for host in 127.0.0.1 localhost ${_dash_hosts:-} 0.0.0.0; do
|
|
[ -z "$host" ] && continue
|
|
for port in "$_dash_port" 9119 9100 9000; do
|
|
if curl -sf -m 2 -X GET "http://${host}:${port}/api/dashboard/plugins/rescan" >/dev/null 2>&1; then
|
|
ok "Triggered dashboard rescan at ${host}:${port}"
|
|
break 2
|
|
fi
|
|
done
|
|
done
|
|
fi
|
|
|
|
# ── 2/6 Remove bootstrap .pth + pip package ───────────────────────────────
|
|
info "[2/6] Removing bootstrap .pth + pip package..."
|
|
|
|
if [ -x "$VENV_PY" ]; then
|
|
SITE_PKGS="$("$VENV_PY" -c 'import site; print(site.getsitepackages()[0])' 2>/dev/null || true)"
|
|
if [ -n "$SITE_PKGS" ] && [ -f "$SITE_PKGS/$PTH_NAME" ]; then
|
|
if [ -n "$DRY_RUN" ]; then
|
|
echo " [dry-run] \"$VENV_PY\" -m plugin.cli relay compat remove --site-packages \"$SITE_PKGS\""
|
|
elif "$VENV_PY" -m plugin.cli relay compat remove --site-packages "$SITE_PKGS" >/dev/null 2>&1; then
|
|
ok "Removed compat hook via hermes relay compat"
|
|
else
|
|
run "rm -f \"$SITE_PKGS/$PTH_NAME\""
|
|
ok "Removed $SITE_PKGS/$PTH_NAME (compat command unavailable)"
|
|
fi
|
|
else
|
|
warn "$PTH_NAME not present in venv site-packages"
|
|
fi
|
|
|
|
if "$VENV_PY" -m pip show hermes-relay >/dev/null 2>&1; then
|
|
run "\"$VENV_PY\" -m pip uninstall --quiet --yes hermes-relay >/dev/null 2>&1 || true"
|
|
ok "pip uninstall hermes-relay"
|
|
else
|
|
warn "hermes-relay pip package was not installed"
|
|
fi
|
|
else
|
|
warn "Venv python not found at $VENV_PY — skipped pip uninstall + .pth removal"
|
|
fi
|
|
|
|
# ── 1/6 Remove the git clone ──────────────────────────────────────────────
|
|
info "[1/6] Removing git clone..."
|
|
if [ -n "$KEEP_CLONE" ]; then
|
|
note "--keep-clone set — leaving $RELAY_HOME in place"
|
|
elif [ -d "$RELAY_HOME" ]; then
|
|
# Sanity guard: refuse to remove a directory that doesn't look like our clone
|
|
if [ -d "$RELAY_HOME/.git" ] && [ -f "$RELAY_HOME/install.sh" ]; then
|
|
run "rm -rf \"$RELAY_HOME\""
|
|
ok "Removed $RELAY_HOME"
|
|
else
|
|
warn "$RELAY_HOME exists but doesn't look like a hermes-relay clone — left untouched"
|
|
fi
|
|
else
|
|
warn "$RELAY_HOME does not exist"
|
|
fi
|
|
|
|
# ── Optional: QR signing secret ────────────────────────────────────────────
|
|
info "[opt] QR signing secret..."
|
|
if [ -f "$QR_SECRET" ]; then
|
|
if [ -n "$REMOVE_SECRET" ]; then
|
|
run "rm -f \"$QR_SECRET\""
|
|
ok "Removed $QR_SECRET (--remove-secret)"
|
|
note "All paired phones will need to re-trust the next QR code on their next pair."
|
|
else
|
|
note "$QR_SECRET preserved (use --remove-secret to wipe)."
|
|
note "Without removing it, re-installing keeps the same QR signing identity"
|
|
note "and any phones still holding their session tokens stay valid."
|
|
fi
|
|
else
|
|
warn "$QR_SECRET does not exist"
|
|
fi
|
|
|
|
# ── Done ───────────────────────────────────────────────────────────────────
|
|
echo ""
|
|
if [ -n "$DRY_RUN" ]; then
|
|
echo " [dry-run complete] No changes made."
|
|
else
|
|
echo " [OK] Hermes-Relay uninstalled."
|
|
fi
|
|
echo ""
|
|
echo " Preserved (other tools depend on these):"
|
|
echo " - $HERMES_HOME/.env"
|
|
echo " - $HERMES_HOME/state.db (sessions database)"
|
|
echo " - $HERMES_HOME/config.yaml (only the relay's skills entry was removed)"
|
|
echo " - $HERMES_HOME/hermes-agent/ (the agent itself)"
|
|
[ -z "$REMOVE_SECRET" ] && [ -f "$QR_SECRET" ] && echo " - $QR_SECRET (QR signing secret)"
|
|
echo ""
|
|
echo " To reinstall:"
|
|
echo " curl -fsSL https://raw.githubusercontent.com/Codename-11/hermes-relay/main/install.sh | bash"
|
|
echo ""
|