diff --git a/hermes_cli/uninstall.py b/hermes_cli/uninstall.py index fb7548bbc8..58c7598040 100644 --- a/hermes_cli/uninstall.py +++ b/hermes_cli/uninstall.py @@ -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 = [] diff --git a/scripts/install.sh b/scripts/install.sh index 493b53e412..071081b045 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -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" < "$command_link_dir/hermes-agent" < 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"