fix(install): expose hermes-agent and hermes-acp launchers on PATH
setup_path() only wrote a 'hermes' launcher into the command-link dir,
even though pyproject.toml declares three [project.scripts]:
hermes, hermes-agent (run_agent:main), hermes-acp (acp_adapter.entry:main).
Fresh venv installs (the common case on macOS/Linux) leave
~/.local/bin/{hermes-agent,hermes-acp} empty, so external tools
expecting 'hermes-acp' as a standalone command (documented as
first-tier supported in website/docs/user-guide/features/acp.md)
fail to find it after a fully successful install.
Loop over the three console-script names, writing a shim per entry
that exec's the venv interpreter with the right checked-in
entrypoint. --no-venv keeps the old single-shim behaviour since
it does not manage the venv and only 'hermes' is guaranteed on PATH.
Fixes #74819
This commit is contained in:
@@ -101,8 +101,10 @@ def remove_wrapper_script():
|
||||
wrapper_paths = [
|
||||
Path.home() / ".local" / "bin" / "hermes",
|
||||
Path.home() / ".local" / "bin" / "hermes-acp",
|
||||
Path.home() / ".local" / "bin" / "hermes-agent",
|
||||
Path("/usr/local/bin/hermes"),
|
||||
Path("/usr/local/bin/hermes-acp"),
|
||||
Path("/usr/local/bin/hermes-agent"),
|
||||
]
|
||||
|
||||
removed = []
|
||||
|
||||
@@ -1725,6 +1725,29 @@ EOF
|
||||
chmod +x "$command_link_dir/hermes"
|
||||
log_success "Installed hermes launcher → $command_link_display_dir/hermes"
|
||||
|
||||
# Also expose `hermes-agent`. The `hermes-agent` console script declared in
|
||||
# pyproject.toml's [project.scripts] lives inside the venv, which is not on
|
||||
# the login-shell PATH. Without this launcher users can't invoke the agent
|
||||
# entrypoint directly from outside the venv. (#74819)
|
||||
rm -f "$command_link_dir/hermes-agent"
|
||||
if [ "$USE_VENV" = true ]; then
|
||||
cat > "$command_link_dir/hermes-agent" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
unset PYTHONPATH
|
||||
unset PYTHONHOME
|
||||
exec "$HERMES_BIN" "$INSTALL_DIR/run_agent.py" "\$@"
|
||||
EOF
|
||||
else
|
||||
cat > "$command_link_dir/hermes-agent" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
unset PYTHONPATH
|
||||
unset PYTHONHOME
|
||||
exec "$HERMES_BIN" run_agent.py "\$@"
|
||||
EOF
|
||||
fi
|
||||
chmod +x "$command_link_dir/hermes-agent"
|
||||
log_success "Installed hermes-agent launcher → $command_link_display_dir/hermes-agent"
|
||||
|
||||
# Also expose `hermes-acp`. ACP hosts (Zed, JetBrains, Buzz) resolve the
|
||||
# agent by command name on the login-shell PATH, and the `hermes-acp`
|
||||
# console script lives inside the venv, which is not on that PATH. Without
|
||||
|
||||
@@ -14,6 +14,7 @@ import re
|
||||
import stat
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
INSTALL_SH = Path(__file__).resolve().parent.parent / "scripts" / "install.sh"
|
||||
|
||||
@@ -126,3 +127,88 @@ def test_acp_launcher_does_not_follow_a_symlink_into_the_venv(tmp_path):
|
||||
assert not shim_path.is_symlink(), (
|
||||
"command_link_dir/hermes-acp must be replaced with a regular file"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# hermes-agent launcher regression (#74819)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
HERMES_AGENT_BLOCK = re.compile(
|
||||
r'(rm -f "\$command_link_dir/hermes-agent".*?'
|
||||
r'log_success "Installed hermes-agent launcher[^\n]*\n)',
|
||||
re.S,
|
||||
)
|
||||
|
||||
|
||||
def _extract_hermes_agent_shim_block() -> str:
|
||||
match = HERMES_AGENT_BLOCK.search(INSTALL_SH.read_text(encoding="utf-8"))
|
||||
assert match, (
|
||||
"could not locate the hermes-agent launcher block in scripts/install.sh — "
|
||||
"if it was renamed, update this test with it"
|
||||
)
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def _run_hermes_agent_block(tmp_path: Path, use_venv: str) -> Path | None:
|
||||
"""Execute the extracted hermes-agent block with the env vars setup_path() sets."""
|
||||
if use_venv == "false":
|
||||
# --no-venv: hermes-agent is NOT installed by this block (handled
|
||||
# elsewhere), so there's nothing to test here.
|
||||
return None
|
||||
|
||||
command_link_dir = tmp_path / "local_bin"
|
||||
command_link_dir.mkdir()
|
||||
hermes_bin = tmp_path / "venv" / "bin" / "python"
|
||||
hermes_bin.parent.mkdir(parents=True)
|
||||
hermes_bin.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||
install_dir = tmp_path / "install"
|
||||
install_dir.mkdir()
|
||||
|
||||
script = (
|
||||
"set -e\n"
|
||||
f"HERMES_BIN={hermes_bin}\n"
|
||||
f"INSTALL_DIR={install_dir}\n"
|
||||
f"command_link_dir={command_link_dir}\n"
|
||||
f"command_link_display_dir={command_link_dir}\n"
|
||||
f"USE_VENV={use_venv}\n"
|
||||
"log_success(){ :; }\n" + _extract_hermes_agent_shim_block()
|
||||
)
|
||||
result = subprocess.run(
|
||||
["bash", "-c", script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=tmp_path,
|
||||
)
|
||||
assert result.returncode == 0, (
|
||||
f"hermes-agent shim block failed:\nstdout={result.stdout}\nstderr={result.stderr}"
|
||||
)
|
||||
return command_link_dir / "hermes-agent"
|
||||
|
||||
|
||||
def test_venv_install_writes_executable_hermes_agent_launcher(tmp_path):
|
||||
"""venv install must write a user-executable hermes-agent launcher."""
|
||||
shim = _run_hermes_agent_block(tmp_path, "true")
|
||||
assert shim is not None
|
||||
assert shim.is_file()
|
||||
assert shim.stat().st_mode & stat.S_IXUSR, "launcher must be user-executable"
|
||||
|
||||
text = shim.read_text(encoding="utf-8")
|
||||
assert "unset PYTHONPATH" in text
|
||||
assert "unset PYTHONHOME" in text
|
||||
assert "run_agent.py" in text, "hermes-agent must dispatch to run_agent.py"
|
||||
|
||||
|
||||
def test_hermes_agent_launcher_cleanup_on_uninstall(tmp_path):
|
||||
"""uninstall.remove_wrapper_script() must remove hermes-agent alongside
|
||||
hermes and hermes-acp."""
|
||||
from hermes_cli.uninstall import remove_wrapper_script
|
||||
|
||||
# Simulate a hermes-agent wrapper in the user-local location
|
||||
local_shim = tmp_path / ".local" / "bin" / "hermes-agent"
|
||||
local_shim.parent.mkdir(parents=True)
|
||||
local_shim.write_text("#!/usr/bin/env bash\nexec hermes-agent\n", encoding="utf-8")
|
||||
|
||||
with patch.object(Path, "home", return_value=tmp_path):
|
||||
removed = remove_wrapper_script()
|
||||
|
||||
assert local_shim in removed, "local hermes-agent wrapper must be removed"
|
||||
|
||||
Reference in New Issue
Block a user