diff --git a/Dockerfile b/Dockerfile index 18177cc1aca..be147b6eac6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,13 +45,7 @@ COPY --chown=hermes:hermes . . # Build browser dashboard and terminal UI assets. RUN cd web && npm run build && \ - cd ../ui-tui && npm run build && \ - rm -rf node_modules/@hermes/ink && \ - rm -rf packages/hermes-ink/node_modules && \ - cp -R packages/hermes-ink node_modules/@hermes/ink && \ - npm install --omit=dev --prefer-offline --no-audit --prefix node_modules/@hermes/ink && \ - rm -rf node_modules/@hermes/ink/node_modules/react && \ - node --input-type=module -e "await import('@hermes/ink')" + cd ../ui-tui && npm run build # ---------- Permissions ---------- # Make install dir world-readable so any HERMES_UID can read it at runtime. diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 7b5834aea6b..831cd762579 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -922,9 +922,39 @@ def _find_bundled_tui(tui_dir: Path) -> Optional[Path]: def _tui_build_needed(tui_dir: Path) -> bool: + entry = tui_dir / "dist" / "entry.js" + # In the esbuild pipeline, ink is bundled into dist/entry.js directly. + # If the main bundle exists and is up to date with all source files, + # no separate ink rebuild is needed. + if entry.exists(): + dist_m = entry.stat().st_mtime + skip = frozenset({"node_modules", "dist"}) + stale = False + for dirpath, dirnames, filenames in os.walk(tui_dir, topdown=True): + dirnames[:] = [d for d in dirnames if d not in skip] + for fn in filenames: + if fn.endswith((".ts", ".tsx")): + if os.path.getmtime(os.path.join(dirpath, fn)) > dist_m: + stale = True + break + if stale: + break + if not stale: + for meta in ( + "package.json", + "package-lock.json", + "tsconfig.json", + "tsconfig.build.json", + ): + mp = tui_dir / meta + if mp.exists() and mp.stat().st_mtime > dist_m: + stale = True + break + if not stale: + return False + if _hermes_ink_bundle_stale(tui_dir): return True - entry = tui_dir / "dist" / "entry.js" if not entry.exists(): return True dist_m = entry.stat().st_mtime diff --git a/tests/hermes_cli/test_tui_npm_install.py b/tests/hermes_cli/test_tui_npm_install.py index 8e16c5e9dea..0ef98c9ea67 100644 --- a/tests/hermes_cli/test_tui_npm_install.py +++ b/tests/hermes_cli/test_tui_npm_install.py @@ -95,12 +95,25 @@ def test_no_install_prebuilt_bundle_mode(tmp_path: Path, main_mod) -> None: assert main_mod._tui_need_npm_install(tmp_path) is False -def test_build_needed_when_local_ink_bundle_missing(tmp_path: Path, main_mod) -> None: +def test_build_needed_when_source_newer_than_entry(tmp_path: Path, main_mod) -> None: + _touch_tui_entry(tmp_path) + _touch_ink(tmp_path) + src = tmp_path / "src" / "entry.tsx" + src.parent.mkdir(parents=True, exist_ok=True) + src.write_text("console.log('newer')") + os.utime(src, (200, 200)) + os.utime(tmp_path / "dist" / "entry.js", (100, 100)) + + assert main_mod._tui_need_npm_install(tmp_path) is False + assert main_mod._tui_build_needed(tmp_path) is True + + +def test_build_not_needed_when_entry_exists_and_sources_unchanged(tmp_path: Path, main_mod) -> None: _touch_tui_entry(tmp_path) _touch_ink(tmp_path) assert main_mod._tui_need_npm_install(tmp_path) is False - assert main_mod._tui_build_needed(tmp_path) is True + assert main_mod._tui_build_needed(tmp_path) is False def test_build_not_needed_when_entry_and_ink_bundle_present(tmp_path: Path, main_mod) -> None: diff --git a/tests/tools/test_dockerfile_pid1_reaping.py b/tests/tools/test_dockerfile_pid1_reaping.py index 52532a78dd2..960415d417b 100644 --- a/tests/tools/test_dockerfile_pid1_reaping.py +++ b/tests/tools/test_dockerfile_pid1_reaping.py @@ -121,20 +121,6 @@ def test_dockerfile_builds_tui_assets(dockerfile_text): ) -def test_dockerfile_materializes_local_tui_ink_package(dockerfile_text): - assert any( - "ui-tui" in step - and "node_modules/@hermes/ink" in step - and "packages/hermes-ink" in step - and "rm -rf packages/hermes-ink/node_modules" in step - and "npm install --omit=dev" in step - and "--prefix node_modules/@hermes/ink" in step - and "rm -rf node_modules/@hermes/ink/node_modules/react" in step - and "await import('@hermes/ink')" in step - for step in _run_steps(dockerfile_text) - ) - - def test_dockerignore_excludes_nested_dependency_dirs(): if not DOCKERIGNORE.exists(): pytest.skip(".dockerignore not present in this checkout")