diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 0b5e79fe9d9..121b77b0f91 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -1713,6 +1713,24 @@ def cmd_setup(args): run_setup_wizard(args) +def cmd_postinstall(args): + """One-shot bootstrap for pip users: install non-Python deps + run setup.""" + from hermes_cli.dep_ensure import ensure_dependency + + print("⚕ Hermes post-install bootstrap") + print() + + for dep in ("node", "browser", "ripgrep", "ffmpeg"): + ensure_dependency(dep) + + if not _has_any_provider_configured(): + print() + cmd_setup(args) + else: + print() + print("✓ Post-install complete.") + + def cmd_model(args): """Select default model — starts with provider selection, then model picker.""" _require_tty("model") @@ -9583,7 +9601,7 @@ _BUILTIN_SUBCOMMANDS = frozenset( "config", "cron", "curator", "dashboard", "debug", "doctor", "dump", "fallback", "gateway", "hooks", "import", "insights", "kanban", "login", "logout", "logs", "lsp", "mcp", "memory", - "model", "pairing", "plugins", "profile", "proxy", "sessions", "setup", + "model", "pairing", "plugins", "postinstall", "profile", "proxy", "sessions", "setup", "skills", "slack", "status", "tools", "uninstall", "update", "version", "webhook", "whatsapp", "chat", # Help-ish invocations — plugin commands not being listed in @@ -10022,6 +10040,17 @@ def main(): ) setup_parser.set_defaults(func=cmd_setup) + # ========================================================================= + # postinstall command + # ========================================================================= + postinstall_parser = subparsers.add_parser( + "postinstall", + help="Bootstrap non-Python deps for pip installs (node, browser, ripgrep, ffmpeg)", + description="One-shot post-install for pip users. Installs system " + "dependencies that pip cannot provide, then runs setup if needed.", + ) + postinstall_parser.set_defaults(func=cmd_postinstall) + # ========================================================================= # whatsapp command # ========================================================================= diff --git a/tests/hermes_cli/test_managed_installs.py b/tests/hermes_cli/test_managed_installs.py index c6b5d792ce0..d2cf2947c6d 100644 --- a/tests/hermes_cli/test_managed_installs.py +++ b/tests/hermes_cli/test_managed_installs.py @@ -29,7 +29,8 @@ def test_format_managed_message_homebrew(monkeypatch): def test_recommended_update_command_defaults_to_hermes_update(monkeypatch): monkeypatch.delenv("HERMES_MANAGED", raising=False) - assert recommended_update_command() == "hermes update" + with patch("hermes_cli.config.detect_install_method", return_value="git"): + assert recommended_update_command() == "hermes update" def test_cmd_update_blocks_managed_homebrew(monkeypatch, capsys): diff --git a/tests/hermes_cli/test_update_check.py b/tests/hermes_cli/test_update_check.py index 2bdc9b24621..92cd2d2e14c 100644 --- a/tests/hermes_cli/test_update_check.py +++ b/tests/hermes_cli/test_update_check.py @@ -59,7 +59,7 @@ def test_check_for_updates_expired_cache(tmp_path, monkeypatch): def test_check_for_updates_no_git_dir(tmp_path, monkeypatch): - """Returns None when .git directory doesn't exist anywhere.""" + """Falls back to PyPI check when .git directory doesn't exist anywhere.""" import hermes_cli.banner as banner # Create a fake banner.py so the fallback path also has no .git @@ -70,8 +70,9 @@ def test_check_for_updates_no_git_dir(tmp_path, monkeypatch): monkeypatch.setattr(banner, "__file__", str(fake_banner)) monkeypatch.setenv("HERMES_HOME", str(tmp_path)) with patch("hermes_cli.banner.subprocess.run") as mock_run: - result = banner.check_for_updates() - assert result is None + with patch("hermes_cli.banner._check_via_pypi", return_value=0): + result = banner.check_for_updates() + assert result == 0 mock_run.assert_not_called() diff --git a/tests/hermes_cli/test_update_gateway_restart.py b/tests/hermes_cli/test_update_gateway_restart.py index 34c878eca79..b53b1463624 100644 --- a/tests/hermes_cli/test_update_gateway_restart.py +++ b/tests/hermes_cli/test_update_gateway_restart.py @@ -178,8 +178,11 @@ class TestLaunchdPlistPath: raise AssertionError("PATH key not found in plist") def test_plist_path_includes_node_modules_bin(self): + node_bin_dir = gateway_cli.PROJECT_ROOT / "node_modules" / ".bin" + if not node_bin_dir.is_dir(): + pytest.skip("node_modules/.bin not present in this checkout") plist = gateway_cli.generate_launchd_plist() - node_bin = str(gateway_cli.PROJECT_ROOT / "node_modules" / ".bin") + node_bin = str(node_bin_dir) lines = plist.splitlines() for i, line in enumerate(lines): if "PATH" in line.strip():