Files
hermes-workspace/install.sh
Eric f5fc172cc0 fix(workspace): finish remaining installer and tasks cleanup (#433)
- make install.sh resilient when pnpm is only available via corepack
- cap pnpm build heap for low-memory installs
- relabel Workspace Kanban sidebar entry to Tasks and clarify copy

Co-authored-by: Aurora release bot <release@outsourc-e.com>
2026-05-13 22:53:23 -04:00

302 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# Hermes Workspace — one-liner installer
#
# Usage:
# curl -fsSL https://raw.githubusercontent.com/outsourc-e/hermes-workspace/main/install.sh | bash
#
# What it does:
# 1. Verifies Node 22+, git, pnpm
# 2. Installs hermes-agent via Nous's official upstream installer
# 3. Clones hermes-workspace
# 4. Sets up .env, enables the Hermes API server, installs deps,
# and links bundled skills
#
# Re-runnable. Will skip anything already installed.
set -euo pipefail
REPO_URL="${REPO_URL:-https://github.com/outsourc-e/hermes-workspace.git}"
INSTALL_DIR="${INSTALL_DIR:-$HOME/hermes-workspace}"
GATEWAY_PORT="${GATEWAY_PORT:-8642}"
NOUS_INSTALLER_URL="${NOUS_INSTALLER_URL:-https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh}"
# ─── helpers ──────────────────────────────────────────────────────────────
cyan() { printf "\033[36m%s\033[0m\n" "$*"; }
green() { printf "\033[32m%s\033[0m\n" "$*"; }
yellow() { printf "\033[33m%s\033[0m\n" "$*"; }
red() { printf "\033[31m%s\033[0m\n" "$*"; }
bold() { printf "\033[1m%s\033[0m\n" "$*"; }
need() { command -v "$1" &>/dev/null || { red "Missing: $1"; red "$2"; exit 1; }; }
banner() {
cat <<'EOF'
╭────────────────────────────────────────────╮
│ HERMES WORKSPACE — zero-fork installer │
│ outsourc-e/hermes-workspace │
╰────────────────────────────────────────────╯
EOF
}
# ensure_path: prepend a dir to PATH for this shell if it's not already there
ensure_path() {
local candidate="$1"
[[ -d "$candidate" ]] || return 0
case ":$PATH:" in
*":$candidate:"*) ;;
*) export PATH="$candidate:$PATH" ;;
esac
}
pnpm_cmd() {
if command -v pnpm &>/dev/null; then
pnpm "$@"
return
fi
if command -v corepack &>/dev/null && corepack pnpm --version &>/dev/null; then
corepack pnpm "$@"
return
fi
red "pnpm is not available in this shell."
red "Try opening a new shell, or install pnpm manually: https://pnpm.io/installation"
exit 1
}
ensure_env_key() {
local file="$1"
local key="$2"
local value="$3"
local tmp
mkdir -p "$(dirname "$file")"
tmp="$(mktemp)"
if [[ -f "$file" ]]; then
awk -v key="$key" -v value="$value" '
BEGIN { found = 0 }
index($0, key "=") == 1 {
print key "=" value
found = 1
next
}
{ print }
END {
if (!found) {
if (NR > 0) print ""
print key "=" value
}
}
' "$file" > "$tmp"
else
printf '%s=%s\n' "$key" "$value" > "$tmp"
fi
mv "$tmp" "$file"
}
# ─── preflight ────────────────────────────────────────────────────────────
banner
cyan "→ Checking prerequisites…"
need node "Install Node 22+: https://nodejs.org/"
node_major=$(node -v | sed -E 's/v([0-9]+).*/\1/')
if [[ "$node_major" -lt 22 ]]; then
red "Node $node_major detected; need 22+."
exit 1
fi
green " Node $(node -v)"
need git "Install git: https://git-scm.com/"
green " git $(git --version | awk '{print $3}')"
need curl "Install curl (usually: apt install curl / brew install curl)"
green " curl ✓"
if ! command -v pnpm &>/dev/null; then
yellow " pnpm not found — installing via corepack…"
if command -v corepack &>/dev/null; then
corepack enable 2>/dev/null || true
corepack prepare pnpm@latest --activate 2>/dev/null || true
fi
if ! command -v pnpm &>/dev/null && ! (command -v corepack &>/dev/null && corepack pnpm --version &>/dev/null); then
npm install -g pnpm
fi
fi
green " pnpm $(pnpm_cmd --version)"
# ─── install hermes-agent (delegate to Nous upstream installer) ──────────
# hermes-agent is NOT on PyPI. It installs from source via Nous's own
# script, which handles PEP 668, uv, Python toolchain, Termux, etc. We
# only need to ensure `hermes` ends up on PATH before continuing.
cyan "→ Installing hermes-agent (via Nous upstream installer)…"
# Pick up hermes if it was installed in a prior run but not on PATH yet
ensure_path "$HOME/.hermes/bin"
ensure_path "$HOME/.local/bin"
if command -v hermes &>/dev/null; then
green " hermes-agent already installed ✓ ($(command -v hermes))"
else
yellow " Delegating to: $NOUS_INSTALLER_URL"
if ! curl -fsSL "$NOUS_INSTALLER_URL" | bash; then
red " Nous installer failed. See its output above for details."
red " You can retry manually:"
red " curl -fsSL $NOUS_INSTALLER_URL | bash"
exit 1
fi
# Nous typically installs `hermes` to ~/.hermes/bin or ~/.local/bin
ensure_path "$HOME/.hermes/bin"
ensure_path "$HOME/.local/bin"
if ! command -v hermes &>/dev/null; then
red " hermes-agent installed, but 'hermes' is not on PATH in this shell."
yellow " Open a new shell (or: source ~/.bashrc / ~/.zshrc) and re-run:"
yellow " curl -fsSL https://hermes-workspace.com/install.sh | bash"
exit 1
fi
green " hermes-agent installed ✓ ($(command -v hermes))"
fi
# ─── clone workspace ──────────────────────────────────────────────────────
cyan "→ Cloning hermes-workspace…"
if [[ -d "$INSTALL_DIR/.git" ]]; then
yellow " $INSTALL_DIR exists; pulling latest"
git -C "$INSTALL_DIR" pull --ff-only
elif [[ -e "$INSTALL_DIR" ]]; then
red "Path exists but is not a git repo: $INSTALL_DIR"
red "Move/remove it or set INSTALL_DIR=..."
exit 1
else
git clone "$REPO_URL" "$INSTALL_DIR"
fi
cd "$INSTALL_DIR"
green " Workspace ready at $INSTALL_DIR"
# ─── env + install ────────────────────────────────────────────────────────
cyan "→ Configuring .env…"
if [[ ! -f .env ]]; then
cp .env.example .env
fi
ensure_env_key "$INSTALL_DIR/.env" "HERMES_API_URL" "http://127.0.0.1:${GATEWAY_PORT}"
green " .env ready ✓"
cyan "→ Enabling Hermes API server…"
HERMES_ENV_PATH="$(hermes config env-path 2>/dev/null || true)"
if [[ -z "$HERMES_ENV_PATH" ]]; then
HERMES_ENV_PATH="$HOME/.hermes/.env"
fi
ensure_env_key "$HERMES_ENV_PATH" "API_SERVER_ENABLED" "true"
green " Hermes env updated: $HERMES_ENV_PATH"
# Guard against a common foot-gun: users editing ~/.hermes/.env by hand and
# writing env var names without underscores (APISERVERENABLED vs
# API_SERVER_ENABLED). The gateway reads exact names — typos are silently
# ignored, which produces a "gateway starts but API server never binds"
# failure that's hard to diagnose from the UI.
if [[ -f "$HERMES_ENV_PATH" ]]; then
SUSPICIOUS=$(grep -E "^(API[A-Z]+|HERMES[A-Z]+)=" "$HERMES_ENV_PATH" 2>/dev/null \
| grep -vE "^(API_|HERMES_)" || true)
if [[ -n "$SUSPICIOUS" ]]; then
yellow ""
yellow "⚠ Found env var names missing underscores in $HERMES_ENV_PATH:"
echo "$SUSPICIOUS" | sed 's/^/ /'
yellow " The gateway reads names with underscores (API_SERVER_ENABLED,"
yellow " not APISERVERENABLED). These lines will be silently ignored."
yellow " Fix them and run: hermes gateway run --replace"
yellow ""
fi
fi
cyan "→ Installing npm deps (pnpm install)…"
pnpm_cmd install --silent
green " deps installed ✓"
# ─── seed Hermes skills (Conductor needs workspace-dispatch) ─────────────
cyan "→ Linking bundled skills into ~/.hermes/skills…"
HERMES_SKILLS_DIR="$HOME/.hermes/skills"
mkdir -p "$HERMES_SKILLS_DIR"
if [[ -d "$INSTALL_DIR/skills" ]]; then
for skill_path in "$INSTALL_DIR/skills"/*/; do
skill_name=$(basename "$skill_path")
target="$HERMES_SKILLS_DIR/$skill_name"
if [[ -e "$target" || -L "$target" ]]; then
continue
fi
ln -sf "$skill_path" "$target" 2>/dev/null && \
green " linked $skill_name" || true
done
fi
# ─── macOS LaunchAgent (plist) ───────────────────────────────────────────
# Best-effort convenience for local macOS installs. This keeps the source of
# truth in-repo and makes sure launchd runs server-entry.js (the thin HTTP
# wrapper), not dist/server/server.js directly.
if [[ "$(uname -s)" == "Darwin" ]]; then
cyan "→ Installing macOS LaunchAgent (com.hermes.workspace)…"
PLIST_TEMPLATE="$INSTALL_DIR/macos/com.hermes.workspace.plist.template"
PLIST_DEST="$HOME/Library/LaunchAgents/com.hermes.workspace.plist"
mkdir -p "$HOME/Library/LaunchAgents"
NODE_BIN="$(command -v node)"
HERMES_PORT="${PORT:-3000}"
HERMES_API_GATEWAY="http://127.0.0.1:${GATEWAY_PORT}"
TOKEN=""
if [[ -f "$HOME/.hermes/.env" ]]; then
TOKEN="$(grep -E '^(HERMES_API_TOKEN|CLAUDE_API_TOKEN)=' "$HOME/.hermes/.env" | head -1 | cut -d= -f2- | tr -d '"' || true)"
fi
if [[ -z "$TOKEN" && -f "$INSTALL_DIR/.env" ]]; then
TOKEN="$(grep -E '^(HERMES_API_TOKEN|CLAUDE_API_TOKEN)=' "$INSTALL_DIR/.env" | head -1 | cut -d= -f2- | tr -d '"' || true)"
fi
sed \
-e "s|{{NODE_BIN}}|${NODE_BIN}|g" \
-e "s|{{INSTALL_DIR}}|${INSTALL_DIR}|g" \
-e "s|{{PORT}}|${HERMES_PORT}|g" \
-e "s|{{HERMES_API_URL}}|${HERMES_API_GATEWAY}|g" \
-e "s|{{HERMES_API_TOKEN}}|${TOKEN}|g" \
"$PLIST_TEMPLATE" > "$PLIST_DEST"
launchctl unload "$PLIST_DEST" 2>/dev/null || true
if launchctl load -w "$PLIST_DEST" 2>/dev/null; then
green " LaunchAgent loaded ✓ (com.hermes.workspace)"
else
yellow " Could not load LaunchAgent now — it will still be available for next login."
fi
green " Plist installed: $PLIST_DEST"
fi
# ─── done ─────────────────────────────────────────────────────────────────
bold ""
bold "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
green " Install complete!"
bold "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cat <<EOF
Next steps (two terminals):
1) Start the Hermes Agent gateway:
hermes gateway run
(first run may prompt for hermes setup)
2) Start the workspace UI:
cd $INSTALL_DIR && pnpm dev
3) Open http://localhost:3000
If the gateway was already running before this install,
restart it so API_SERVER_ENABLED=true takes effect.
EOF
cyan "Happy building. 🚀"