fix(termux): improve status and install UX

This commit is contained in:
adybag14-cyber 2026-04-09 09:08:33 +02:00 committed by Teknium
parent 122925a6f2
commit 4e40e93b98
6 changed files with 174 additions and 31 deletions

View file

@ -543,7 +543,10 @@ def run_doctor(args):
if shutil.which("docker"):
check_ok("docker", "(optional)")
else:
check_warn("docker not found", "(optional)")
if _is_termux():
check_info("Docker backend is not available inside Termux (expected on Android)")
else:
check_warn("docker not found", "(optional)")
# SSH (if using ssh backend)
if terminal_env == "ssh":
@ -591,11 +594,17 @@ def run_doctor(args):
if agent_browser_path.exists():
check_ok("agent-browser (Node.js)", "(browser automation)")
else:
check_warn("agent-browser not installed", "(run: npm install)")
if _is_termux():
check_info("agent-browser is not installed (expected in the tested Termux path)")
check_info("Install it manually later with: npm install")
else:
check_warn("agent-browser not installed", "(run: npm install)")
else:
check_warn("Node.js not found", "(optional, needed for browser tools)")
if _is_termux():
check_info("Node.js not found (browser tools are optional in the tested Termux path)")
check_info("Install Node.js on Termux with: pkg install nodejs")
else:
check_warn("Node.js not found", "(optional, needed for browser tools)")
# npm audit for all Node.js packages
if shutil.which("npm"):

View file

@ -79,6 +79,11 @@ def _effective_provider_label() -> str:
return provider_label(effective)
def _is_termux() -> bool:
prefix = os.getenv("PREFIX", "")
return bool(os.getenv("TERMUX_VERSION") or "com.termux/files/usr" in prefix)
def show_status(args):
"""Show status of all Hermes Agent components."""
show_all = getattr(args, 'all', False)
@ -325,7 +330,25 @@ def show_status(args):
print()
print(color("◆ Gateway Service", Colors.CYAN, Colors.BOLD))
if sys.platform.startswith('linux'):
if _is_termux():
try:
from hermes_cli.gateway import find_gateway_pids
gateway_pids = find_gateway_pids()
except Exception:
gateway_pids = []
is_running = bool(gateway_pids)
print(f" Status: {check_mark(is_running)} {'running' if is_running else 'stopped'}")
print(" Manager: Termux / manual process")
if gateway_pids:
rendered = ", ".join(str(pid) for pid in gateway_pids[:3])
if len(gateway_pids) > 3:
rendered += ", ..."
print(f" PID(s): {rendered}")
else:
print(" Start with: hermes gateway")
print(" Note: Android may stop background jobs when Termux is suspended")
elif sys.platform.startswith('linux'):
try:
from hermes_cli.gateway import get_service_name
_gw_svc = get_service_name()
@ -339,7 +362,7 @@ def show_status(args):
timeout=5
)
is_active = result.stdout.strip() == "active"
except subprocess.TimeoutExpired:
except (FileNotFoundError, subprocess.TimeoutExpired):
is_active = False
print(f" Status: {check_mark(is_active)} {'running' if is_active else 'stopped'}")
print(" Manager: systemd (user)")

View file

