feat: add Node.js installation support to the setup script
- Introduced automatic installation of Node.js version 22 if not found on the system, enhancing the setup process for browser tools. - Improved the check for existing Node.js installations, including support for Hermes-managed installations. - Added logic to download and extract the appropriate Node.js binary based on the system architecture and OS. - Updated the installation script to handle missing dependencies like ripgrep and ffmpeg, providing installation prompts for macOS users.
This commit is contained in:
@@ -31,6 +31,7 @@ REPO_URL_HTTPS="https://github.com/NousResearch/hermes-agent.git"
|
||||
HERMES_HOME="$HOME/.hermes"
|
||||
INSTALL_DIR="${HERMES_INSTALL_DIR:-$HERMES_HOME/hermes-agent}"
|
||||
PYTHON_VERSION="3.11"
|
||||
NODE_VERSION="22"
|
||||
|
||||
# Options
|
||||
USE_VENV=true
|
||||
@@ -262,198 +263,258 @@ check_git() {
|
||||
}
|
||||
|
||||
check_node() {
|
||||
log_info "Checking Node.js (optional, for browser tools)..."
|
||||
|
||||
log_info "Checking Node.js (for browser tools)..."
|
||||
|
||||
if command -v node &> /dev/null; then
|
||||
NODE_VERSION=$(node --version)
|
||||
log_success "Node.js $NODE_VERSION found"
|
||||
local found_ver=$(node --version)
|
||||
log_success "Node.js $found_ver found"
|
||||
HAS_NODE=true
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_warn "Node.js not found (browser tools will be limited)"
|
||||
log_info "To install Node.js (optional):"
|
||||
|
||||
case "$OS" in
|
||||
linux)
|
||||
case "$DISTRO" in
|
||||
ubuntu|debian)
|
||||
log_info " curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -"
|
||||
log_info " sudo apt install -y nodejs"
|
||||
;;
|
||||
fedora)
|
||||
log_info " sudo dnf install nodejs"
|
||||
;;
|
||||
arch)
|
||||
log_info " sudo pacman -S nodejs npm"
|
||||
;;
|
||||
*)
|
||||
log_info " https://nodejs.org/en/download/"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
macos)
|
||||
log_info " brew install node"
|
||||
log_info " Or: https://nodejs.org/en/download/"
|
||||
;;
|
||||
esac
|
||||
|
||||
HAS_NODE=false
|
||||
# Don't exit - Node is optional
|
||||
}
|
||||
|
||||
check_ripgrep() {
|
||||
log_info "Checking ripgrep (optional, for faster file search)..."
|
||||
|
||||
if command -v rg &> /dev/null; then
|
||||
RG_VERSION=$(rg --version | head -1)
|
||||
log_success "$RG_VERSION found"
|
||||
HAS_RIPGREP=true
|
||||
# Check our own managed install from a previous run
|
||||
if [ -x "$HERMES_HOME/node/bin/node" ]; then
|
||||
export PATH="$HERMES_HOME/node/bin:$PATH"
|
||||
local found_ver=$("$HERMES_HOME/node/bin/node" --version)
|
||||
log_success "Node.js $found_ver found (Hermes-managed)"
|
||||
HAS_NODE=true
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_warn "ripgrep not found (file search will use grep fallback)"
|
||||
|
||||
# Offer to install
|
||||
echo ""
|
||||
read -p "Would you like to install ripgrep? (faster search, recommended) [Y/n] " -n 1 -r
|
||||
echo
|
||||
|
||||
if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then
|
||||
log_info "Installing ripgrep..."
|
||||
|
||||
# Check if we can use sudo
|
||||
CAN_SUDO=false
|
||||
if command -v sudo &> /dev/null; then
|
||||
if sudo -n true 2>/dev/null || sudo -v 2>/dev/null; then
|
||||
CAN_SUDO=true
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$OS" in
|
||||
linux)
|
||||
if [ "$CAN_SUDO" = true ]; then
|
||||
case "$DISTRO" in
|
||||
ubuntu|debian)
|
||||
if sudo apt install -y ripgrep 2>/dev/null; then
|
||||
log_success "ripgrep installed"
|
||||
HAS_RIPGREP=true
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
fedora)
|
||||
if sudo dnf install -y ripgrep 2>/dev/null; then
|
||||
log_success "ripgrep installed"
|
||||
HAS_RIPGREP=true
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
arch)
|
||||
if sudo pacman -S --noconfirm ripgrep 2>/dev/null; then
|
||||
log_success "ripgrep installed"
|
||||
HAS_RIPGREP=true
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
else
|
||||
log_warn "sudo not available - cannot auto-install system packages"
|
||||
if command -v cargo &> /dev/null; then
|
||||
log_info "Trying cargo install (no sudo required)..."
|
||||
if cargo install ripgrep 2>/dev/null; then
|
||||
log_success "ripgrep installed via cargo"
|
||||
HAS_RIPGREP=true
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
macos)
|
||||
if command -v brew &> /dev/null; then
|
||||
if brew install ripgrep 2>/dev/null; then
|
||||
log_success "ripgrep installed"
|
||||
HAS_RIPGREP=true
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
log_warn "Auto-install failed. You can install manually later:"
|
||||
else
|
||||
log_info "Skipping ripgrep installation. To install manually:"
|
||||
fi
|
||||
|
||||
# Show manual install instructions
|
||||
case "$OS" in
|
||||
linux)
|
||||
case "$DISTRO" in
|
||||
ubuntu|debian)
|
||||
log_info " sudo apt install ripgrep"
|
||||
;;
|
||||
fedora)
|
||||
log_info " sudo dnf install ripgrep"
|
||||
;;
|
||||
arch)
|
||||
log_info " sudo pacman -S ripgrep"
|
||||
;;
|
||||
*)
|
||||
log_info " https://github.com/BurntSushi/ripgrep#installation"
|
||||
;;
|
||||
esac
|
||||
if command -v cargo &> /dev/null; then
|
||||
log_info " Or without sudo: cargo install ripgrep"
|
||||
fi
|
||||
;;
|
||||
macos)
|
||||
log_info " brew install ripgrep"
|
||||
;;
|
||||
esac
|
||||
|
||||
HAS_RIPGREP=false
|
||||
# Don't exit - ripgrep is optional (grep fallback exists)
|
||||
|
||||
log_info "Node.js not found — installing Node.js $NODE_VERSION LTS..."
|
||||
install_node
|
||||
}
|
||||
|
||||
check_ffmpeg() {
|
||||
log_info "Checking ffmpeg (optional, for TTS voice messages)..."
|
||||
|
||||
if command -v ffmpeg &> /dev/null; then
|
||||
local ffmpeg_version=$(ffmpeg -version 2>/dev/null | head -1 | awk '{print $3}')
|
||||
log_success "ffmpeg found: $ffmpeg_version"
|
||||
HAS_FFMPEG=true
|
||||
return
|
||||
install_node() {
|
||||
local arch=$(uname -m)
|
||||
local node_arch
|
||||
case "$arch" in
|
||||
x86_64) node_arch="x64" ;;
|
||||
aarch64|arm64) node_arch="arm64" ;;
|
||||
armv7l) node_arch="armv7l" ;;
|
||||
*)
|
||||
log_warn "Unsupported architecture ($arch) for Node.js auto-install"
|
||||
log_info "Install manually: https://nodejs.org/en/download/"
|
||||
HAS_NODE=false
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
local node_os
|
||||
case "$OS" in
|
||||
linux) node_os="linux" ;;
|
||||
macos) node_os="darwin" ;;
|
||||
*)
|
||||
log_warn "Unsupported OS for Node.js auto-install"
|
||||
HAS_NODE=false
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# Resolve the latest v22.x.x tarball name from the index page
|
||||
local index_url="https://nodejs.org/dist/latest-v${NODE_VERSION}.x/"
|
||||
local tarball_name
|
||||
tarball_name=$(curl -fsSL "$index_url" \
|
||||
| grep -oE "node-v${NODE_VERSION}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.xz" \
|
||||
| head -1)
|
||||
|
||||
# Fallback to .tar.gz if .tar.xz not available
|
||||
if [ -z "$tarball_name" ]; then
|
||||
tarball_name=$(curl -fsSL "$index_url" \
|
||||
| grep -oE "node-v${NODE_VERSION}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.gz" \
|
||||
| head -1)
|
||||
fi
|
||||
|
||||
log_warn "ffmpeg not found"
|
||||
log_info "ffmpeg is needed for Telegram voice bubbles when using the default Edge TTS provider."
|
||||
log_info "Without it, Edge TTS audio is sent as a file instead of a voice bubble."
|
||||
log_info "(OpenAI and ElevenLabs TTS produce Opus natively and don't need ffmpeg.)"
|
||||
log_info ""
|
||||
log_info "To install ffmpeg:"
|
||||
|
||||
|
||||
if [ -z "$tarball_name" ]; then
|
||||
log_warn "Could not find Node.js $NODE_VERSION binary for $node_os-$node_arch"
|
||||
log_info "Install manually: https://nodejs.org/en/download/"
|
||||
HAS_NODE=false
|
||||
return 0
|
||||
fi
|
||||
|
||||
local download_url="${index_url}${tarball_name}"
|
||||
local tmp_dir
|
||||
tmp_dir=$(mktemp -d)
|
||||
|
||||
log_info "Downloading $tarball_name..."
|
||||
if ! curl -fsSL "$download_url" -o "$tmp_dir/$tarball_name"; then
|
||||
log_warn "Download failed"
|
||||
rm -rf "$tmp_dir"
|
||||
HAS_NODE=false
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Extracting to ~/.hermes/node/..."
|
||||
if [[ "$tarball_name" == *.tar.xz ]]; then
|
||||
tar xf "$tmp_dir/$tarball_name" -C "$tmp_dir"
|
||||
else
|
||||
tar xzf "$tmp_dir/$tarball_name" -C "$tmp_dir"
|
||||
fi
|
||||
|
||||
local extracted_dir
|
||||
extracted_dir=$(ls -d "$tmp_dir"/node-v* 2>/dev/null | head -1)
|
||||
|
||||
if [ ! -d "$extracted_dir" ]; then
|
||||
log_warn "Extraction failed"
|
||||
rm -rf "$tmp_dir"
|
||||
HAS_NODE=false
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Place into ~/.hermes/node/ and symlink binaries to ~/.local/bin/
|
||||
rm -rf "$HERMES_HOME/node"
|
||||
mv "$extracted_dir" "$HERMES_HOME/node"
|
||||
rm -rf "$tmp_dir"
|
||||
|
||||
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"
|
||||
|
||||
local installed_ver
|
||||
installed_ver=$("$HERMES_HOME/node/bin/node" --version 2>/dev/null)
|
||||
log_success "Node.js $installed_ver installed to ~/.hermes/node/"
|
||||
HAS_NODE=true
|
||||
}
|
||||
|
||||
install_system_packages() {
|
||||
# Detect what's missing
|
||||
HAS_RIPGREP=false
|
||||
HAS_FFMPEG=false
|
||||
local need_ripgrep=false
|
||||
local need_ffmpeg=false
|
||||
|
||||
log_info "Checking ripgrep (fast file search)..."
|
||||
if command -v rg &> /dev/null; then
|
||||
log_success "$(rg --version | head -1) found"
|
||||
HAS_RIPGREP=true
|
||||
else
|
||||
need_ripgrep=true
|
||||
fi
|
||||
|
||||
log_info "Checking ffmpeg (TTS voice messages)..."
|
||||
if command -v ffmpeg &> /dev/null; then
|
||||
local ffmpeg_ver=$(ffmpeg -version 2>/dev/null | head -1 | awk '{print $3}')
|
||||
log_success "ffmpeg $ffmpeg_ver found"
|
||||
HAS_FFMPEG=true
|
||||
else
|
||||
need_ffmpeg=true
|
||||
fi
|
||||
|
||||
# Nothing to install — done
|
||||
if [ "$need_ripgrep" = false ] && [ "$need_ffmpeg" = false ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Build a human-readable description + package list
|
||||
local desc_parts=()
|
||||
local pkgs=()
|
||||
if [ "$need_ripgrep" = true ]; then
|
||||
desc_parts+=("ripgrep for faster file search")
|
||||
pkgs+=("ripgrep")
|
||||
fi
|
||||
if [ "$need_ffmpeg" = true ]; then
|
||||
desc_parts+=("ffmpeg for TTS voice messages")
|
||||
pkgs+=("ffmpeg")
|
||||
fi
|
||||
local description
|
||||
description=$(IFS=" and "; echo "${desc_parts[*]}")
|
||||
|
||||
# ── macOS: brew ──
|
||||
if [ "$OS" = "macos" ]; then
|
||||
if command -v brew &> /dev/null; then
|
||||
log_info "Installing ${pkgs[*]} via Homebrew..."
|
||||
if brew install "${pkgs[@]}"; then
|
||||
[ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed"
|
||||
[ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
log_warn "Could not auto-install (brew not found or install failed)"
|
||||
log_info "Install manually: brew install ${pkgs[*]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ── Linux: resolve package manager command ──
|
||||
local pkg_install=""
|
||||
case "$DISTRO" in
|
||||
ubuntu|debian) pkg_install="apt install -y" ;;
|
||||
fedora) pkg_install="dnf install -y" ;;
|
||||
arch) pkg_install="pacman -S --noconfirm" ;;
|
||||
esac
|
||||
|
||||
if [ -n "$pkg_install" ]; then
|
||||
local install_cmd="$pkg_install ${pkgs[*]}"
|
||||
|
||||
# Already root — just install
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
log_info "Installing ${pkgs[*]}..."
|
||||
if $install_cmd; then
|
||||
[ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed"
|
||||
[ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed"
|
||||
return 0
|
||||
fi
|
||||
# Passwordless sudo — just install
|
||||
elif command -v sudo &> /dev/null && sudo -n true 2>/dev/null; then
|
||||
log_info "Installing ${pkgs[*]}..."
|
||||
if sudo $install_cmd; then
|
||||
[ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed"
|
||||
[ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed"
|
||||
return 0
|
||||
fi
|
||||
# sudo needs password — ask once for everything
|
||||
elif command -v sudo &> /dev/null; then
|
||||
echo ""
|
||||
read -p "Install ${description}? (requires sudo) [y/N] " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
if sudo $install_cmd; then
|
||||
[ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed"
|
||||
[ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Fallback for ripgrep: cargo ──
|
||||
if [ "$need_ripgrep" = true ] && [ "$HAS_RIPGREP" = false ]; then
|
||||
if command -v cargo &> /dev/null; then
|
||||
log_info "Trying cargo install ripgrep (no sudo needed)..."
|
||||
if cargo install ripgrep; then
|
||||
log_success "ripgrep installed via cargo"
|
||||
HAS_RIPGREP=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Show manual instructions for anything still missing ──
|
||||
if [ "$HAS_RIPGREP" = false ] && [ "$need_ripgrep" = true ]; then
|
||||
log_warn "ripgrep not installed (file search will use grep fallback)"
|
||||
show_manual_install_hint "ripgrep"
|
||||
fi
|
||||
if [ "$HAS_FFMPEG" = false ] && [ "$need_ffmpeg" = true ]; then
|
||||
log_warn "ffmpeg not installed (TTS voice messages will be limited)"
|
||||
show_manual_install_hint "ffmpeg"
|
||||
fi
|
||||
}
|
||||
|
||||
show_manual_install_hint() {
|
||||
local pkg="$1"
|
||||
log_info "To install $pkg manually:"
|
||||
case "$OS" in
|
||||
linux)
|
||||
case "$DISTRO" in
|
||||
ubuntu|debian)
|
||||
log_info " sudo apt install ffmpeg"
|
||||
;;
|
||||
fedora)
|
||||
log_info " sudo dnf install ffmpeg"
|
||||
;;
|
||||
arch)
|
||||
log_info " sudo pacman -S ffmpeg"
|
||||
;;
|
||||
*)
|
||||
log_info " https://ffmpeg.org/download.html"
|
||||
;;
|
||||
ubuntu|debian) log_info " sudo apt install $pkg" ;;
|
||||
fedora) log_info " sudo dnf install $pkg" ;;
|
||||
arch) log_info " sudo pacman -S $pkg" ;;
|
||||
*) log_info " Use your package manager or visit the project homepage" ;;
|
||||
esac
|
||||
;;
|
||||
macos)
|
||||
log_info " brew install ffmpeg"
|
||||
;;
|
||||
macos) log_info " brew install $pkg" ;;
|
||||
esac
|
||||
|
||||
HAS_FFMPEG=false
|
||||
# Don't exit - ffmpeg is optional
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
@@ -808,12 +869,12 @@ print_success() {
|
||||
echo " source ~/.bashrc # or ~/.zshrc"
|
||||
echo ""
|
||||
|
||||
# Show Node.js warning if not installed
|
||||
# Show Node.js warning if auto-install failed
|
||||
if [ "$HAS_NODE" = false ]; then
|
||||
echo -e "${YELLOW}"
|
||||
echo "Note: Node.js was not found. Browser automation tools"
|
||||
echo "will have limited functionality. Install Node.js later"
|
||||
echo "if you need full browser support."
|
||||
echo "Note: Node.js could not be installed automatically."
|
||||
echo "Browser tools need Node.js. Install manually:"
|
||||
echo " https://nodejs.org/en/download/"
|
||||
echo -e "${NC}"
|
||||
fi
|
||||
|
||||
@@ -839,8 +900,7 @@ main() {
|
||||
check_python
|
||||
check_git
|
||||
check_node
|
||||
check_ripgrep
|
||||
check_ffmpeg
|
||||
install_system_packages
|
||||
|
||||
clone_repo
|
||||
setup_venv
|
||||
|
||||
Reference in New Issue
Block a user