Skip to content
Merged
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
25 changes: 17 additions & 8 deletions hermes_cli/memory_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,25 @@ def _install_dependencies(provider_name: str) -> None:
print(f"\n Installing dependencies: {', '.join(missing)}")

import shutil

uv_path = shutil.which("uv")
if not uv_path:
print(f" ⚠ uv not found — cannot install dependencies")
print(f" Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh")
print(f" Then re-run: hermes memory setup")
return
if uv_path:
install_cmd = [uv_path, "pip", "install", "--python", sys.executable, "--quiet"] + missing
manual_cmd = f"uv pip install --python {sys.executable} {' '.join(missing)}"
else:
pip_cmd = shutil.which("pip3") or shutil.which("pip")
if not pip_cmd:
print(f" ⚠ uv not found — cannot install dependencies")
print(f" Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh")
print(f" Then re-run: hermes memory setup")
return
print(f" ⚠ uv not found. Falling back to standard pip...")
install_cmd = [sys.executable, "-m", "pip", "install", "--quiet"] + missing
manual_cmd = f"{sys.executable} -m pip install {' '.join(missing)}"

try:
subprocess.run(
[uv_path, "pip", "install", "--python", sys.executable, "--quiet"] + missing,
install_cmd,
check=True, timeout=120,
capture_output=True,
)
Expand All @@ -116,10 +125,10 @@ def _install_dependencies(provider_name: str) -> None:
stderr = (e.stderr or b"").decode()[:200]
if stderr:
print(f" {stderr}")
print(f" Run manually: uv pip install --python {sys.executable} {' '.join(missing)}")
print(f" Run manually: {manual_cmd}")
except Exception as e:
print(f" ⚠ Install failed: {e}")
print(f" Run manually: uv pip install --python {sys.executable} {' '.join(missing)}")
print(f" Run manually: {manual_cmd}")

# Also show external dependencies (non-pip) if any
ext_deps = meta.get("external_dependencies", [])
Expand Down
47 changes: 47 additions & 0 deletions tests/hermes_cli/test_memory_setup_provider_arg.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,50 @@ def test_unknown_provider_reports_and_returns_early(self, capsys):
out = capsys.readouterr().out
assert "not found" in out
assert "hermes memory setup" in out


class TestInstallDependenciesRunner:
"""`_install_dependencies` must install via `uv` when present and fall back
to standard `pip` when `uv` is unavailable (e.g. slim containers / CI images
that don't ship uv) instead of dead-ending with "cannot install"."""

def _run_with_missing_dep(self, tmp_path, which_side_effect):
"""Drive _install_dependencies for a plugin that declares one missing
pip dep, capturing the subprocess.run argv (or None if never called)."""
import sys

(tmp_path / "plugin.yaml").write_text(
"pip_dependencies:\n - definitely-not-installed-xyz\n", encoding="utf-8"
)
captured = {}

def fake_run(cmd, **kw):
captured["cmd"] = cmd
return SimpleNamespace()

with patch("plugins.memory.find_provider_dir", return_value=tmp_path), \
patch("shutil.which", side_effect=which_side_effect), \
patch("subprocess.run", fake_run):
memory_setup._install_dependencies("x")
return captured.get("cmd"), sys.executable

def test_uses_uv_when_available(self, tmp_path):
cmd, _ = self._run_with_missing_dep(
tmp_path, lambda b: "/usr/bin/uv" if b == "uv" else None
)
assert cmd is not None
assert cmd[:3] == ["/usr/bin/uv", "pip", "install"]

def test_falls_back_to_pip_when_uv_missing(self, tmp_path, capsys):
"""The salvaged behavior (#5954): no uv but pip present -> python -m pip."""
cmd, py = self._run_with_missing_dep(
tmp_path, lambda b: "/usr/bin/pip3" if b == "pip3" else None
)
assert cmd is not None
assert cmd[:4] == [py, "-m", "pip", "install"]
assert "Falling back to standard pip" in capsys.readouterr().out

def test_aborts_when_neither_uv_nor_pip(self, tmp_path, capsys):
cmd, _ = self._run_with_missing_dep(tmp_path, lambda b: None)
assert cmd is None # no install attempted
assert "cannot install dependencies" in capsys.readouterr().out
Loading