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:
Austin Pickett
2026-09-25 14:32:18 -04:00
parent 0c0796bf46
commit fee49791a3
2 changed files with 110 additions and 0 deletions

View File

@@ -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):

View File

@@ -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 ──────────────────────────────────────────