Introduce the pm store: a unified, hash-verified package store that
replaces lazy_deps and the old installer's ad-hoc tool downloads.
Store tools are provisioned on PATH (ffmpeg, node/npm via pinned uv),
with a resumable 8-way downloader, verify() returning failure reasons,
and adopt() made EPERM-safe. chromium ships in the payload for every
target. The 3600-line install.sh is replaced by a staged bootstrapper
(heavy deps are pm's job after this); setup-hermes.sh, Dockerfile and
nix pin tables are rewired onto the store. Old install-script tests,
lazy_deps/managed_uv/build_info, and the ps1/bash installer test
batteries are removed with the machinery they tested.
Rebuilt from ethie/pm onto upstream/main (ac6c8028e0) after the
utf-8-sig sweep. 16 hot files (main also churned them) hand-merged:
platform adapters, main.py, electron/main.ts, tui_gateway/server.py,
cua_backend, installer-tests workflow, install.sh (full rewrite),
setup-hermes.sh, plugins doc.
257 lines
10 KiB
Bash
Executable File
257 lines
10 KiB
Bash
Executable File
#!/bin/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. Provision Python + the venv + hash-verified dependency sync by running
|
|
# `python -m pm.cli install` through that uv (the same code path
|
|
# `hermes pm install` uses). pyproject.toml + uv.lock are the single
|
|
# authority for pins (see tests/test_project_metadata.py).
|
|
# 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
|
|
|
|
# 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; }
|
|
|
|
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="$(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 -v target="$target" -v field="$1" '
|
|
/^ "uv": \{/ { in_uv = 1 }
|
|
in_uv && $0 ~ "^ \"" target "\": \\{" { in_t = 1 }
|
|
in_t && $0 ~ "^ \"" field "\":" {
|
|
gsub(/.*: "|,?$/, ""); print; exit
|
|
}' "$lock"
|
|
}
|
|
uv_version="$(awk '
|
|
/^ "uv": \{/ { in_uv = 1 }
|
|
in_uv && /^ "version":/ { gsub(/.*: "|,?$/, ""); print; exit }' "$lock")"
|
|
py_version="$(awk '
|
|
/^ "python": \{/ { in_py = 1 }
|
|
in_py && /^ "version":/ { gsub(/.*: "|"$|",$/, ""); print; 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##*/}"
|
|
curl -fsSL -o "$archive" "$url"
|
|
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)"
|
|
if ! "$uv" run --no-project --python "${py_version:-3.11}" python -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)"
|
|
|
|
# ============================================================================
|
|
# 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
|
|
|
|
# ============================================================================
|
|
# PATH setup — symlink hermes into a user-facing bin dir
|
|
# ============================================================================
|
|
|
|
echo -e "${CYAN}→${NC} Setting up hermes command..."
|
|
|
|
# pm installs the venv; find its python across layouts (posix venv vs win).
|
|
PYBIN=""
|
|
for candidate in "$SCRIPT_DIR/venv/bin/python" "$SCRIPT_DIR/venv/Scripts/python.exe"; do
|
|
[ -x "$candidate" ] && { PYBIN="$candidate"; break; }
|
|
done
|
|
|
|
HERMES_BIN=""
|
|
for candidate in "$SCRIPT_DIR/venv/bin/hermes" "$SCRIPT_DIR/venv/Scripts/hermes.exe"; do
|
|
[ -e "$candidate" ] && { HERMES_BIN="$candidate"; break; }
|
|
done
|
|
|
|
if [ -n "$HERMES_BIN" ] && [ "$os" != win32 ]; then
|
|
mkdir -p "$HOME/.local/bin"
|
|
ln -sf "$HERMES_BIN" "$HOME/.local/bin/hermes"
|
|
echo -e "${GREEN}✓${NC} Symlinked hermes → ~/.local/bin/hermes"
|
|
fi
|
|
|
|
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 [ -n "$PYBIN" ] && "$PYBIN" "$SCRIPT_DIR/tools/skills_sync.py" 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 ""
|