From ca0703feae9812a2bc177539c02e61538b7c3090 Mon Sep 17 00:00:00 2001 From: Austin Pickett Date: Sat, 18 Jul 2026 17:17:25 -0400 Subject: [PATCH] fix(cli): try bundled TUI before requiring ui-tui workspace (#67116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _make_tui_argv() called _ensure_tui_workspace(tui_dir) unconditionally before checking for a prebuilt bundle. That function sys.exit(1)s when ui-tui/ doesn't exist, which it never does on a pip/pipx install — the wheel ships hermes_cli/tui_dist/entry.js but never ships ui-tui/ at all (that directory only exists in a git checkout). Every dashboard Chat tab connection on a pip/pipx install therefore hard-exited before ever reaching _find_bundled_tui(), surfacing as the unhelpful "Chat unavailable: 1" banner despite having a fully valid bundled entry.js on disk. Move the bundled-wheel/HERMES_TUI_DIR shortcut ahead of the workspace check. --dev is unaffected (it never uses the bundled path and still requires the workspace), and the checkout-without-bundle path is unaffected (bundled lookup returns None, falls through to the existing git-restore/npm-install/build flow). Adds a contributors/emails mapping for the original author. Fixes #56665 Co-authored-by: lucaskvasirr --- contributors/emails/lucaskvasir@duck.com | 2 + hermes_cli/main.py | 15 ++++- tests/hermes_cli/test_tui_npm_install.py | 73 ++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 contributors/emails/lucaskvasir@duck.com diff --git a/contributors/emails/lucaskvasir@duck.com b/contributors/emails/lucaskvasir@duck.com new file mode 100644 index 000000000..6285693a7 --- /dev/null +++ b/contributors/emails/lucaskvasir@duck.com @@ -0,0 +1,2 @@ +lucaskvasirr +# PR #56672 salvage (fix(cli): try bundled TUI before requiring ui-tui workspace; #56665) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 1525041c1..88f5fa375 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -1766,10 +1766,14 @@ def _make_tui_argv(tui_dir: Path, tui_dev: bool) -> tuple[list[str], Path]: ) sys.exit(1) - if not ext_dir: - _ensure_tui_workspace(tui_dir) - # 1. Prebuilt bundle (nix / packaged release): just run it. + # + # This must run BEFORE _ensure_tui_workspace() below. A pip/pipx install + # ships hermes_cli/tui_dist/entry.js in the wheel but never ships ui-tui/ + # at all (that directory only exists in a git checkout) — so requiring + # the workspace to exist first made every pip/pipx dashboard Chat tab + # connection hard-exit before it ever got a chance to try the bundled + # entry.js it already has. See #56665. if not tui_dev: if ext_dir: p = Path(ext_dir) @@ -1783,6 +1787,11 @@ def _make_tui_argv(tui_dir: Path, tui_dev: bool) -> tuple[list[str], Path]: node = _node_bin("node") return [node, "--expose-gc", str(bundled)], bundled.parent + # No prebuilt bundle available (or --dev, which never uses one) — we're + # about to npm install/build from source, so the workspace must exist. + if not ext_dir: + _ensure_tui_workspace(tui_dir) + # 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. # Existing desktop behaviour runs npm from the workspace root. Termux diff --git a/tests/hermes_cli/test_tui_npm_install.py b/tests/hermes_cli/test_tui_npm_install.py index e4fd04c55..d466d2867 100644 --- a/tests/hermes_cli/test_tui_npm_install.py +++ b/tests/hermes_cli/test_tui_npm_install.py @@ -361,6 +361,79 @@ def test_make_tui_argv_decodes_dev_prebuild_with_utf8_replace( _assert_utf8_replace_capture(calls[0][1]) +def test_make_tui_argv_uses_bundled_tui_when_workspace_missing( + tmp_path: Path, main_mod, monkeypatch +) -> None: + """pip/pipx install regression (#56665): the wheel ships + hermes_cli/tui_dist/entry.js but never ships ui-tui/ (that directory only + exists in a git checkout). Before this fix, _make_tui_argv called + _ensure_tui_workspace() unconditionally before checking for the bundled + entry.js, so every pip/pipx dashboard Chat tab connection hard-exited + with `sys.exit(1)` — surfaced to the user as the unhelpful "Chat + unavailable: 1" — despite having a perfectly runnable bundled TUI on + disk. The bundled-wheel shortcut must be tried first and succeed without + ever touching the (missing) ui-tui workspace or git. + """ + monkeypatch.delenv("HERMES_TUI_DIR", raising=False) + monkeypatch.setattr(main_mod, "_ensure_tui_node", lambda: None) + + bundled_entry = tmp_path / "bundled" / "entry.js" + bundled_entry.parent.mkdir(parents=True) + bundled_entry.write_text("// bundled TUI") + monkeypatch.setattr(main_mod, "_find_bundled_tui", lambda: bundled_entry) + + def which(name: str) -> str | None: + if name == "node": + return "/usr/bin/node" + raise AssertionError(f"unexpected shutil.which({name!r}) call — bundled path must not need npm/git") + + monkeypatch.setattr(main_mod.shutil, "which", which) + + def fail_run(*_args, **_kwargs): + raise AssertionError("bundled TUI path must not spawn any subprocess (no npm install/build, no git restore)") + + monkeypatch.setattr(main_mod.subprocess, "run", fail_run) + + # ui-tui/ deliberately does not exist under tmp_path, and there is no + # .git either — this mirrors a pip/pipx install exactly. + tui_dir = tmp_path / "ui-tui" + assert not tui_dir.exists() + + argv, cwd = main_mod._make_tui_argv(tui_dir, tui_dev=False) + + assert argv == ["/usr/bin/node", "--expose-gc", str(bundled_entry)] + assert cwd == bundled_entry.parent + + +def test_make_tui_argv_dev_mode_still_requires_workspace_even_with_bundle( + tmp_path: Path, main_mod, monkeypatch, capsys +) -> None: + """--dev never uses the prebuilt bundle (there's no source to hot-reload + from a bundled entry.js), so it must still hit the workspace guard when + ui-tui/ is missing — the bundled-first reordering must not weaken --dev. + """ + monkeypatch.delenv("HERMES_TUI_DIR", raising=False) + monkeypatch.setattr(main_mod, "_ensure_tui_node", lambda: None) + + bundled_entry = tmp_path / "bundled" / "entry.js" + bundled_entry.parent.mkdir(parents=True) + bundled_entry.write_text("// bundled TUI") + monkeypatch.setattr(main_mod, "_find_bundled_tui", lambda: bundled_entry) + + def which(name: str) -> str | None: + if name == "git": + return "/usr/bin/git" + raise AssertionError("node/npm lookup must not run when ui-tui is missing") + + monkeypatch.setattr(main_mod.shutil, "which", which) + + with pytest.raises(SystemExit) as exc: + main_mod._make_tui_argv(tmp_path / "ui-tui", tui_dev=True) + + assert exc.value.code == 1 + assert "TUI workspace is missing" in capsys.readouterr().err + + def test_make_tui_argv_exits_with_recovery_hint_when_workspace_unrecoverable( tmp_path: Path, main_mod, monkeypatch, capsys ) -> None: