diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 8d1a10000..48bbdbf0d 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -3133,6 +3133,8 @@ def _update_via_zip(args): ) _install_python_dependencies_with_optional_fallback(pip_cmd) + _update_node_dependencies() + # Sync skills try: from tools.skills_sync import sync_skills @@ -3652,9 +3654,42 @@ def _install_python_dependencies_with_optional_fallback( print(f" ⚠ Skipped optional extras that still failed: {', '.join(failed_extras)}") +def _update_node_dependencies() -> None: + npm = shutil.which("npm") + if not npm: + return + + paths = ( + ("repo root", PROJECT_ROOT), + ("ui-tui", PROJECT_ROOT / "ui-tui"), + ) + if not any((path / "package.json").exists() for _, path in paths): + return + + print("→ Updating Node.js dependencies...") + for label, path in paths: + if not (path / "package.json").exists(): + continue + + result = subprocess.run( + [npm, "install", "--silent", "--no-fund", "--no-audit", "--progress=false"], + cwd=path, + capture_output=True, + text=True, + check=False, + ) + if result.returncode == 0: + print(f" ✓ {label}") + continue + + print(f" ⚠ npm install failed in {label}") + stderr = (result.stderr or "").strip() + if stderr: + print(f" {stderr.splitlines()[-1]}") + + def cmd_update(args): """Update Hermes Agent to the latest version.""" - import shutil from hermes_cli.config import is_managed, managed_error if is_managed(): @@ -3873,13 +3908,8 @@ def cmd_update(args): ) _install_python_dependencies_with_optional_fallback(pip_cmd) - # Check for Node.js deps - if (PROJECT_ROOT / "package.json").exists(): - import shutil - if shutil.which("npm"): - print("→ Updating Node.js dependencies...") - subprocess.run(["npm", "install", "--silent"], cwd=PROJECT_ROOT, check=False) - + _update_node_dependencies() + print() print("✓ Code updated!") diff --git a/tests/hermes_cli/test_cmd_update.py b/tests/hermes_cli/test_cmd_update.py index 9ffa809a5..c8f284228 100644 --- a/tests/hermes_cli/test_cmd_update.py +++ b/tests/hermes_cli/test_cmd_update.py @@ -106,6 +106,49 @@ class TestCmdUpdateBranchFallback: pull_cmds = [c for c in commands if "pull" in c] assert len(pull_cmds) == 0 + @patch("shutil.which") + @patch("subprocess.run") + def test_update_refreshes_repo_and_tui_node_dependencies( + self, mock_run, mock_which, mock_args + ): + mock_which.side_effect = {"uv": "/usr/bin/uv", "npm": "/usr/bin/npm"}.get + mock_run.side_effect = _make_run_side_effect( + branch="main", verify_ok=True, commit_count="1" + ) + + cmd_update(mock_args) + + npm_calls = [ + (call.args[0], call.kwargs.get("cwd")) + for call in mock_run.call_args_list + if call.args and call.args[0][0] == "/usr/bin/npm" + ] + + assert npm_calls == [ + ( + [ + "/usr/bin/npm", + "install", + "--silent", + "--no-fund", + "--no-audit", + "--progress=false", + ], + PROJECT_ROOT, + ), + ( + [ + "/usr/bin/npm", + "install", + "--silent", + "--no-fund", + "--no-audit", + "--progress=false", + ], + PROJECT_ROOT / "ui-tui", + ), + ] + def test_update_non_interactive_skips_migration_prompt(self, mock_args, capsys): """When stdin/stdout aren't TTYs, config migration prompt is skipped.""" with patch("shutil.which", return_value=None), patch(