mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
fix(termux): scope frontend npm installs
This commit is contained in:
parent
9ca11b35d5
commit
af8b917dab
3 changed files with 201 additions and 9 deletions
|
|
@ -1264,6 +1264,32 @@ def _workspace_root(dir: Path) -> Path:
|
|||
return dir
|
||||
|
||||
|
||||
def _termux_workspace_install_context(
|
||||
dir: Path, *, include_child_workspaces: bool = False
|
||||
) -> tuple[Path, tuple[str, ...]]:
|
||||
"""Return Termux-only ``(cwd, npm_args)`` for installing deps for *dir* only."""
|
||||
ws_root = _workspace_root(dir)
|
||||
if ws_root == dir:
|
||||
return dir, ()
|
||||
|
||||
try:
|
||||
workspace = dir.relative_to(ws_root).as_posix()
|
||||
except ValueError:
|
||||
return ws_root, ()
|
||||
|
||||
workspace_args: list[str] = ["--workspace", workspace]
|
||||
if include_child_workspaces:
|
||||
packages_dir = dir / "packages"
|
||||
if packages_dir.is_dir():
|
||||
for child in sorted(packages_dir.iterdir()):
|
||||
if child.is_dir() and (child / "package.json").is_file():
|
||||
workspace_args.extend(
|
||||
["--workspace", child.relative_to(ws_root).as_posix()]
|
||||
)
|
||||
workspace_args.append("--include-workspace-root=false")
|
||||
return ws_root, tuple(workspace_args)
|
||||
|
||||
|
||||
def _tui_need_npm_install(root: Path) -> bool:
|
||||
"""True when @hermes/ink is missing or node_modules is behind package-lock.json.
|
||||
|
||||
|
|
@ -1524,16 +1550,43 @@ def _make_tui_argv(tui_dir: Path, tui_dev: bool) -> tuple[list[str], Path]:
|
|||
|
||||
# 2. Normal flow: npm install if needed, always esbuild, then node dist/entry.js.
|
||||
# --dev flow: npm install if needed, then tsx src/entry.tsx.
|
||||
# npm install runs from the workspace root (where package-lock.json lives);
|
||||
# npm workspaces resolves ui-tui deps automatically.
|
||||
# Existing desktop behaviour runs npm from the workspace root. Termux
|
||||
# scopes the install to ui-tui so launch does not pull desktop/web
|
||||
# dependencies into the hot path.
|
||||
did_install = False
|
||||
if _tui_need_npm_install(tui_dir):
|
||||
termux_startup = _is_termux_startup_environment()
|
||||
termux_need_rebuild = False
|
||||
if termux_startup and not tui_dev:
|
||||
termux_need_rebuild = _tui_need_rebuild(tui_dir)
|
||||
|
||||
skip_install_for_fresh_termux_bundle = (
|
||||
termux_startup and not tui_dev and not termux_need_rebuild
|
||||
)
|
||||
if (
|
||||
not skip_install_for_fresh_termux_bundle
|
||||
and _tui_need_npm_install(tui_dir)
|
||||
):
|
||||
npm = _node_bin("npm")
|
||||
if not os.environ.get("HERMES_QUIET"):
|
||||
print("Installing TUI dependencies…")
|
||||
npm_cwd = _workspace_root(tui_dir)
|
||||
npm_workspace_args: tuple[str, ...] = ()
|
||||
if termux_startup:
|
||||
npm_cwd, npm_workspace_args = _termux_workspace_install_context(
|
||||
tui_dir,
|
||||
include_child_workspaces=True,
|
||||
)
|
||||
result = subprocess.run(
|
||||
[npm, "install", "--silent", "--no-fund", "--no-audit", "--progress=false"],
|
||||
cwd=str(_workspace_root(tui_dir)),
|
||||
[
|
||||
npm,
|
||||
"install",
|
||||
*npm_workspace_args,
|
||||
"--silent",
|
||||
"--no-fund",
|
||||
"--no-audit",
|
||||
"--progress=false",
|
||||
],
|
||||
cwd=str(npm_cwd),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
|
|
@ -1579,8 +1632,8 @@ def _make_tui_argv(tui_dir: Path, tui_dev: bool) -> tuple[list[str], Path]:
|
|||
# Termux cold starts use the freshness check because esbuild startup is
|
||||
# expensive on old mobile CPUs.
|
||||
should_build = True
|
||||
if _is_termux_startup_environment():
|
||||
should_build = did_install or _tui_need_rebuild(tui_dir)
|
||||
if termux_startup:
|
||||
should_build = did_install or termux_need_rebuild
|
||||
|
||||
if should_build:
|
||||
npm = _node_bin("npm")
|
||||
|
|
@ -7004,10 +7057,14 @@ def _build_web_ui(web_dir: Path, *, fatal: bool = False) -> bool:
|
|||
if text:
|
||||
_say(text)
|
||||
|
||||
npm_cwd = _workspace_root(web_dir)
|
||||
npm_workspace_args: tuple[str, ...] = ()
|
||||
if _is_termux_startup_environment():
|
||||
npm_cwd, npm_workspace_args = _termux_workspace_install_context(web_dir)
|
||||
r1 = _run_npm_install_deterministic(
|
||||
npm,
|
||||
_workspace_root(web_dir),
|
||||
extra_args=("--silent",),
|
||||
npm_cwd,
|
||||
extra_args=(*npm_workspace_args, "--silent"),
|
||||
)
|
||||
if r1.returncode != 0:
|
||||
_say(
|
||||
|
|
|
|||
|
|
@ -172,6 +172,97 @@ def test_make_tui_argv_skips_build_only_on_termux_when_fresh(
|
|||
assert cwd == tmp_path
|
||||
|
||||
|
||||
def test_make_tui_argv_skips_install_on_termux_when_bundle_fresh(
|
||||
tmp_path: Path, main_mod, monkeypatch
|
||||
) -> None:
|
||||
_touch_tui_entry(tmp_path)
|
||||
monkeypatch.setenv("TERMUX_VERSION", "1")
|
||||
monkeypatch.setattr(main_mod, "_tui_need_npm_install", lambda _root: True)
|
||||
monkeypatch.setattr(main_mod, "_tui_need_rebuild", lambda _root: False)
|
||||
monkeypatch.setattr(main_mod.shutil, "which", lambda name: f"/bin/{name}")
|
||||
|
||||
def fail_run(*_args, **_kwargs):
|
||||
raise AssertionError("fresh Termux TUI launch must not run npm")
|
||||
|
||||
monkeypatch.setattr(main_mod.subprocess, "run", fail_run)
|
||||
|
||||
argv, cwd = main_mod._make_tui_argv(tmp_path, tui_dev=False)
|
||||
|
||||
assert argv == ["/bin/node", "--expose-gc", str(tmp_path / "dist" / "entry.js")]
|
||||
assert cwd == tmp_path
|
||||
|
||||
|
||||
def test_make_tui_argv_scopes_npm_install_on_termux_workspace(
|
||||
tmp_path: Path, main_mod, monkeypatch
|
||||
) -> None:
|
||||
tui_dir = tmp_path / "ui-tui"
|
||||
tui_dir.mkdir()
|
||||
(tui_dir / "package.json").write_text("{}")
|
||||
ink_dir = tui_dir / "packages" / "hermes-ink"
|
||||
ink_dir.mkdir(parents=True)
|
||||
(ink_dir / "package.json").write_text("{}")
|
||||
(tmp_path / "package-lock.json").write_text("{}")
|
||||
|
||||
monkeypatch.setenv("TERMUX_VERSION", "1")
|
||||
monkeypatch.setattr(main_mod, "_tui_need_npm_install", lambda _root: True)
|
||||
monkeypatch.setattr(main_mod, "_tui_need_rebuild", lambda _root: True)
|
||||
monkeypatch.setattr(main_mod.shutil, "which", lambda name: f"/bin/{name}")
|
||||
calls = []
|
||||
|
||||
def fake_run(*args, **kwargs):
|
||||
calls.append((args, kwargs))
|
||||
return types.SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||
|
||||
monkeypatch.setattr(main_mod.subprocess, "run", fake_run)
|
||||
|
||||
main_mod._make_tui_argv(tui_dir, tui_dev=False)
|
||||
|
||||
install_cmd = calls[0][0][0]
|
||||
assert install_cmd[:7] == [
|
||||
"/bin/npm",
|
||||
"install",
|
||||
"--workspace",
|
||||
"ui-tui",
|
||||
"--workspace",
|
||||
"ui-tui/packages/hermes-ink",
|
||||
"--include-workspace-root=false",
|
||||
]
|
||||
assert calls[0][1]["cwd"] == str(tmp_path)
|
||||
|
||||
|
||||
def test_make_tui_argv_keeps_desktop_workspace_install_behaviour(
|
||||
tmp_path: Path, main_mod, monkeypatch
|
||||
) -> None:
|
||||
tui_dir = tmp_path / "ui-tui"
|
||||
tui_dir.mkdir()
|
||||
(tui_dir / "package.json").write_text("{}")
|
||||
(tmp_path / "package-lock.json").write_text("{}")
|
||||
|
||||
monkeypatch.delenv("TERMUX_VERSION", raising=False)
|
||||
monkeypatch.setenv("PREFIX", "/usr")
|
||||
monkeypatch.setattr(main_mod, "_tui_need_npm_install", lambda _root: True)
|
||||
monkeypatch.setattr(main_mod.shutil, "which", lambda name: f"/bin/{name}")
|
||||
calls = []
|
||||
|
||||
def fake_run(*args, **kwargs):
|
||||
calls.append((args, kwargs))
|
||||
return types.SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||
|
||||
monkeypatch.setattr(main_mod.subprocess, "run", fake_run)
|
||||
|
||||
main_mod._make_tui_argv(tui_dir, tui_dev=False)
|
||||
|
||||
assert calls[0][0][0] == [
|
||||
"/bin/npm",
|
||||
"install",
|
||||
"--silent",
|
||||
"--no-fund",
|
||||
"--no-audit",
|
||||
"--progress=false",
|
||||
]
|
||||
assert calls[0][1]["cwd"] == str(tmp_path)
|
||||
|
||||
|
||||
def test_make_tui_argv_keeps_desktop_always_build_behaviour(
|
||||
tmp_path: Path, main_mod, monkeypatch
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -164,6 +164,50 @@ class TestBuildWebUISkipsWhenFresh:
|
|||
assert args[0] == ["/usr/bin/npm", "run", "build"]
|
||||
assert kwargs["cwd"] == web_dir
|
||||
|
||||
def test_termux_web_install_is_workspace_scoped(self, tmp_path, monkeypatch):
|
||||
web_dir, _ = _make_web_dir(tmp_path)
|
||||
(tmp_path / "package-lock.json").write_text("{}", encoding="utf-8")
|
||||
monkeypatch.setenv("TERMUX_VERSION", "1")
|
||||
|
||||
install_cp = __import__("subprocess").CompletedProcess([], 0, stdout="", stderr="")
|
||||
build_cp = __import__("subprocess").CompletedProcess([], 0, stdout="", stderr="")
|
||||
with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \
|
||||
patch("hermes_cli.main.subprocess.run", return_value=install_cp) as mock_run, \
|
||||
patch("hermes_cli.main._run_with_idle_timeout", return_value=build_cp):
|
||||
result = _build_web_ui(web_dir)
|
||||
|
||||
assert result is True
|
||||
args, kwargs = mock_run.call_args
|
||||
assert args[0] == [
|
||||
"/usr/bin/npm",
|
||||
"ci",
|
||||
"--workspace",
|
||||
"web",
|
||||
"--include-workspace-root=false",
|
||||
"--silent",
|
||||
]
|
||||
assert kwargs["cwd"] == tmp_path
|
||||
|
||||
def test_desktop_web_install_uses_existing_workspace_root(
|
||||
self, tmp_path, monkeypatch
|
||||
):
|
||||
web_dir, _ = _make_web_dir(tmp_path)
|
||||
(tmp_path / "package-lock.json").write_text("{}", encoding="utf-8")
|
||||
monkeypatch.delenv("TERMUX_VERSION", raising=False)
|
||||
monkeypatch.setenv("PREFIX", "/usr")
|
||||
|
||||
install_cp = __import__("subprocess").CompletedProcess([], 0, stdout="", stderr="")
|
||||
build_cp = __import__("subprocess").CompletedProcess([], 0, stdout="", stderr="")
|
||||
with patch("hermes_cli.main.shutil.which", return_value="/usr/bin/npm"), \
|
||||
patch("hermes_cli.main.subprocess.run", return_value=install_cp) as mock_run, \
|
||||
patch("hermes_cli.main._run_with_idle_timeout", return_value=build_cp):
|
||||
result = _build_web_ui(web_dir)
|
||||
|
||||
assert result is True
|
||||
args, kwargs = mock_run.call_args
|
||||
assert args[0] == ["/usr/bin/npm", "ci", "--silent"]
|
||||
assert kwargs["cwd"] == tmp_path
|
||||
|
||||
|
||||
class TestBuildWebUIRetryAndStaleFallback:
|
||||
"""Coverage for the retry + stale-dist fallback added in #23824 / issue #23817."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue