Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions hermes_cli/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,13 +371,32 @@ def print_systemd_linger_guidance() -> None:
def get_launchd_plist_path() -> Path:
return Path.home() / "Library" / "LaunchAgents" / "ai.hermes.gateway.plist"

def _find_venv_dir() -> Path | None:
"""Return the active virtualenv directory under PROJECT_ROOT, or None.

Checks .venv first (setup-hermes.sh default), then venv, then falls
back to sys.prefix if it lives under PROJECT_ROOT (pip install -e).
"""
for name in (".venv", "venv"):
candidate = PROJECT_ROOT / name
if (candidate / "bin" / "python").exists() or (candidate / "Scripts" / "python.exe").exists():
return candidate
# pip-installed editable: sys.prefix may point to a venv outside PROJECT_ROOT
prefix = Path(sys.prefix)
if prefix != Path(sys.base_prefix): # we're inside a venv
return prefix
return None


def get_python_path() -> str:
if is_windows():
venv_python = PROJECT_ROOT / "venv" / "Scripts" / "python.exe"
else:
venv_python = PROJECT_ROOT / "venv" / "bin" / "python"
if venv_python.exists():
return str(venv_python)
venv_dir = _find_venv_dir()
if venv_dir is not None:
if is_windows():
venv_python = venv_dir / "Scripts" / "python.exe"
else:
venv_python = venv_dir / "bin" / "python"
if venv_python.exists():
return str(venv_python)
return sys.executable

def get_hermes_cli_path() -> str:
Expand All @@ -399,8 +418,9 @@ def get_hermes_cli_path() -> str:
def generate_systemd_unit(system: bool = False, run_as_user: str | None = None) -> str:
python_path = get_python_path()
working_dir = str(PROJECT_ROOT)
venv_dir = str(PROJECT_ROOT / "venv")
venv_bin = str(PROJECT_ROOT / "venv" / "bin")
_venv = _find_venv_dir()
venv_dir = str(_venv) if _venv else str(PROJECT_ROOT / "venv")
venv_bin = str(_venv / "bin") if _venv else str(PROJECT_ROOT / "venv" / "bin")
node_bin = str(PROJECT_ROOT / "node_modules" / ".bin")

path_entries = [venv_bin, node_bin]
Expand Down
55 changes: 55 additions & 0 deletions tests/hermes_cli/test_gateway_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,58 @@ def test_systemctl_cmd_skips_ensure_for_system_mode(self, monkeypatch):
result = gateway_cli._systemctl_cmd(system=True)
assert result == ["systemctl"]
assert calls == []


class TestFindVenvDir:
def test_finds_dot_venv(self, tmp_path, monkeypatch):
dot_venv = tmp_path / ".venv" / "bin"
dot_venv.mkdir(parents=True)
(dot_venv / "python").write_text("#!/bin/sh")
monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path)
result = gateway_cli._find_venv_dir()
assert result == tmp_path / ".venv"

def test_finds_venv(self, tmp_path, monkeypatch):
venv_bin = tmp_path / "venv" / "bin"
venv_bin.mkdir(parents=True)
(venv_bin / "python").write_text("#!/bin/sh")
monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path)
result = gateway_cli._find_venv_dir()
assert result == tmp_path / "venv"

def test_prefers_dot_venv_over_venv(self, tmp_path, monkeypatch):
for name in (".venv", "venv"):
bin_dir = tmp_path / name / "bin"
bin_dir.mkdir(parents=True)
(bin_dir / "python").write_text("#!/bin/sh")
monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path)
result = gateway_cli._find_venv_dir()
assert result == tmp_path / ".venv"

def test_returns_none_when_no_venv(self, tmp_path, monkeypatch):
import sys
monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path)
monkeypatch.setattr(gateway_cli.sys, "prefix", sys.base_prefix)
monkeypatch.setattr(gateway_cli.sys, "base_prefix", sys.base_prefix)
result = gateway_cli._find_venv_dir()
assert result is None


class TestSystemdUnitVenvPath:
def test_unit_uses_dot_venv_when_present(self, tmp_path, monkeypatch):
dot_venv = tmp_path / ".venv" / "bin"
dot_venv.mkdir(parents=True)
(dot_venv / "python").write_text("#!/bin/sh")
monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path)
unit = gateway_cli.generate_systemd_unit(system=False)
assert ".venv" in unit
assert "VIRTUAL_ENV=" in unit

def test_unit_uses_venv_when_only_venv_present(self, tmp_path, monkeypatch):
venv_bin = tmp_path / "venv" / "bin"
venv_bin.mkdir(parents=True)
(venv_bin / "python").write_text("#!/bin/sh")
monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path)
unit = gateway_cli.generate_systemd_unit(system=False)
assert "VIRTUAL_ENV=" in unit
assert ".venv" not in unit
Loading