mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat: lazy bootstrap node
This commit is contained in:
parent
39b1336d1f
commit
c4b9750bc1
3 changed files with 303 additions and 2 deletions
|
|
@ -141,11 +141,18 @@ See `hermes claw migrate --help` for all options, or use the `openclaw-migration
|
||||||
|
|
||||||
We welcome contributions! See the [Contributing Guide](https://hermes-agent.nousresearch.com/docs/developer-guide/contributing) for development setup, code style, and PR process.
|
We welcome contributions! See the [Contributing Guide](https://hermes-agent.nousresearch.com/docs/developer-guide/contributing) for development setup, code style, and PR process.
|
||||||
|
|
||||||
Quick start for contributors:
|
Quick start for contributors — clone and go with `setup-hermes.sh`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/NousResearch/hermes-agent.git
|
git clone https://github.com/NousResearch/hermes-agent.git
|
||||||
cd hermes-agent
|
cd hermes-agent
|
||||||
|
./setup-hermes.sh # installs uv, creates venv, installs .[all], symlinks ~/.local/bin/hermes
|
||||||
|
./hermes # auto-detects the venv, no need to `source` first
|
||||||
|
```
|
||||||
|
|
||||||
|
Manual path (equivalent to the above):
|
||||||
|
|
||||||
|
```bash
|
||||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
uv venv venv --python 3.11
|
uv venv venv --python 3.11
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
|
|
|
||||||
|
|
@ -789,8 +789,63 @@ def _hermes_ink_bundle_stale(tui_dir: Path) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_tui_node() -> None:
|
||||||
|
"""Make sure `node` + `npm` are on PATH for the TUI.
|
||||||
|
|
||||||
|
If either is missing and scripts/lib/node-bootstrap.sh is available, source
|
||||||
|
it and call `ensure_node` (fnm/nvm/proto/brew/bundled cascade). After
|
||||||
|
install, capture the resolved node binary path from the bash subprocess
|
||||||
|
and prepend its directory to os.environ["PATH"] so shutil.which finds the
|
||||||
|
new binaries in this Python process — regardless of which version manager
|
||||||
|
was used (nvm, fnm, proto, brew, or the bundled fallback).
|
||||||
|
|
||||||
|
Idempotent no-op when node+npm are already discoverable. Set
|
||||||
|
``HERMES_SKIP_NODE_BOOTSTRAP=1`` to disable auto-install.
|
||||||
|
"""
|
||||||
|
if shutil.which("node") and shutil.which("npm"):
|
||||||
|
return
|
||||||
|
if os.environ.get("HERMES_SKIP_NODE_BOOTSTRAP"):
|
||||||
|
return
|
||||||
|
|
||||||
|
helper = PROJECT_ROOT / "scripts" / "lib" / "node-bootstrap.sh"
|
||||||
|
if not helper.is_file():
|
||||||
|
return
|
||||||
|
|
||||||
|
hermes_home = os.environ.get("HERMES_HOME") or str(Path.home() / ".hermes")
|
||||||
|
try:
|
||||||
|
# Helper writes logs to stderr; we ask bash to print `command -v node`
|
||||||
|
# on stdout once ensure_node succeeds. Subshell PATH edits don't leak
|
||||||
|
# back into Python, so the stdout capture is the bridge.
|
||||||
|
result = subprocess.run(
|
||||||
|
["bash", "-c", f'source "{helper}" >&2 && ensure_node >&2 && command -v node'],
|
||||||
|
env={**os.environ, "HERMES_HOME": hermes_home},
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
return
|
||||||
|
|
||||||
|
parts = os.environ.get("PATH", "").split(os.pathsep)
|
||||||
|
extras: list[Path] = []
|
||||||
|
|
||||||
|
resolved = (result.stdout or "").strip()
|
||||||
|
if resolved:
|
||||||
|
extras.append(Path(resolved).resolve().parent)
|
||||||
|
|
||||||
|
extras.extend([Path(hermes_home) / "node" / "bin", Path.home() / ".local" / "bin"])
|
||||||
|
|
||||||
|
for extra in extras:
|
||||||
|
s = str(extra)
|
||||||
|
if extra.is_dir() and s not in parts:
|
||||||
|
parts.insert(0, s)
|
||||||
|
os.environ["PATH"] = os.pathsep.join(parts)
|
||||||
|
|
||||||
|
|
||||||
def _make_tui_argv(tui_dir: Path, tui_dev: bool) -> tuple[list[str], Path]:
|
def _make_tui_argv(tui_dir: Path, tui_dev: bool) -> tuple[list[str], Path]:
|
||||||
"""Ink TUI: --dev → tsx src; else node dist (HERMES_TUI_DIR or ui-tui, build when stale)."""
|
"""Ink TUI: --dev → tsx src; else node dist (HERMES_TUI_DIR or ui-tui, build when stale)."""
|
||||||
|
_ensure_tui_node()
|
||||||
|
|
||||||
def _node_bin(bin: str)-> str:
|
def _node_bin(bin: str)-> str:
|
||||||
path = shutil.which(bin)
|
path = shutil.which(bin)
|
||||||
if not path:
|
if not path:
|
||||||
|
|
@ -809,6 +864,7 @@ def _make_tui_argv(tui_dir: Path, tui_dev: bool) -> tuple[list[str], Path]:
|
||||||
|
|
||||||
npm = _node_bin("npm")
|
npm = _node_bin("npm")
|
||||||
if _tui_need_npm_install(tui_dir):
|
if _tui_need_npm_install(tui_dir):
|
||||||
|
if not os.environ.get("HERMES_QUIET"):
|
||||||
print("Installing TUI dependencies…")
|
print("Installing TUI dependencies…")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[npm, "install", "--silent", "--no-fund", "--no-audit", "--progress=false"],
|
[npm, "install", "--silent", "--no-fund", "--no-audit", "--progress=false"],
|
||||||
|
|
|
||||||
238
scripts/lib/node-bootstrap.sh
Normal file
238
scripts/lib/node-bootstrap.sh
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================================
|
||||||
|
# scripts/lib/node-bootstrap.sh
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Sourceable helper: ensure Node.js >= MIN_VERSION is available for the TUI
|
||||||
|
# (React + Ink), browser tools, and the WhatsApp bridge.
|
||||||
|
#
|
||||||
|
# Strategy (first hit wins — respects the user's existing tooling):
|
||||||
|
# 1. modern `node` already on PATH
|
||||||
|
# 2. ~/.hermes/node/ from a prior Hermes-managed install
|
||||||
|
# 3. fnm, proto, nvm (in that order) if the user already uses a version manager
|
||||||
|
# 4. Termux `pkg`, macOS Homebrew
|
||||||
|
# 5. pinned nodejs.org tarball into ~/.hermes/node/ (always works, zero shell rc edits)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# source scripts/lib/node-bootstrap.sh
|
||||||
|
# ensure_node # returns 0 on success, non-zero on failure
|
||||||
|
# if [ "$HERMES_NODE_AVAILABLE" = true ]; then ...; fi
|
||||||
|
#
|
||||||
|
# Env inputs (set before sourcing to override defaults):
|
||||||
|
# HERMES_NODE_MIN_VERSION (default: 20) — accepted on PATH
|
||||||
|
# HERMES_NODE_TARGET_MAJOR (default: 22) — installed when we install
|
||||||
|
# HERMES_HOME (default: $HOME/.hermes)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
HERMES_NODE_MIN_VERSION="${HERMES_NODE_MIN_VERSION:-20}"
|
||||||
|
HERMES_NODE_TARGET_MAJOR="${HERMES_NODE_TARGET_MAJOR:-22}"
|
||||||
|
HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
|
||||||
|
HERMES_NODE_AVAILABLE=false
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Logging — prefer the host script's log_* helpers when present
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_nb_log() { declare -F log_info >/dev/null 2>&1 && log_info "$*" || printf '→ %s\n' "$*" >&2; }
|
||||||
|
_nb_ok() { declare -F log_success >/dev/null 2>&1 && log_success "$*" || printf '✓ %s\n' "$*" >&2; }
|
||||||
|
_nb_warn() { declare -F log_warn >/dev/null 2>&1 && log_warn "$*" || printf '⚠ %s\n' "$*" >&2; }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Platform + version helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_nb_is_termux() {
|
||||||
|
[ -n "${TERMUX_VERSION:-}" ] || [[ "${PREFIX:-}" == *"com.termux/files/usr"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
_nb_node_major() {
|
||||||
|
local v
|
||||||
|
v=$(node --version 2>/dev/null | sed 's/^v//' | cut -d. -f1)
|
||||||
|
[[ "$v" =~ ^[0-9]+$ ]] && echo "$v" || echo 0
|
||||||
|
}
|
||||||
|
|
||||||
|
_nb_have_modern_node() {
|
||||||
|
command -v node >/dev/null 2>&1 || return 1
|
||||||
|
[ "$(_nb_node_major)" -ge "$HERMES_NODE_MIN_VERSION" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Version-manager paths — respect what the user already uses
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_nb_try_fnm() {
|
||||||
|
command -v fnm >/dev/null 2>&1 || return 1
|
||||||
|
_nb_log "fnm detected — installing Node $HERMES_NODE_TARGET_MAJOR..."
|
||||||
|
eval "$(fnm env 2>/dev/null)" || true
|
||||||
|
fnm install "$HERMES_NODE_TARGET_MAJOR" >/dev/null 2>&1 || return 1
|
||||||
|
fnm use "$HERMES_NODE_TARGET_MAJOR" >/dev/null 2>&1 || return 1
|
||||||
|
_nb_have_modern_node || return 1
|
||||||
|
_nb_ok "Node $(node --version) activated via fnm"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
_nb_try_proto() {
|
||||||
|
command -v proto >/dev/null 2>&1 || return 1
|
||||||
|
_nb_log "proto detected — installing Node $HERMES_NODE_TARGET_MAJOR..."
|
||||||
|
proto install node "$HERMES_NODE_TARGET_MAJOR" >/dev/null 2>&1 || return 1
|
||||||
|
_nb_have_modern_node || return 1
|
||||||
|
_nb_ok "Node $(node --version) activated via proto"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
_nb_try_nvm() {
|
||||||
|
local nvm_sh="${NVM_DIR:-$HOME/.nvm}/nvm.sh"
|
||||||
|
[ -s "$nvm_sh" ] || return 1
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
\. "$nvm_sh" >/dev/null 2>&1 || return 1
|
||||||
|
_nb_log "nvm detected — installing Node $HERMES_NODE_TARGET_MAJOR..."
|
||||||
|
nvm install "$HERMES_NODE_TARGET_MAJOR" >/dev/null 2>&1 || return 1
|
||||||
|
nvm use "$HERMES_NODE_TARGET_MAJOR" >/dev/null 2>&1 || return 1
|
||||||
|
_nb_have_modern_node || return 1
|
||||||
|
_nb_ok "Node $(node --version) activated via nvm"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Platform package managers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_nb_try_termux_pkg() {
|
||||||
|
_nb_is_termux || return 1
|
||||||
|
_nb_log "Installing Node.js via pkg..."
|
||||||
|
pkg install -y nodejs >/dev/null 2>&1 || return 1
|
||||||
|
_nb_have_modern_node || return 1
|
||||||
|
_nb_ok "Node $(node --version) installed via pkg"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
_nb_try_brew() {
|
||||||
|
[ "$(uname -s)" = "Darwin" ] || return 1
|
||||||
|
command -v brew >/dev/null 2>&1 || return 1
|
||||||
|
_nb_log "Installing Node via Homebrew..."
|
||||||
|
brew install "node@${HERMES_NODE_TARGET_MAJOR}" >/dev/null 2>&1 \
|
||||||
|
|| brew install node >/dev/null 2>&1 \
|
||||||
|
|| return 1
|
||||||
|
brew link --overwrite --force "node@${HERMES_NODE_TARGET_MAJOR}" >/dev/null 2>&1 || true
|
||||||
|
_nb_have_modern_node || return 1
|
||||||
|
_nb_ok "Node $(node --version) installed via Homebrew"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Bundled binary fallback — always works, no shell rc edits
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_nb_install_bundled_node() {
|
||||||
|
local arch node_arch os_name node_os
|
||||||
|
arch=$(uname -m)
|
||||||
|
case "$arch" in
|
||||||
|
x86_64) node_arch="x64" ;;
|
||||||
|
aarch64|arm64) node_arch="arm64" ;;
|
||||||
|
armv7l) node_arch="armv7l" ;;
|
||||||
|
*)
|
||||||
|
_nb_warn "Unsupported arch ($arch) — install Node.js manually: https://nodejs.org/"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
os_name=$(uname -s)
|
||||||
|
case "$os_name" in
|
||||||
|
Linux*) node_os="linux" ;;
|
||||||
|
Darwin*) node_os="darwin" ;;
|
||||||
|
*)
|
||||||
|
_nb_warn "Unsupported OS ($os_name) — install Node.js manually: https://nodejs.org/"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local index_url="https://nodejs.org/dist/latest-v${HERMES_NODE_TARGET_MAJOR}.x/"
|
||||||
|
local tarball
|
||||||
|
tarball=$(curl -fsSL "$index_url" \
|
||||||
|
| grep -oE "node-v${HERMES_NODE_TARGET_MAJOR}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.xz" \
|
||||||
|
| head -1)
|
||||||
|
if [ -z "$tarball" ]; then
|
||||||
|
tarball=$(curl -fsSL "$index_url" \
|
||||||
|
| grep -oE "node-v${HERMES_NODE_TARGET_MAJOR}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.gz" \
|
||||||
|
| head -1)
|
||||||
|
fi
|
||||||
|
if [ -z "$tarball" ]; then
|
||||||
|
_nb_warn "Could not resolve Node $HERMES_NODE_TARGET_MAJOR binary for $node_os-$node_arch"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
_nb_log "Downloading $tarball..."
|
||||||
|
curl -fsSL "${index_url}${tarball}" -o "$tmp/$tarball" || {
|
||||||
|
_nb_warn "Download failed"; rm -rf "$tmp"; return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
_nb_log "Extracting to $HERMES_HOME/node/..."
|
||||||
|
if [[ "$tarball" == *.tar.xz ]]; then
|
||||||
|
tar xf "$tmp/$tarball" -C "$tmp" || { rm -rf "$tmp"; return 1; }
|
||||||
|
else
|
||||||
|
tar xzf "$tmp/$tarball" -C "$tmp" || { rm -rf "$tmp"; return 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
local extracted
|
||||||
|
extracted=$(find "$tmp" -maxdepth 1 -type d -name 'node-v*' 2>/dev/null | head -1)
|
||||||
|
if [ ! -d "$extracted" ]; then
|
||||||
|
_nb_warn "Extraction produced no node-v* directory"
|
||||||
|
rm -rf "$tmp"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$HERMES_HOME"
|
||||||
|
rm -rf "$HERMES_HOME/node"
|
||||||
|
mv "$extracted" "$HERMES_HOME/node"
|
||||||
|
rm -rf "$tmp"
|
||||||
|
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
ln -sf "$HERMES_HOME/node/bin/node" "$HOME/.local/bin/node"
|
||||||
|
ln -sf "$HERMES_HOME/node/bin/npm" "$HOME/.local/bin/npm"
|
||||||
|
ln -sf "$HERMES_HOME/node/bin/npx" "$HOME/.local/bin/npx"
|
||||||
|
export PATH="$HERMES_HOME/node/bin:$PATH"
|
||||||
|
|
||||||
|
_nb_have_modern_node || return 1
|
||||||
|
_nb_ok "Node $(node --version) installed to $HERMES_HOME/node/"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Public entry point
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ensure_node() {
|
||||||
|
HERMES_NODE_AVAILABLE=false
|
||||||
|
|
||||||
|
if _nb_have_modern_node; then
|
||||||
|
_nb_ok "Node $(node --version) found"
|
||||||
|
HERMES_NODE_AVAILABLE=true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -x "$HERMES_HOME/node/bin/node" ]; then
|
||||||
|
export PATH="$HERMES_HOME/node/bin:$PATH"
|
||||||
|
if _nb_have_modern_node; then
|
||||||
|
_nb_ok "Node $(node --version) found (Hermes-managed)"
|
||||||
|
HERMES_NODE_AVAILABLE=true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Version managers first — respect the user's existing setup.
|
||||||
|
_nb_try_fnm && { HERMES_NODE_AVAILABLE=true; return 0; }
|
||||||
|
_nb_try_proto && { HERMES_NODE_AVAILABLE=true; return 0; }
|
||||||
|
_nb_try_nvm && { HERMES_NODE_AVAILABLE=true; return 0; }
|
||||||
|
|
||||||
|
# Platform package managers.
|
||||||
|
_nb_try_termux_pkg && { HERMES_NODE_AVAILABLE=true; return 0; }
|
||||||
|
_nb_try_brew && { HERMES_NODE_AVAILABLE=true; return 0; }
|
||||||
|
|
||||||
|
# Last resort: pinned nodejs.org tarball.
|
||||||
|
_nb_install_bundled_node && { HERMES_NODE_AVAILABLE=true; return 0; }
|
||||||
|
|
||||||
|
_nb_warn "Node.js install failed — TUI and browser tools will be unavailable."
|
||||||
|
_nb_warn "Install manually: https://nodejs.org/en/download/ (or: \`brew install node\`, \`fnm install $HERMES_NODE_TARGET_MAJOR\`, etc.)"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue