fix(termux): improve status and install UX
This commit is contained in:
@@ -543,7 +543,10 @@ def run_doctor(args):
|
|||||||
if shutil.which("docker"):
|
if shutil.which("docker"):
|
||||||
check_ok("docker", "(optional)")
|
check_ok("docker", "(optional)")
|
||||||
else:
|
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)
|
# SSH (if using ssh backend)
|
||||||
if terminal_env == "ssh":
|
if terminal_env == "ssh":
|
||||||
@@ -591,11 +594,17 @@ def run_doctor(args):
|
|||||||
if agent_browser_path.exists():
|
if agent_browser_path.exists():
|
||||||
check_ok("agent-browser (Node.js)", "(browser automation)")
|
check_ok("agent-browser (Node.js)", "(browser automation)")
|
||||||
else:
|
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:
|
else:
|
||||||
check_warn("Node.js not found", "(optional, needed for browser tools)")
|
|
||||||
if _is_termux():
|
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")
|
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
|
# npm audit for all Node.js packages
|
||||||
if shutil.which("npm"):
|
if shutil.which("npm"):
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ def _effective_provider_label() -> str:
|
|||||||
return provider_label(effective)
|
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):
|
def show_status(args):
|
||||||
"""Show status of all Hermes Agent components."""
|
"""Show status of all Hermes Agent components."""
|
||||||
show_all = getattr(args, 'all', False)
|
show_all = getattr(args, 'all', False)
|
||||||
@@ -325,7 +330,25 @@ def show_status(args):
|
|||||||
print()
|
print()
|
||||||
print(color("◆ Gateway Service", Colors.CYAN, Colors.BOLD))
|
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:
|
try:
|
||||||
from hermes_cli.gateway import get_service_name
|
from hermes_cli.gateway import get_service_name
|
||||||
_gw_svc = get_service_name()
|
_gw_svc = get_service_name()
|
||||||
@@ -339,7 +362,7 @@ def show_status(args):
|
|||||||
timeout=5
|
timeout=5
|
||||||
)
|
)
|
||||||
is_active = result.stdout.strip() == "active"
|
is_active = result.stdout.strip() == "active"
|
||||||
except subprocess.TimeoutExpired:
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||||
is_active = False
|
is_active = False
|
||||||
print(f" Status: {check_mark(is_active)} {'running' if is_active else 'stopped'}")
|
print(f" Status: {check_mark(is_active)} {'running' if is_active else 'stopped'}")
|
||||||
print(" Manager: systemd (user)")
|
print(" Manager: systemd (user)")
|
||||||
|
|||||||
@@ -121,6 +121,32 @@ is_termux() {
|
|||||||
[ -n "${TERMUX_VERSION:-}" ] || [[ "${PREFIX:-}" == *"com.termux/files/usr"* ]]
|
[ -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
|
# System detection
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -897,15 +923,27 @@ setup_path() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create symlink in ~/.local/bin (standard user binary location, usually on PATH)
|
local command_link_dir
|
||||||
mkdir -p "$HOME/.local/bin"
|
local command_link_display_dir
|
||||||
ln -sf "$HERMES_BIN" "$HOME/.local/bin/hermes"
|
command_link_dir="$(get_command_link_dir)"
|
||||||
log_success "Symlinked hermes → ~/.local/bin/hermes"
|
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.
|
# 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,
|
# Detect the user's actual login shell (not the shell running this script,
|
||||||
# which is always bash when piped from curl).
|
# 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=()
|
SHELL_CONFIGS=()
|
||||||
LOGIN_SHELL="$(basename "${SHELL:-/bin/bash}")"
|
LOGIN_SHELL="$(basename "${SHELL:-/bin/bash}")"
|
||||||
case "$LOGIN_SHELL" in
|
case "$LOGIN_SHELL" in
|
||||||
@@ -951,7 +989,7 @@ setup_path() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Export for current session so hermes works immediately
|
# 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"
|
log_success "hermes command ready"
|
||||||
}
|
}
|
||||||
@@ -1149,8 +1187,7 @@ maybe_start_gateway() {
|
|||||||
read -p "Pair WhatsApp now? [Y/n] " -n 1 -r
|
read -p "Pair WhatsApp now? [Y/n] " -n 1 -r
|
||||||
echo
|
echo
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then
|
if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then
|
||||||
HERMES_CMD="$HOME/.local/bin/hermes"
|
HERMES_CMD="$(get_hermes_command_path)"
|
||||||
[ ! -x "$HERMES_CMD" ] && HERMES_CMD="hermes"
|
|
||||||
$HERMES_CMD whatsapp || true
|
$HERMES_CMD whatsapp || true
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
@@ -1164,16 +1201,17 @@ maybe_start_gateway() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
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
|
echo
|
||||||
|
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then
|
if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then
|
||||||
HERMES_CMD="$HOME/.local/bin/hermes"
|
HERMES_CMD="$(get_hermes_command_path)"
|
||||||
if [ ! -x "$HERMES_CMD" ]; then
|
|
||||||
HERMES_CMD="hermes"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v systemctl &> /dev/null; then
|
if [ "$DISTRO" != "termux" ] && command -v systemctl &> /dev/null; then
|
||||||
log_info "Installing systemd service..."
|
log_info "Installing systemd service..."
|
||||||
if $HERMES_CMD gateway install 2>/dev/null; then
|
if $HERMES_CMD gateway install 2>/dev/null; then
|
||||||
log_success "Gateway service installed"
|
log_success "Gateway service installed"
|
||||||
@@ -1186,12 +1224,19 @@ maybe_start_gateway() {
|
|||||||
log_warn "Systemd install failed. You can start manually: hermes gateway"
|
log_warn "Systemd install failed. You can start manually: hermes gateway"
|
||||||
fi
|
fi
|
||||||
else
|
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 &
|
nohup $HERMES_CMD gateway > "$HERMES_HOME/logs/gateway.log" 2>&1 &
|
||||||
GATEWAY_PID=$!
|
GATEWAY_PID=$!
|
||||||
log_success "Gateway started (PID $GATEWAY_PID). Logs: ~/.hermes/logs/gateway.log"
|
log_success "Gateway started (PID $GATEWAY_PID). Logs: ~/.hermes/logs/gateway.log"
|
||||||
log_info "To stop: kill $GATEWAY_PID"
|
log_info "To stop: kill $GATEWAY_PID"
|
||||||
log_info "To restart later: hermes gateway"
|
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
|
fi
|
||||||
else
|
else
|
||||||
log_info "Skipped. Start the gateway later with: hermes gateway"
|
log_info "Skipped. Start the gateway later with: hermes gateway"
|
||||||
@@ -1230,17 +1275,22 @@ print_success() {
|
|||||||
|
|
||||||
echo -e "${CYAN}─────────────────────────────────────────────────────────${NC}"
|
echo -e "${CYAN}─────────────────────────────────────────────────────────${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}⚡ Reload your shell to use 'hermes' command:${NC}"
|
if [ "$DISTRO" = "termux" ]; then
|
||||||
echo ""
|
echo -e "${YELLOW}⚡ 'hermes' was linked into $(get_command_link_display_dir), which is already on PATH in Termux.${NC}"
|
||||||
LOGIN_SHELL="$(basename "${SHELL:-/bin/bash}")"
|
echo ""
|
||||||
if [ "$LOGIN_SHELL" = "zsh" ]; then
|
|
||||||
echo " source ~/.zshrc"
|
|
||||||
elif [ "$LOGIN_SHELL" = "bash" ]; then
|
|
||||||
echo " source ~/.bashrc"
|
|
||||||
else
|
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
|
fi
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Show Node.js warning if auto-install failed
|
# Show Node.js warning if auto-install failed
|
||||||
if [ "$HAS_NODE" = false ]; then
|
if [ "$HAS_NODE" = false ]; then
|
||||||
|
|||||||
@@ -223,3 +223,25 @@ class TestDoctorMemoryProviderSection:
|
|||||||
out = self._run_doctor_and_capture(monkeypatch, tmp_path, provider="mem0")
|
out = self._run_doctor_and_capture(monkeypatch, tmp_path, provider="mem0")
|
||||||
assert "Memory Provider" in out
|
assert "Memory Provider" in out
|
||||||
assert "Built-in memory active" not 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
|
||||||
|
|||||||
@@ -12,3 +12,33 @@ def test_show_status_includes_tavily_key(monkeypatch, capsys, tmp_path):
|
|||||||
output = capsys.readouterr().out
|
output = capsys.readouterr().out
|
||||||
assert "Tavily" in output
|
assert "Tavily" in output
|
||||||
assert "tvly...cdef" 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
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ On Termux, the installer automatically:
|
|||||||
- uses `pkg` for system packages
|
- uses `pkg` for system packages
|
||||||
- creates the venv with `python -m venv`
|
- creates the venv with `python -m venv`
|
||||||
- installs `.[termux]` with `pip`
|
- installs `.[termux]` with `pip`
|
||||||
|
- links `hermes` into `$PREFIX/bin` so it stays on your Termux PATH
|
||||||
- skips the untested browser / WhatsApp bootstrap
|
- skips the untested browser / WhatsApp bootstrap
|
||||||
|
|
||||||
If you want the explicit commands or need to debug a failed install, use the manual path below.
|
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
|
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
|
```bash
|
||||||
hermes version
|
hermes version
|
||||||
hermes doctor
|
hermes doctor
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. Start Hermes
|
### 7. Start Hermes
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
hermes
|
hermes
|
||||||
|
|||||||
Reference in New Issue
Block a user