Files
webui/start.sh
2026-03-30 20:40:19 -07:00

261 lines
9.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# ============================================================
# Hermes Web UI -- portable bootstrap
# Usage: ./start.sh [port]
#
# One-command startup. Discovers your Hermes install, sets up
# a local virtualenv if needed, installs dependencies, then
# launches the server and prints everything you need to know.
#
# Override any step with environment variables:
# HERMES_WEBUI_AGENT_DIR path to hermes-agent checkout
# HERMES_WEBUI_PYTHON python executable to use
# HERMES_WEBUI_PORT port to listen on (default: 8787)
# HERMES_WEBUI_HOST bind address (default: 127.0.0.1)
# HERMES_HOME override ~/.hermes base
# HERMES_WEBUI_STATE_DIR override state directory
# ============================================================
set -euo pipefail
# ── Load .env if present (machine-local overrides, not committed) ─────────────
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "${_SCRIPT_DIR}/.env" ]]; then
set -a
# shellcheck source=/dev/null
source "${_SCRIPT_DIR}/.env"
set +a
fi
# ── Colours ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
ok() { echo -e "${GREEN}[ok]${RESET} $*"; }
warn() { echo -e "${YELLOW}[!!]${RESET} $*"; }
die() { echo -e "${RED}[XX]${RESET} $*" >&2; exit 1; }
info() { echo -e "${CYAN}[--]${RESET} $*"; }
hdr() { echo -e "\n${BOLD}$*${RESET}"; }
# ── Resolve repo root (the directory this script lives in) ───────────────────
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
info "Repo root: ${REPO_ROOT}"
# ── Port ─────────────────────────────────────────────────────────────────────
PORT="${1:-${HERMES_WEBUI_PORT:-8787}}"
export HERMES_WEBUI_PORT="${PORT}"
# ── Python discovery ─────────────────────────────────────────────────────────
hdr "Discovering Python..."
_find_python() {
# 1. Explicit env var
if [[ -n "${HERMES_WEBUI_PYTHON:-}" ]]; then
echo "${HERMES_WEBUI_PYTHON}"; return
fi
# 2. Agent venv (discovered below -- call again after agent dir found)
# (handled after agent dir discovery)
# 3. Local .venv in repo
if [[ -x "${REPO_ROOT}/.venv/bin/python" ]]; then
echo "${REPO_ROOT}/.venv/bin/python"; return
fi
# 4. System python3
if command -v python3 &>/dev/null; then
echo "$(command -v python3)"; return
fi
echo ""
}
PYTHON="$(_find_python)"
# ── Hermes agent discovery ────────────────────────────────────────────────────
hdr "Discovering Hermes agent..."
HERMES_HOME="${HERMES_HOME:-${HOME}/.hermes}"
AGENT_DIR=""
_find_agent() {
local candidates=(
"${HERMES_WEBUI_AGENT_DIR:-}"
"${HERMES_HOME}/hermes-agent"
"${REPO_ROOT}/../hermes-agent"
"${HOME}/.hermes/hermes-agent"
"${HOME}/hermes-agent"
)
for d in "${candidates[@]}"; do
[[ -z "$d" ]] && continue
d="$(cd "${d}" 2>/dev/null && pwd || true)"
if [[ -n "$d" && -f "${d}/run_agent.py" ]]; then
echo "$d"; return
fi
done
echo ""
}
AGENT_DIR="$(_find_agent)"
if [[ -n "${AGENT_DIR}" ]]; then
ok "Hermes agent: ${AGENT_DIR}"
export HERMES_WEBUI_AGENT_DIR="${AGENT_DIR}"
# Now that we have agent dir, prefer its venv if we don't already have a python
if [[ -z "${HERMES_WEBUI_PYTHON:-}" && -x "${AGENT_DIR}/venv/bin/python" ]]; then
PYTHON="${AGENT_DIR}/venv/bin/python"
fi
else
warn "Hermes agent not found. Agent features will not work."
warn "Fix with: export HERMES_WEBUI_AGENT_DIR=/path/to/hermes-agent"
fi
if [[ -n "${PYTHON}" ]]; then
ok "Python: ${PYTHON} ($(${PYTHON} --version 2>&1))"
else
warn "No Python found. Attempting to install..."
if command -v apt-get &>/dev/null; then
sudo apt-get install -y python3 python3-venv python3-pip
elif command -v brew &>/dev/null; then
brew install python3
else
die "Could not find or install Python. Please install Python 3.8+ and re-run."
fi
PYTHON="$(command -v python3)"
ok "Python installed: ${PYTHON}"
fi
# ── Minimum Python version check ─────────────────────────────────────────────
PY_VER="$(${PYTHON} -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
PY_MAJOR="$(echo "${PY_VER}" | cut -d. -f1)"
PY_MINOR="$(echo "${PY_VER}" | cut -d. -f2)"
if [[ "${PY_MAJOR}" -lt 3 || ( "${PY_MAJOR}" -eq 3 && "${PY_MINOR}" -lt 8 ) ]]; then
die "Python 3.8+ required. Found: ${PY_VER}"
fi
# ── Dependency check / local venv setup ──────────────────────────────────────
hdr "Checking dependencies..."
VENV_NEEDED=false
VENV_PATH="${REPO_ROOT}/.venv"
# If the chosen python is already the agent venv, its deps are already installed.
# If it is a system python, check if we can import the webui deps, create a local
# .venv if not.
_check_deps() {
"${PYTHON}" -c "import yaml" 2>/dev/null
}
if ! _check_deps; then
info "PyYAML not found in ${PYTHON}. Creating local .venv..."
if [[ ! -d "${VENV_PATH}" ]]; then
"${PYTHON}" -m venv "${VENV_PATH}" || die "Failed to create virtualenv at ${VENV_PATH}"
fi
VENV_PY="${VENV_PATH}/bin/python"
"${VENV_PY}" -m pip install --quiet --upgrade pip
if [[ -f "${REPO_ROOT}/requirements.txt" ]]; then
info "Installing from requirements.txt..."
"${VENV_PY}" -m pip install --quiet -r "${REPO_ROOT}/requirements.txt"
else
info "Installing minimal deps (pyyaml)..."
"${VENV_PY}" -m pip install --quiet pyyaml
fi
PYTHON="${VENV_PY}"
ok "Local venv ready: ${VENV_PATH}"
else
ok "Dependencies satisfied."
fi
# ── Kill any stale instance on the same port ─────────────────────────────────
hdr "Checking for existing instances..."
EXISTING=$(lsof -ti tcp:"${PORT}" 2>/dev/null || true)
if [[ -n "${EXISTING}" ]]; then
warn "Killing existing process on port ${PORT} (PID ${EXISTING})"
kill "${EXISTING}" 2>/dev/null || true
sleep 0.5
fi
# Also kill any server.py process from this repo
pkill -f "${REPO_ROOT}/server.py" 2>/dev/null || true
# ── Set up working directory for Hermes imports ───────────────────────────────
# server.py / api/config.py inject agent dir into sys.path at import time,
# but we also cd into the agent dir so relative imports in run_agent work.
if [[ -n "${AGENT_DIR}" ]]; then
WORKDIR="${AGENT_DIR}"
else
WORKDIR="${REPO_ROOT}"
fi
# ── Launch ───────────────────────────────────────────────────────────────────
hdr "Starting Hermes Web UI..."
LOG="/tmp/hermes-webui-${PORT}.log"
export HERMES_WEBUI_HOST="${HERMES_WEBUI_HOST:-127.0.0.1}"
export HERMES_WEBUI_STATE_DIR="${HERMES_WEBUI_STATE_DIR:-${HERMES_HOME}/webui-mvp}"
nohup "${PYTHON}" "${REPO_ROOT}/server.py" \
> "${LOG}" 2>&1 &
PID=$!
echo -e "\n${CYAN} PID ${PID} starting...${RESET}"
sleep 1.5
# ── Health check ─────────────────────────────────────────────────────────────
HEALTH_URL="http://${HERMES_WEBUI_HOST:-127.0.0.1}:${PORT}/health"
MAX_WAIT=15
ELAPSED=0
while [[ $ELAPSED -lt $MAX_WAIT ]]; do
if curl -sf "${HEALTH_URL}" | grep -q '"status"' 2>/dev/null; then
break
fi
sleep 0.5
ELAPSED=$((ELAPSED + 1))
done
if ! curl -sf "${HEALTH_URL}" | grep -q '"status"' 2>/dev/null; then
warn "Health check did not pass within ${MAX_WAIT}s. Check log:"
tail -20 "${LOG}"
echo ""
warn "Server may still be starting. Try: curl ${HEALTH_URL}"
else
ok "Server is healthy."
fi
# ── Print access instructions ─────────────────────────────────────────────────
BIND_HOST="${HERMES_WEBUI_HOST:-127.0.0.1}"
echo ""
echo -e "${BOLD}========================================${RESET}"
echo -e "${GREEN} Hermes Web UI is running${RESET}"
echo -e "${BOLD}========================================${RESET}"
echo ""
if [[ "${BIND_HOST}" == "127.0.0.1" || "${BIND_HOST}" == "localhost" ]]; then
# Server is bound to loopback -- detect if we are on a remote machine
# by checking if $SSH_CLIENT or $SSH_TTY is set
if [[ -n "${SSH_CLIENT:-}" || -n "${SSH_TTY:-}" ]]; then
SERVER_IP="$(hostname -I 2>/dev/null | awk '{print $1}' || echo "<your-server-ip>")"
echo -e " You are on a remote machine. To access from your local browser:"
echo ""
echo -e " ${CYAN}ssh -N -L ${PORT}:127.0.0.1:${PORT} \$(whoami)@${SERVER_IP}${RESET}"
echo ""
echo -e " Then open: ${BOLD}http://localhost:${PORT}${RESET}"
else
echo -e " Open: ${BOLD}http://localhost:${PORT}${RESET}"
fi
else
echo -e " Open: ${BOLD}http://${BIND_HOST}:${PORT}${RESET}"
fi
echo ""
echo -e " Log: ${LOG}"
echo -e " PID: ${PID}"
echo ""