fix(desktop): handle package-manager installs with no desktop source tree
A Homebrew (or other pip/package-manager) install lives in a site-packages tree that does not ship apps/desktop, so `hermes desktop` failed with a generic missing-source error implying a broken checkout. Detect the install kind (Cellar/site-packages), prefer launching a separately installed /Applications/Hermes.app, and otherwise print a brew-specific instruction instead. Fixes #61056
This commit is contained in:
@@ -1383,6 +1383,40 @@ def _packaged_desktop_launch_command(packaged_executable: Path) -> list[str]:
|
||||
return launch_command
|
||||
|
||||
|
||||
def _site_packages_install_kind(project_root: Path) -> Optional[str]:
|
||||
"""The package manager owning a non-editable install at *project_root*, or None.
|
||||
|
||||
A package-manager install (Homebrew, pip, distro packaging) places this
|
||||
code in a ``site-packages``/``dist-packages`` tree. Such a tree ships no
|
||||
``apps/desktop`` source, so the build ladder below can never run — the
|
||||
caller must not treat it like a broken checkout. A Homebrew formula lives
|
||||
under a ``Cellar`` directory; any other site-packages owner is reported
|
||||
generically as pip.
|
||||
"""
|
||||
parts = Path(project_root).parts
|
||||
if "site-packages" in parts or "dist-packages" in parts:
|
||||
return "homebrew" if "Cellar" in parts else "pip"
|
||||
return None
|
||||
|
||||
|
||||
def _launch_installed_macos_desktop_app() -> bool:
|
||||
"""Launch a separately installed ``/Applications/Hermes.app``, if present.
|
||||
|
||||
Returns True only when the app bundle exists and a detached launch was
|
||||
started — the caller then exits without touching the build ladder.
|
||||
"""
|
||||
if sys.platform != "darwin":
|
||||
return False
|
||||
executable = Path("/Applications/Hermes.app/Contents/MacOS/Hermes")
|
||||
if not executable.is_file():
|
||||
return False
|
||||
from hermes_cli.bundled_app import launch_detached
|
||||
|
||||
pid = launch_detached([str(executable)], cwd=executable.parent)
|
||||
print(f"→ Launched the installed Hermes Desktop app: {executable} (pid {pid})")
|
||||
return True
|
||||
|
||||
|
||||
def cmd_gui(args: argparse.Namespace):
|
||||
"""Build and launch the native Electron desktop GUI."""
|
||||
from hermes_cli.main import PROJECT_ROOT
|
||||
@@ -1396,7 +1430,21 @@ def cmd_gui(args: argparse.Namespace):
|
||||
|
||||
bundled = is_bundled_payload(PROJECT_ROOT)
|
||||
if not bundled and not (desktop_dir / "package.json").exists():
|
||||
# A package-manager install (Homebrew, pip, ...) ships no desktop
|
||||
# source tree, so building here is impossible by construction (#61056).
|
||||
# Prefer the separately installed desktop app; otherwise explain the
|
||||
# packaging shape instead of the generic missing-source error.
|
||||
install_kind = _site_packages_install_kind(PROJECT_ROOT)
|
||||
if install_kind is not None and _launch_installed_macos_desktop_app():
|
||||
sys.exit(0)
|
||||
print(f"Desktop GUI source not found at: {desktop_dir}")
|
||||
if install_kind == "homebrew":
|
||||
print(
|
||||
" This Hermes came from Homebrew, which does not ship the desktop app's\n"
|
||||
" source tree, so it cannot be built from this install.\n"
|
||||
" Install the desktop app from https://hermes-agent.nousresearch.com,\n"
|
||||
" or run `hermes desktop` from a source checkout."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
|
||||
@@ -209,6 +209,68 @@ def test_packaged_renderer_bom_does_not_bypass_entry_validation(tmp_path):
|
||||
# Dependency admission and staging are exercised by test_desktop_source_build.py.
|
||||
|
||||
|
||||
# --- package-manager (Homebrew/pip) installs: no desktop source tree -------
|
||||
# (#61056: `hermes desktop` under Homebrew looked for apps/desktop inside the
|
||||
# Cellar site-packages, which the formula does not ship.)
|
||||
|
||||
|
||||
def test_site_packages_install_kind_detects_brew_and_pip():
|
||||
brew_root = Path("/opt/homebrew/Cellar/hermes-agent/2026.7.7.2/libexec/site-packages")
|
||||
pip_root = Path("/usr/lib/python3/dist-packages")
|
||||
venv_root = Path("/home/u/proj/venv/lib/python3.12/site-packages")
|
||||
assert main_desktop._site_packages_install_kind(brew_root) == "homebrew"
|
||||
assert main_desktop._site_packages_install_kind(pip_root) == "pip"
|
||||
assert main_desktop._site_packages_install_kind(venv_root) == "pip"
|
||||
assert main_desktop._site_packages_install_kind(Path("/home/u/hermes-agent")) is None
|
||||
|
||||
|
||||
def test_gui_brew_install_prints_brew_guidance_not_venv_hint(tmp_path, monkeypatch, capsys):
|
||||
"""A Homebrew install has no apps/desktop tree and can never build one — the
|
||||
error must say so instead of implying a broken checkout."""
|
||||
root = tmp_path / "Cellar" / "hermes-agent" / "2026.7.7.2" / "libexec" / "site-packages"
|
||||
root.mkdir(parents=True)
|
||||
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
|
||||
monkeypatch.setattr(main_desktop, "_launch_installed_macos_desktop_app", lambda: False)
|
||||
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
main_desktop.cmd_gui(_ns())
|
||||
|
||||
assert exc.value.code == 1
|
||||
out = capsys.readouterr().out
|
||||
assert "Desktop GUI source not found" in out
|
||||
assert "Homebrew" in out
|
||||
|
||||
|
||||
def test_gui_brew_install_launches_installed_app_when_present(tmp_path, monkeypatch):
|
||||
"""Prefer the separately installed /Applications/Hermes.app over failing."""
|
||||
root = tmp_path / "Cellar" / "hermes-agent" / "2026.7.7.2" / "libexec" / "site-packages"
|
||||
root.mkdir(parents=True)
|
||||
monkeypatch.setattr(cli_main, "PROJECT_ROOT", root)
|
||||
launched = []
|
||||
monkeypatch.setattr(main_desktop, "_launch_installed_macos_desktop_app", lambda: launched.append(1) or True)
|
||||
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
main_desktop.cmd_gui(_ns())
|
||||
|
||||
assert exc.value.code == 0
|
||||
assert launched == [1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exists,platform", [(True, "darwin"), (False, "darwin"), (True, "linux")])
|
||||
def test_launch_installed_macos_desktop_app_gates_on_bundle_and_platform(tmp_path, monkeypatch, exists, platform):
|
||||
monkeypatch.setattr(main_desktop.sys, "platform", platform)
|
||||
exe = Path("/Applications/Hermes.app/Contents/MacOS/Hermes")
|
||||
monkeypatch.setattr(main_desktop.Path, "is_file", lambda self: exists if self == exe else Path.is_file(self))
|
||||
calls = []
|
||||
if exists and platform == "darwin":
|
||||
import hermes_cli.bundled_app as bundled_app
|
||||
monkeypatch.setattr(bundled_app, "launch_detached", lambda argv, **kw: calls.append(argv) or 4321)
|
||||
|
||||
assert main_desktop._launch_installed_macos_desktop_app() is (exists and platform == "darwin")
|
||||
if exists and platform == "darwin":
|
||||
assert calls == [[str(exe)]]
|
||||
|
||||
|
||||
# ── Content-hash stamp tests ──────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user