@ -121,6 +121,32 @@ 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
}
get_hermes_command_path() {
local link_dir
link_dir="$(get_command_link_dir)"
if [ -x "$link_dir/hermes" ]; then
echo "$link_dir/hermes"
else
echo "hermes"
fi
}
# ============================================================================
# System detection
# ============================================================================
@ -897,15 +923,27 @@ setup_path() {
return 0
fi
# Create symlink in ~/.local/bin (standard user binary location, usually on PATH)
mkdir -p "$HOME/.local/bin"
ln -sf "$HERMES_BIN" "$HOME/.local/bin/hermes"
log_success "Symlinked hermes → ~/.local/bin/hermes"
local command_link_dir
local command_link_display_dir
command_link_dir="$(get_command_link_dir)"
command_link_display_dir="$(get_command_link_display_dir)"
# Create a user-facing shim for the hermes command.
mkdir -p "$command_link_dir"
ln -sf "$HERMES_BIN" "$command_link_dir/hermes"
log_success "Symlinked hermes → $command_link_display_dir/hermes"
if [ "$DISTRO" = "termux" ]; then
export PATH="$command_link_dir:$PATH"
log_info "$command_link_display_dir is the native Termux command path"
log_success "hermes command ready"
return 0
fi
# Check if ~/.local/bin is on PATH; if not, add it to shell config.
# Detect the user's actual login shell (not the shell running this script,
# which is always bash when piped from curl).
if ! echo "$PATH" | tr ':' '\n' | grep -q "^$HOME/.local/bin$"; then
if ! echo "$PATH" | tr ':' '\n' | grep -q "^$command_link_dir$"; then
SHELL_CONFIGS=()
LOGIN_SHELL="$(basename "${SHELL:-/bin/bash}")"
case "$LOGIN_SHELL" in
@ -951,7 +989,7 @@ setup_path() {
fi
# Export for current session so hermes works immediately
export PATH="$HOME/.local/bin:$PATH"
export PATH="$command_link_dir:$PATH"
log_success "hermes command ready"
}
@ -1149,8 +1187,7 @@ maybe_start_gateway() {
read -p "Pair WhatsApp now? [Y/n] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then
HERMES_CMD="$HOME/.local/bin/hermes"
[ ! -x "$HERMES_CMD" ] && HERMES_CMD="hermes"
HERMES_CMD="$(get_hermes_command_path)"
$HERMES_CMD whatsapp || true
fi
else
@ -1164,16 +1201,17 @@ maybe_start_gateway() {
fi
echo ""
read -p "Would you like to install the gateway as a background service? [Y/n] " -n 1 -r < /dev/tty
if [ "$DISTRO" = "termux" ]; then
read -p "Would you like to start the gateway in the background? [Y/n] " -n 1 -r < /dev/tty
else
read -p "Would you like to install the gateway as a background service? [Y/n] " -n 1 -r < /dev/tty
fi
echo
if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then
HERMES_CMD="$HOME/.local/bin/hermes"
if [ ! -x "$HERMES_CMD" ]; then
HERMES_CMD="hermes"
fi
HERMES_CMD="$(get_hermes_command_path)"
if command -v systemctl &> /dev/null; then
if [ "$DISTRO" != "termux" ] && command -v systemctl &> /dev/null; then
log_info "Installing systemd service..."
if $HERMES_CMD gateway install 2>/dev/null; then
log_success "Gateway service installed"
@ -1186,12 +1224,19 @@ maybe_start_gateway() {
log_warn "Systemd install failed. You can start manually: hermes gateway"
fi
else
log_info "systemd not available — starting gateway in background..."
if [ "$DISTRO" = "termux" ]; then
log_info "Termux detected — starting gateway in best-effort background mode..."
else
log_info "systemd not available — starting gateway in background..."
fi
nohup $HERMES_CMD gateway > "$HERMES_HOME/logs/gateway.log" 2>&1 &
GATEWAY_PID=$!
log_success "Gateway started (PID $GATEWAY_PID). Logs: ~/.hermes/logs/gateway.log"
log_info "To stop: kill $GATEWAY_PID"
log_info "To restart later: hermes gateway"
if [ "$DISTRO" = "termux" ]; then
log_warn "Android may stop background processes when Termux is suspended or the system reclaims resources."
fi
fi
else
log_info "Skipped. Start the gateway later with: hermes gateway"
@ -1230,17 +1275,22 @@ print_success() {
echo -e "${CYAN}─────────────────────────────────────────────────────────${NC}"
echo ""
echo -e "${YELLOW}⚡ Reload your shell to use 'hermes' command:${NC}"
echo ""
LOGIN_SHELL="$(basename "${SHELL:-/bin/bash}")"
if [ "$LOGIN_SHELL" = "zsh" ]; then
echo " source ~/.zshrc"
elif [ "$LOGIN_SHELL" = "bash" ]; then
echo " source ~/.bashrc"
if [ "$DISTRO" = "termux" ]; then
echo -e "${YELLOW}⚡ 'hermes' was linked into $(get_command_link_display_dir), which is already on PATH in Termux.${NC}"
echo ""
else
echo " source ~/.bashrc # or ~/.zshrc"
echo -e "${YELLOW}⚡ Reload your shell to use 'hermes' command:${NC}"
echo ""
LOGIN_SHELL="$(basename "${SHELL:-/bin/bash}")"
if [ "$LOGIN_SHELL" = "zsh" ]; then
echo " source ~/.zshrc"
elif [ "$LOGIN_SHELL" = "bash" ]; then
echo " source ~/.bashrc"
else
echo " source ~/.bashrc # or ~/.zshrc"
fi
echo ""
fi
echo ""
# Show Node.js warning if auto-install failed
if [ "$HAS_NODE" = false ]; then

View file

@ -223,3 +223,25 @@ class TestDoctorMemoryProviderSection:
out = self._run_doctor_and_capture(monkeypatch, tmp_path, provider="mem0")
assert "Memory Provider" in out
assert "Built-in memory active" not in out
def test_run_doctor_termux_treats_docker_and_browser_warnings_as_expected(monkeypatch, tmp_path):
helper = TestDoctorMemoryProviderSection()
monkeypatch.setenv("TERMUX_VERSION", "0.118.3")
monkeypatch.setenv("PREFIX", "/data/data/com.termux/files/usr")
real_which = doctor_mod.shutil.which
def fake_which(cmd):
if cmd in {"docker", "node", "npm"}:
return None
return real_which(cmd)
monkeypatch.setattr(doctor_mod.shutil, "which", fake_which)
out = helper._run_doctor_and_capture(monkeypatch, tmp_path, provider="")
assert "Docker backend is not available inside Termux" in out
assert "Node.js not found (browser tools are optional in the tested Termux path)" in out
assert "Install Node.js on Termux with: pkg install nodejs" in out
assert "docker not found (optional)" not in out

View file

@ -12,3 +12,33 @@ def test_show_status_includes_tavily_key(monkeypatch, capsys, tmp_path):
output = capsys.readouterr().out
assert "Tavily" in output
assert "tvly...cdef" in output
def test_show_status_termux_gateway_section_skips_systemctl(monkeypatch, capsys, tmp_path):
from hermes_cli import status as status_mod
import hermes_cli.auth as auth_mod
import hermes_cli.gateway as gateway_mod
monkeypatch.setenv("TERMUX_VERSION", "0.118.3")
monkeypatch.setenv("PREFIX", "/data/data/com.termux/files/usr")
monkeypatch.setattr(status_mod, "get_env_path", lambda: tmp_path / ".env", raising=False)
monkeypatch.setattr(status_mod, "get_hermes_home", lambda: tmp_path, raising=False)
monkeypatch.setattr(status_mod, "load_config", lambda: {"model": "gpt-5.4"}, raising=False)
monkeypatch.setattr(status_mod, "resolve_requested_provider", lambda requested=None: "openai-codex", raising=False)
monkeypatch.setattr(status_mod, "resolve_provider", lambda requested=None, **kwargs: "openai-codex", raising=False)
monkeypatch.setattr(status_mod, "provider_label", lambda provider: "OpenAI Codex", raising=False)
monkeypatch.setattr(auth_mod, "get_nous_auth_status", lambda: {}, raising=False)
monkeypatch.setattr(auth_mod, "get_codex_auth_status", lambda: {}, raising=False)
monkeypatch.setattr(gateway_mod, "find_gateway_pids", lambda exclude_pids=None: [], raising=False)
def _unexpected_systemctl(*args, **kwargs):
raise AssertionError("systemctl should not be called in the Termux status view")
monkeypatch.setattr(status_mod.subprocess, "run", _unexpected_systemctl)
status_mod.show_status(SimpleNamespace(all=False, deep=False))
output = capsys.readouterr().out
assert "Manager: Termux / manual process" in output
assert "Start with: hermes gateway" in output
assert "systemd (user)" not in output

View file

@ -51,6 +51,7 @@ On Termux, the installer automatically:
- uses `pkg` for system packages
- creates the venv with `python -m venv`
- installs `.[termux]` with `pip`
- links `hermes` into `$PREFIX/bin` so it stays on your Termux PATH
- skips the untested browser / WhatsApp bootstrap
If you want the explicit commands or need to debug a failed install, use the manual path below.
@ -110,14 +111,22 @@ If you only want the minimal core agent, this also works:
python -m pip install -e '.' -c constraints-termux.txt
```
### 5. Verify the install
### 5. Put `hermes` on your Termux PATH
```bash
ln -sf "$PWD/venv/bin/hermes" "$PREFIX/bin/hermes"
```
`$PREFIX/bin` is already on PATH in Termux, so this makes the `hermes` command persist across new shells without re-activating the venv every time.
### 6. Verify the install
```bash
hermes version
hermes doctor
```
### 6. Start Hermes
### 7. Start Hermes
```bash
hermes