diff --git a/hermes_cli/main.py b/hermes_cli/main.py index d442e138f..21d544d87 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -725,9 +725,18 @@ def _print_tui_exit_summary(session_id: Optional[str]) -> None: ) -def _tui_deps_ready(root: Path) -> bool: - """Nix and local dev both need file: workspace @hermes/ink under node_modules.""" - return (root / "node_modules" / "@hermes" / "ink" / "package.json").is_file() +def _tui_need_npm_install(root: Path) -> bool: + """True when @hermes/ink is missing or node_modules is behind package-lock.json (post-pull).""" + ink = root / "node_modules" / "@hermes" / "ink" / "package.json" + if not ink.is_file(): + return True + lock = root / "package-lock.json" + if not lock.is_file(): + return False + marker = root / "node_modules" / ".package-lock.json" + if not marker.is_file(): + return True + return lock.stat().st_mtime > marker.stat().st_mtime def _find_bundled_tui(tui_dir: Path) -> Optional[Path]: @@ -735,9 +744,9 @@ def _find_bundled_tui(tui_dir: Path) -> Optional[Path]: env = os.environ.get("HERMES_TUI_DIR") if env: p = Path(env) - if (p / "dist" / "entry.js").exists() and _tui_deps_ready(p): + if (p / "dist" / "entry.js").exists() and not _tui_need_npm_install(p): return p - if (tui_dir / "dist" / "entry.js").exists() and _tui_deps_ready(tui_dir): + if (tui_dir / "dist" / "entry.js").exists() and not _tui_need_npm_install(tui_dir): return tui_dir return None @@ -794,12 +803,12 @@ def _make_tui_argv(tui_dir: Path, tui_dev: bool) -> tuple[list[str], Path]: ext_dir = os.environ.get("HERMES_TUI_DIR") if ext_dir: p = Path(ext_dir) - if (p / "dist" / "entry.js").exists() and _tui_deps_ready(p): + if (p / "dist" / "entry.js").exists() and not _tui_need_npm_install(p): node = _node_bin("node") return [node, str(p / "dist" / "entry.js")], p npm = _node_bin("npm") - if not _tui_deps_ready(tui_dir): + if _tui_need_npm_install(tui_dir): print("Installing TUI dependencies…") result = subprocess.run( [npm, "install", "--silent", "--no-fund", "--no-audit", "--progress=false"], diff --git a/tests/hermes_cli/test_tui_npm_install.py b/tests/hermes_cli/test_tui_npm_install.py new file mode 100644 index 000000000..3f3191ccf --- /dev/null +++ b/tests/hermes_cli/test_tui_npm_install.py @@ -0,0 +1,53 @@ +"""_tui_need_npm_install: auto npm when lockfile ahead of node_modules.""" + +import os +from pathlib import Path + +import pytest + + +@pytest.fixture +def main_mod(): + import hermes_cli.main as m + + return m + + +def _touch_ink(root: Path) -> None: + ink = root / "node_modules" / "@hermes" / "ink" / "package.json" + ink.parent.mkdir(parents=True, exist_ok=True) + ink.write_text("{}") + + +def test_need_install_when_ink_missing(tmp_path: Path, main_mod) -> None: + (tmp_path / "package-lock.json").write_text("{}") + assert main_mod._tui_need_npm_install(tmp_path) is True + + +def test_need_install_when_lock_newer_than_marker(tmp_path: Path, main_mod) -> None: + _touch_ink(tmp_path) + (tmp_path / "package-lock.json").write_text("{}") + (tmp_path / "node_modules" / ".package-lock.json").write_text("{}") + os.utime(tmp_path / "package-lock.json", (200, 200)) + os.utime(tmp_path / "node_modules" / ".package-lock.json", (100, 100)) + assert main_mod._tui_need_npm_install(tmp_path) is True + + +def test_no_install_when_lock_older_than_marker(tmp_path: Path, main_mod) -> None: + _touch_ink(tmp_path) + (tmp_path / "package-lock.json").write_text("{}") + (tmp_path / "node_modules" / ".package-lock.json").write_text("{}") + os.utime(tmp_path / "package-lock.json", (100, 100)) + os.utime(tmp_path / "node_modules" / ".package-lock.json", (200, 200)) + assert main_mod._tui_need_npm_install(tmp_path) is False + + +def test_need_install_when_marker_missing(tmp_path: Path, main_mod) -> None: + _touch_ink(tmp_path) + (tmp_path / "package-lock.json").write_text("{}") + assert main_mod._tui_need_npm_install(tmp_path) is True + + +def test_no_install_without_lockfile_when_ink_present(tmp_path: Path, main_mod) -> None: + _touch_ink(tmp_path) + assert main_mod._tui_need_npm_install(tmp_path) is False