Files
hermes-agent/setup-hermes.sh

286 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# ============================================================================
# Hermes Agent Setup Script — THE dev-environment entry point.
# ============================================================================
# Sets up the pm-managed development environment from a fresh clone:
# 1. Stage the pinned uv from pm/lock.json (sha256-verified, into the pm
# store slot) — pm needs uv to bootstrap, so it cannot stage uv itself.
# 2. Use uv to install and locate bootstrap Python, then let uv exit.
# Run `python -m pm.cli install` directly so PM can safely replace uv.
# PM owns the final interpreter, tool store, and dependency generation.
# 3. Point you at `source ./activate` — the venv-style way to put the pm
# env (PATH + tool vars) into your current shell.
# There is no pip fallback tier here on purpose.
# ============================================================================
set -e
# Activation needs only provisioning, not user-facing installation side effects.
runtime_only=false
case "${1:-}" in
--runtime-only) runtime_only=true ;;
'') ;;
*) printf 'Unknown setup option: %s\n' "$1" >&2; exit 2 ;;
esac
# Colors
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
RED='\033[0;31m'
NC='\033[0m'
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Prevent uv from discovering config files (uv.toml, pyproject.toml) from the
# wrong user's home directory when running under sudo -u <user>. See #21269.
export UV_NO_CONFIG=1
echo ""
echo -e "${CYAN}⚕ Hermes Agent Setup${NC}"
echo ""
# ============================================================================
# Install / locate uv — staged from pm/lock.json (the lockfile is the only
# authority; no astral-latest, no curl|sh).
# ============================================================================
echo -e "${CYAN}→${NC} Checking for uv..."
lock="$SCRIPT_DIR/pm/lock.json"
[ -f "$lock" ] || { echo -e "${RED}✗${NC} pm/lock.json not found" >&2; exit 1; }
# Read the shared mirror location before Python is available.
mirror_origin="$(awk -F '"' '/^ "origin"/ { print $4; exit }' "$SCRIPT_DIR/pm/artifact-mirror.json")"
mirror_prefix="$(awk -F '"' '/^ "prefix"/ { print $4; exit }' "$SCRIPT_DIR/pm/artifact-mirror.json")"
mirror_url_for() { # $1 = lowercase sha256
[ -n "$mirror_origin" ] && [ -n "$mirror_prefix" ] || return 1
printf '%s/%s%s' "$mirror_origin" "$mirror_prefix" "$1"
}
case "$(uname -s)" in
Linux) os=linux ;;
Darwin) os=darwin ;;
MINGW*|MSYS*|CYGWIN*) os=win32 ;;
*) echo -e "${RED}✗${NC} unsupported OS $(uname -s)" >&2; exit 1 ;;
esac
if [ "$os" = win32 ]; then
# PROCESSOR_ARCHITECTURE lies under an emulated shell (x64 msys on a
# WoA box reports AMD64); the registry carries the machine's truth.
winarch="$(MSYS2_ARG_CONV_EXCL='*' reg.exe query 'HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' /v PROCESSOR_ARCHITECTURE 2>/dev/null | tr -d '\r' | awk '/PROCESSOR_ARCHITECTURE/ {print $NF}')"
case "${winarch:-${PROCESSOR_ARCHITECTURE:-}}" in
ARM64) arch=arm64 ;;
*) arch=x64 ;;
esac
else
case "$(uname -m)" in
arm64|aarch64) arch=arm64 ;;
x86_64|amd64) arch=x64 ;;
*) echo -e "${RED}✗${NC} unsupported arch $(uname -m)" >&2; exit 1 ;;
esac
fi
target="$os-$arch"
# lock.json is machine-written (sorted keys, 2-space indent): read the uv
# pin's version + this target's url/sha256 with awk — no python yet.
pin() { # $1 = field (url | sha256)
awk -F '"' -v target="$target" -v field="$1" '
/^ "uv": \{/ { in_uv = 1 }
in_uv && /^ }/ { exit }
in_uv && /^ "/ { in_t = ($2 == target) }
in_t && $2 == field { print $4; exit }' "$lock"
}
uv_version="$(awk -F '"' '
/^ "uv": \{/ { in_uv = 1 }
in_uv && /^ }/ { exit }
in_uv && $2 == "version" { print $4; exit }' "$lock")"
py_version="$(awk -F '"' '
/^ "python": \{/ { in_py = 1 }
in_py && /^ }/ { exit }
in_py && $2 == "version" { print $4; exit }' "$lock" \
| cut -d+ -f1 | cut -d. -f1,2)"
[ -n "$uv_version" ] || { echo -e "${RED}✗${NC} no uv pin in pm/lock.json" >&2; exit 1; }
store="${HERMES_RUNTIME_DIR:-$HOME/.hermes/tools}"
entry="$store/uv-$uv_version-$target"
uv="$entry/uv"; [ "$os" = win32 ] && uv="$entry/uv.exe"
if [ -x "$uv" ]; then
echo -e "${GREEN}✓${NC} pinned uv found ($("$uv" --version 2>/dev/null))"
else
url="$(pin url)"; sha="$(pin sha256)"
[ -n "$url" ] && [ -n "$sha" ] || { echo -e "${RED}✗${NC} no uv artifact for $target" >&2; exit 1; }
echo -e "${CYAN}→${NC} Staging pinned uv $uv_version ($target) into the pm store..."
mkdir -p "$store"
tmp="$(mktemp -d "$store/.bootstrap-XXXXXX")"; trap 'rm -rf "$tmp"' EXIT
archive="$tmp/${url##*/}"
fetch_pinned() {
if curl -fsSL -o "$2" "$1"; then return 0; else _curl_status=$?; fi
case "$_curl_status" in 5|6|7|18|22|28|52|55|56) ;; *) return "$_curl_status" ;; esac
local mirror; mirror="$(mirror_url_for "$sha")" || return 1
curl -fsSL -o "$2" "$mirror" || return 1
}
if ! fetch_pinned "$url" "$archive"; then
_tried="$url"
if _m="$(mirror_url_for "$sha")"; then _tried="$_tried or $_m"; fi
echo -e "${RED}✗${NC} failed to download pinned uv from $_tried" >&2
exit 1
fi
got="$( (sha256sum "$archive" 2>/dev/null || shasum -a 256 "$archive") | cut -d' ' -f1 | tr -d '\\')"
[ "$got" = "$sha" ] || { echo -e "${RED}✗${NC} sha256 mismatch for uv (got $got, pinned $sha)" >&2; exit 1; }
mkdir -p "$tmp/tree"
case "$archive" in
*.zip) unzip -q "$archive" -d "$tmp/tree" ;;
*) tar -xzf "$archive" -C "$tmp/tree" ;;
esac
# flatten a single wrapping dir (uv tarballs ship uv-<triple>/uv)
inner="$(find "$tmp/tree" -mindepth 1 -maxdepth 1)"
if [ "$(printf '%s\n' "$inner" | wc -l)" = 1 ] && [ -d "$inner" ]; then
mv "$inner" "$tmp/entry"
else
mv "$tmp/tree" "$tmp/entry"
fi
rm -rf "$entry"
mv "$tmp/entry" "$entry"
echo -e "${GREEN}✓${NC} uv installed ($("$uv" --version 2>/dev/null))"
fi
# ============================================================================
# Delegate to pm: python + venv + tool store + hash-verified venv sync
# ============================================================================
echo -e "${CYAN}→${NC} Installing python + tools + dependencies via pm (hash-verified via uv.lock)..."
echo -e "${CYAN}→${NC} (first run on a fresh checkout can take 1-5 minutes)"
# PM can replace its uv entry only after the bootstrap uv has exited.
"$uv" python install --no-bin "$py_version"
boot_py="$("$uv" python find --managed-python "$py_version")"
boot_py="${boot_py%$'\r'}"
if ! "$boot_py" -m pm.cli install; then
echo -e "${RED}✗${NC} pm install failed — see output above."
exit 1
fi
echo -e "${GREEN}✓${NC} Tools + dependencies installed (hash-verified via pm + uv.lock)"
if [ "$runtime_only" = true ]; then
exit 0
fi
# ============================================================================
# Environment file
# ============================================================================
if [ ! -f ".env" ]; then
if [ -f ".env.example" ]; then
cp .env.example .env
# .env holds API keys — restrict to owner-only access (matches
# scripts/install.sh which already chmods 600 after creation).
chmod 600 .env 2>/dev/null || true
echo -e "${GREEN}✓${NC} Created .env from template"
fi
else
# Tighten an existing .env's perms in case it was created elsewhere
# under a permissive umask.
chmod 600 .env 2>/dev/null || true
echo -e "${GREEN}✓${NC} .env exists"
fi
# ============================================================================
# Publish user-facing launchers
# ============================================================================
echo -e "${CYAN}→${NC} Setting up hermes command..."
# Reuse the bootstrap interpreter only to run the shared launcher writer.
bin_dir="$HOME/.local/bin"
if [ "$os" = win32 ]; then
bin_dir="$(cygpath -am "${HERMES_HOME:-${LOCALAPPDATA:-$HOME/AppData/Local}/hermes}/bin")"
fi
if ! "$boot_py" -I -X utf8 hermes_cli/_launchers.py "$bin_dir"; then
echo -e "${RED}✗${NC} launcher publication failed" >&2
exit 1
fi
echo -e "${GREEN}✓${NC} Published Hermes commands in $bin_dir"
if [ "$os" != win32 ]; then
# Determine the appropriate shell config file
SHELL_CONFIG=""
if [[ "$SHELL" == *"zsh"* ]]; then
SHELL_CONFIG="$HOME/.zshrc"
elif [[ "$SHELL" == *"bash"* ]]; then
SHELL_CONFIG="$HOME/.bashrc"
[ ! -f "$SHELL_CONFIG" ] && SHELL_CONFIG="$HOME/.bash_profile"
else
# Fallback to checking existing files
if [ -f "$HOME/.zshrc" ]; then
SHELL_CONFIG="$HOME/.zshrc"
elif [ -f "$HOME/.bashrc" ]; then
SHELL_CONFIG="$HOME/.bashrc"
elif [ -f "$HOME/.bash_profile" ]; then
SHELL_CONFIG="$HOME/.bash_profile"
fi
fi
if [ -n "$SHELL_CONFIG" ]; then
# Touch the file just in case it doesn't exist yet but was selected
touch "$SHELL_CONFIG" 2>/dev/null || true
if ! echo "$PATH" | tr ':' '\n' | grep -q "^$HOME/.local/bin$"; then
if ! grep -q '\.local/bin' "$SHELL_CONFIG" 2>/dev/null; then
echo "" >> "$SHELL_CONFIG"
echo "# Hermes Agent — ensure ~/.local/bin is on PATH" >> "$SHELL_CONFIG"
echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$SHELL_CONFIG"
echo -e "${GREEN}✓${NC} Added ~/.local/bin to PATH in $SHELL_CONFIG"
else
echo -e "${GREEN}✓${NC} ~/.local/bin already in $SHELL_CONFIG"
fi
else
echo -e "${GREEN}✓${NC} ~/.local/bin already on PATH"
fi
fi
fi
# ============================================================================
# Seed bundled skills into ~/.hermes/skills/
# ============================================================================
HERMES_SKILLS_DIR="${HERMES_HOME:-$HOME/.hermes}/skills"
mkdir -p "$HERMES_SKILLS_DIR"
echo ""
echo "Syncing bundled skills to ~/.hermes/skills/ ..."
if "$boot_py" -m tools.skills_sync 2>/dev/null; then
echo -e "${GREEN}✓${NC} Skills synced"
else
# Fallback: copy if sync script fails (missing deps, etc.)
if [ -d "$SCRIPT_DIR/skills" ]; then
cp -rn "$SCRIPT_DIR/skills/"* "$HERMES_SKILLS_DIR/" 2>/dev/null || true
echo -e "${GREEN}✓${NC} Skills copied"
fi
fi
# ============================================================================
# Done
# ============================================================================
echo ""
echo -e "${GREEN}✓ Setup complete!${NC}"
echo ""
echo "Next steps:"
echo ""
echo " 1. Activate the dev environment (venv-style, in THIS shell):"
echo " source ./activate"
echo ""
echo " 2. Run the setup wizard to configure API keys:"
echo " hermes setup"
echo ""
echo " 3. Start chatting:"
echo " hermes"
echo ""
echo "Other commands:"
echo " hermes pm install # Re-run the tool + dependency install"
echo " hermes status # Check configuration"
echo " hermes doctor # Diagnose issues"
echo " deactivate # Undo the activation (restore PATH etc.)"
echo ""