diff --git a/setup-hermes.sh b/setup-hermes.sh index d2a1b12ea..5d0f2928a 100755 --- a/setup-hermes.sh +++ b/setup-hermes.sh @@ -3,17 +3,17 @@ # Hermes Agent Setup Script # ============================================================================ # Quick setup for developers who cloned the repo manually. -# Uses uv for fast Python provisioning and package management. +# Uses uv for desktop/server setup and Python's stdlib venv + pip on Termux. # # Usage: # ./setup-hermes.sh # # This script: -# 1. Installs uv if not present -# 2. Creates a virtual environment with Python 3.11 via uv -# 3. Installs all dependencies (main package + submodules) +# 1. Detects desktop/server vs Android/Termux setup path +# 2. Creates a Python 3.11 virtual environment +# 3. Installs the appropriate dependency set for the platform # 4. Creates .env from template (if not exists) -# 5. Symlinks the 'hermes' CLI command into ~/.local/bin +# 5. Symlinks the 'hermes' CLI command into a user-facing bin dir # 6. Runs the setup wizard (optional) # ============================================================================ @@ -31,6 +31,26 @@ cd "$SCRIPT_DIR" PYTHON_VERSION="3.11" +is_termux() { + [ -n "${TERMUX_VERSION:-}" ] || [[ "${PREFIX:-}" == *"com.termux/files/usr"* ]] +} + +get_command_link_dir() { + if is_termux && [ -n "${PREFIX:-}" ]; then + echo "$PREFIX/bin" + else + echo "$HOME/.local/bin" + fi +} + +get_command_link_display_dir() { + if is_termux && [ -n "${PREFIX:-}" ]; then + echo '$PREFIX/bin' + else + echo '~/.local/bin' + fi +} + echo "" echo -e "${CYAN}⚕ Hermes Agent Setup${NC}" echo "" @@ -42,36 +62,40 @@ echo "" echo -e "${CYAN}→${NC} Checking for uv..." UV_CMD="" -if command -v uv &> /dev/null; then - UV_CMD="uv" -elif [ -x "$HOME/.local/bin/uv" ]; then - UV_CMD="$HOME/.local/bin/uv" -elif [ -x "$HOME/.cargo/bin/uv" ]; then - UV_CMD="$HOME/.cargo/bin/uv" -fi - -if [ -n "$UV_CMD" ]; then - UV_VERSION=$($UV_CMD --version 2>/dev/null) - echo -e "${GREEN}✓${NC} uv found ($UV_VERSION)" +if is_termux; then + echo -e "${CYAN}→${NC} Termux detected — using Python's stdlib venv + pip instead of uv" else - echo -e "${CYAN}→${NC} Installing uv..." - if curl -LsSf https://astral.sh/uv/install.sh | sh 2>/dev/null; then - if [ -x "$HOME/.local/bin/uv" ]; then - UV_CMD="$HOME/.local/bin/uv" - elif [ -x "$HOME/.cargo/bin/uv" ]; then - UV_CMD="$HOME/.cargo/bin/uv" - fi - - if [ -n "$UV_CMD" ]; then - UV_VERSION=$($UV_CMD --version 2>/dev/null) - echo -e "${GREEN}✓${NC} uv installed ($UV_VERSION)" + if command -v uv &> /dev/null; then + UV_CMD="uv" + elif [ -x "$HOME/.local/bin/uv" ]; then + UV_CMD="$HOME/.local/bin/uv" + elif [ -x "$HOME/.cargo/bin/uv" ]; then + UV_CMD="$HOME/.cargo/bin/uv" + fi + + if [ -n "$UV_CMD" ]; then + UV_VERSION=$($UV_CMD --version 2>/dev/null) + echo -e "${GREEN}✓${NC} uv found ($UV_VERSION)" + else + echo -e "${CYAN}→${NC} Installing uv..." + if curl -LsSf https://astral.sh/uv/install.sh | sh 2>/dev/null; then + if [ -x "$HOME/.local/bin/uv" ]; then + UV_CMD="$HOME/.local/bin/uv" + elif [ -x "$HOME/.cargo/bin/uv" ]; then + UV_CMD="$HOME/.cargo/bin/uv" + fi + + if [ -n "$UV_CMD" ]; then + UV_VERSION=$($UV_CMD --version 2>/dev/null) + echo -e "${GREEN}✓${NC} uv installed ($UV_VERSION)" + else + echo -e "${RED}✗${NC} uv installed but not found. Add ~/.local/bin to PATH and retry." + exit 1 + fi else - echo -e "${RED}✗${NC} uv installed but not found. Add ~/.local/bin to PATH and retry." + echo -e "${RED}✗${NC} Failed to install uv. Visit https://docs.astral.sh/uv/" exit 1 fi - else - echo -e "${RED}✗${NC} Failed to install uv. Visit https://docs.astral.sh/uv/" - exit 1 fi fi @@ -81,16 +105,34 @@ fi echo -e "${CYAN}→${NC} Checking Python $PYTHON_VERSION..." -if $UV_CMD python find "$PYTHON_VERSION" &> /dev/null; then - PYTHON_PATH=$($UV_CMD python find "$PYTHON_VERSION") - PYTHON_FOUND_VERSION=$($PYTHON_PATH --version 2>/dev/null) - echo -e "${GREEN}✓${NC} $PYTHON_FOUND_VERSION found" +if is_termux; then + if command -v python >/dev/null 2>&1; then + PYTHON_PATH="$(command -v python)" + if "$PYTHON_PATH" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)' 2>/dev/null; then + PYTHON_FOUND_VERSION=$($PYTHON_PATH --version 2>/dev/null) + echo -e "${GREEN}✓${NC} $PYTHON_FOUND_VERSION found" + else + echo -e "${RED}✗${NC} Termux Python must be 3.11+" + echo " Run: pkg install python" + exit 1 + fi + else + echo -e "${RED}✗${NC} Python not found in Termux" + echo " Run: pkg install python" + exit 1 + fi else - echo -e "${CYAN}→${NC} Python $PYTHON_VERSION not found, installing via uv..." - $UV_CMD python install "$PYTHON_VERSION" - PYTHON_PATH=$($UV_CMD python find "$PYTHON_VERSION") - PYTHON_FOUND_VERSION=$($PYTHON_PATH --version 2>/dev/null) - echo -e "${GREEN}✓${NC} $PYTHON_FOUND_VERSION installed" + if $UV_CMD python find "$PYTHON_VERSION" &> /dev/null; then + PYTHON_PATH=$($UV_CMD python find "$PYTHON_VERSION") + PYTHON_FOUND_VERSION=$($PYTHON_PATH --version 2>/dev/null) + echo -e "${GREEN}✓${NC} $PYTHON_FOUND_VERSION found" + else + echo -e "${CYAN}→${NC} Python $PYTHON_VERSION not found, installing via uv..." + $UV_CMD python install "$PYTHON_VERSION" + PYTHON_PATH=$($UV_CMD python find "$PYTHON_VERSION") + PYTHON_FOUND_VERSION=$($PYTHON_PATH --version 2>/dev/null) + echo -e "${GREEN}✓${NC} $PYTHON_FOUND_VERSION installed" + fi fi # ============================================================================ @@ -104,11 +146,16 @@ if [ -d "venv" ]; then rm -rf venv fi -$UV_CMD venv venv --python "$PYTHON_VERSION" -echo -e "${GREEN}✓${NC} venv created (Python $PYTHON_VERSION)" +if is_termux; then + "$PYTHON_PATH" -m venv venv + echo -e "${GREEN}✓${NC} venv created with stdlib venv" +else + $UV_CMD venv venv --python "$PYTHON_VERSION" + echo -e "${GREEN}✓${NC} venv created (Python $PYTHON_VERSION)" +fi -# Tell uv to install into this venv (no activation needed for uv) export VIRTUAL_ENV="$SCRIPT_DIR/venv" +SETUP_PYTHON="$SCRIPT_DIR/venv/bin/python" # ============================================================================ # Dependencies @@ -116,19 +163,34 @@ export VIRTUAL_ENV="$SCRIPT_DIR/venv" echo -e "${CYAN}→${NC} Installing dependencies..." -# Prefer uv sync with lockfile (hash-verified installs) when available, -# fall back to pip install for compatibility or when lockfile is stale. -if [ -f "uv.lock" ]; then - echo -e "${CYAN}→${NC} Using uv.lock for hash-verified installation..." - UV_PROJECT_ENVIRONMENT="$SCRIPT_DIR/venv" $UV_CMD sync --all-extras --locked 2>/dev/null && \ - echo -e "${GREEN}✓${NC} Dependencies installed (lockfile verified)" || { - echo -e "${YELLOW}⚠${NC} Lockfile install failed (may be outdated), falling back to pip install..." +if is_termux; then + export ANDROID_API_LEVEL="$(getprop ro.build.version.sdk 2>/dev/null || printf '%s' "${ANDROID_API_LEVEL:-}")" + echo -e "${CYAN}→${NC} Termux detected — installing the tested Android bundle" + "$SETUP_PYTHON" -m pip install --upgrade pip setuptools wheel + if [ -f "constraints-termux.txt" ]; then + "$SETUP_PYTHON" -m pip install -e ".[termux]" -c constraints-termux.txt || { + echo -e "${YELLOW}⚠${NC} Termux bundle install failed, falling back to base install..." + "$SETUP_PYTHON" -m pip install -e "." -c constraints-termux.txt + } + else + "$SETUP_PYTHON" -m pip install -e ".[termux]" || "$SETUP_PYTHON" -m pip install -e "." + fi + echo -e "${GREEN}✓${NC} Dependencies installed" +else + # Prefer uv sync with lockfile (hash-verified installs) when available, + # fall back to pip install for compatibility or when lockfile is stale. + if [ -f "uv.lock" ]; then + echo -e "${CYAN}→${NC} Using uv.lock for hash-verified installation..." + UV_PROJECT_ENVIRONMENT="$SCRIPT_DIR/venv" $UV_CMD sync --all-extras --locked 2>/dev/null && \ + echo -e "${GREEN}✓${NC} Dependencies installed (lockfile verified)" || { + echo -e "${YELLOW}⚠${NC} Lockfile install failed (may be outdated), falling back to pip install..." + $UV_CMD pip install -e ".[all]" || $UV_CMD pip install -e "." + echo -e "${GREEN}✓${NC} Dependencies installed" + } + else $UV_CMD pip install -e ".[all]" || $UV_CMD pip install -e "." echo -e "${GREEN}✓${NC} Dependencies installed" - } -else - $UV_CMD pip install -e ".[all]" || $UV_CMD pip install -e "." - echo -e "${GREEN}✓${NC} Dependencies installed" + fi fi # ============================================================================ @@ -138,7 +200,9 @@ fi echo -e "${CYAN}→${NC} Installing optional submodules..." # tinker-atropos (RL training backend) -if [ -d "tinker-atropos" ] && [ -f "tinker-atropos/pyproject.toml" ]; then +if is_termux; then + echo -e "${CYAN}→${NC} Skipping tinker-atropos on Termux (not part of the tested Android path)" +elif [ -d "tinker-atropos" ] && [ -f "tinker-atropos/pyproject.toml" ]; then $UV_CMD pip install -e "./tinker-atropos" && \ echo -e "${GREEN}✓${NC} tinker-atropos installed" || \ echo -e "${YELLOW}⚠${NC} tinker-atropos install failed (RL tools may not work)" @@ -160,34 +224,42 @@ else echo if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then INSTALLED=false - - # Check if sudo is available - if command -v sudo &> /dev/null && sudo -n true 2>/dev/null; then - if command -v apt &> /dev/null; then - sudo apt install -y ripgrep && INSTALLED=true - elif command -v dnf &> /dev/null; then - sudo dnf install -y ripgrep && INSTALLED=true + + if is_termux; then + pkg install -y ripgrep && INSTALLED=true + else + # Check if sudo is available + if command -v sudo &> /dev/null && sudo -n true 2>/dev/null; then + if command -v apt &> /dev/null; then + sudo apt install -y ripgrep && INSTALLED=true + elif command -v dnf &> /dev/null; then + sudo dnf install -y ripgrep && INSTALLED=true + fi + fi + + # Try brew (no sudo needed) + if [ "$INSTALLED" = false ] && command -v brew &> /dev/null; then + brew install ripgrep && INSTALLED=true + fi + + # Try cargo (no sudo needed) + if [ "$INSTALLED" = false ] && command -v cargo &> /dev/null; then + echo -e "${CYAN}→${NC} Trying cargo install (no sudo required)..." + cargo install ripgrep && INSTALLED=true fi fi - - # Try brew (no sudo needed) - if [ "$INSTALLED" = false ] && command -v brew &> /dev/null; then - brew install ripgrep && INSTALLED=true - fi - - # Try cargo (no sudo needed) - if [ "$INSTALLED" = false ] && command -v cargo &> /dev/null; then - echo -e "${CYAN}→${NC} Trying cargo install (no sudo required)..." - cargo install ripgrep && INSTALLED=true - fi - + if [ "$INSTALLED" = true ]; then echo -e "${GREEN}✓${NC} ripgrep installed" else echo -e "${YELLOW}⚠${NC} Auto-install failed. Install options:" - echo " sudo apt install ripgrep # Debian/Ubuntu" - echo " brew install ripgrep # macOS" - echo " cargo install ripgrep # With Rust (no sudo)" + if is_termux; then + echo " pkg install ripgrep # Termux / Android" + else + echo " sudo apt install ripgrep # Debian/Ubuntu" + echo " brew install ripgrep # macOS" + echo " cargo install ripgrep # With Rust (no sudo)" + fi echo " https://github.com/BurntSushi/ripgrep#installation" fi fi @@ -207,49 +279,56 @@ else fi # ============================================================================ -# PATH setup — symlink hermes into ~/.local/bin +# PATH setup — symlink hermes into a user-facing bin dir # ============================================================================ echo -e "${CYAN}→${NC} Setting up hermes command..." HERMES_BIN="$SCRIPT_DIR/venv/bin/hermes" -mkdir -p "$HOME/.local/bin" -ln -sf "$HERMES_BIN" "$HOME/.local/bin/hermes" -echo -e "${GREEN}✓${NC} Symlinked hermes → ~/.local/bin/hermes" +COMMAND_LINK_DIR="$(get_command_link_dir)" +COMMAND_LINK_DISPLAY_DIR="$(get_command_link_display_dir)" +mkdir -p "$COMMAND_LINK_DIR" +ln -sf "$HERMES_BIN" "$COMMAND_LINK_DIR/hermes" +echo -e "${GREEN}✓${NC} Symlinked hermes → $COMMAND_LINK_DISPLAY_DIR/hermes" -# 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" +if is_termux; then + export PATH="$COMMAND_LINK_DIR:$PATH" + echo -e "${GREEN}✓${NC} $COMMAND_LINK_DISPLAY_DIR is already on PATH in Termux" else - # Fallback to checking existing files - if [ -f "$HOME/.zshrc" ]; then + # Determine the appropriate shell config file + SHELL_CONFIG="" + if [[ "$SHELL" == *"zsh"* ]]; then SHELL_CONFIG="$HOME/.zshrc" - elif [ -f "$HOME/.bashrc" ]; then + elif [[ "$SHELL" == *"bash"* ]]; 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 + [ ! -f "$SHELL_CONFIG" ] && SHELL_CONFIG="$HOME/.bash_profile" else - echo -e "${GREEN}✓${NC} ~/.local/bin already on PATH" + # 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 @@ -281,18 +360,31 @@ echo -e "${GREEN}✓ Setup complete!${NC}" echo "" echo "Next steps:" echo "" -echo " 1. Reload your shell:" -echo " source $SHELL_CONFIG" -echo "" -echo " 2. Run the setup wizard to configure API keys:" -echo " hermes setup" -echo "" -echo " 3. Start chatting:" -echo " hermes" -echo "" +if is_termux; then + echo " 1. Run the setup wizard to configure API keys:" + echo " hermes setup" + echo "" + echo " 2. Start chatting:" + echo " hermes" + echo "" +else + echo " 1. Reload your shell:" + echo " source $SHELL_CONFIG" + echo "" + echo " 2. Run the setup wizard to configure API keys:" + echo " hermes setup" + echo "" + echo " 3. Start chatting:" + echo " hermes" + echo "" +fi echo "Other commands:" echo " hermes status # Check configuration" -echo " hermes gateway install # Install gateway service (messaging + cron)" +if is_termux; then + echo " hermes gateway # Run gateway in foreground" +else + echo " hermes gateway install # Install gateway service (messaging + cron)" +fi echo " hermes cron list # View scheduled jobs" echo " hermes doctor # Diagnose issues" echo "" diff --git a/tests/hermes_cli/test_setup_hermes_script.py b/tests/hermes_cli/test_setup_hermes_script.py new file mode 100644 index 000000000..7978e660a --- /dev/null +++ b/tests/hermes_cli/test_setup_hermes_script.py @@ -0,0 +1,21 @@ +from pathlib import Path +import subprocess + + +REPO_ROOT = Path(__file__).resolve().parents[2] +SETUP_SCRIPT = REPO_ROOT / "setup-hermes.sh" + + +def test_setup_hermes_script_is_valid_shell(): + result = subprocess.run(["bash", "-n", str(SETUP_SCRIPT)], capture_output=True, text=True) + assert result.returncode == 0, result.stderr + + +def test_setup_hermes_script_has_termux_path(): + content = SETUP_SCRIPT.read_text(encoding="utf-8") + + assert "is_termux()" in content + assert ".[termux]" in content + assert "constraints-termux.txt" in content + assert "$PREFIX/bin" in content + assert "Skipping tinker-atropos on Termux" in content