Preserve upstream fixes without restoring retired dependency installers. Run configured-feature checks in the selected build interpreter. Reuse a supported base Python during bootstrap, and preserve durable backup media. Refresh the dependency lock through PM. Keep the frozen historical import surface unchanged. Adapt incoming native tests to the platform markers. Verification: the incoming 86-file pass found two fixture mismatches; both passed after correction. Targeted PM/update/compatibility checks, Electron and renderer typechecks, and desktop tests passed. Native Windows/macOS update journeys and the full suite remain unrun.
287 lines
11 KiB
Bash
Executable File
287 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. Match object
|
|
# keys, not indentation — same rule as pin() below.
|
|
mirror_origin="$(awk -F '"' '$2 == "origin" { print $4; exit }' "$SCRIPT_DIR/pm/artifact-mirror.json")"
|
|
mirror_prefix="$(awk -F '"' '$2 == "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"
|
|
|
|
# The machine-written lock has one member per line. Follow object names and
|
|
# braces, not indentation, to read pins before Python is available.
|
|
pin() { # $1 = field (url | sha256 | version), $2 = package (default: uv)
|
|
awk -F '"' -v package="${2:-uv}" -v target="$target" -v field="$1" '
|
|
/^[[:space:]]*("[^"]+"[[:space:]]*:[[:space:]]*)?\{[[:space:]]*$/ {
|
|
path[++depth] = $2; next
|
|
}
|
|
/^[[:space:]]*}[[:space:]]*,?[[:space:]]*$/ {
|
|
delete path[depth--]; next
|
|
}
|
|
path[2] == "packages" && path[3] == package && $2 == field &&
|
|
((field == "version" && depth == 3) ||
|
|
(depth == 5 && path[4] == "artifacts" && path[5] == target)) {
|
|
print $4; exit
|
|
}' "$lock"
|
|
}
|
|
uv_version="$(pin version)"
|
|
py_version="$(pin version python | 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 ""
|