mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-23 10:42:00 +00:00
fix(cli): auto-restore a deleted ui-tui workspace from git before TUI launch
The Windows update path can leave tracked ui-tui/ files deleted in the working tree (HEAD intact). The guard now self-heals: when ui-tui/ is missing in a git checkout, run `git restore -- ui-tui` and continue, falling back to the printed manual-recovery steps only when git can't recover it (no checkout / restore failed). Builds on konsisumer's missing-workspace guard.
This commit is contained in:
parent
537ad9ea9a
commit
db097fb088
2 changed files with 94 additions and 7 deletions
|
|
@ -1650,8 +1650,50 @@ def _find_bundled_tui(hermes_cli_dir: Path | None = None) -> Path | None:
|
|||
return bundled if bundled.is_file() else None
|
||||
|
||||
|
||||
def _exit_missing_tui_workspace(tui_dir: Path) -> "NoReturn":
|
||||
"""Abort TUI launch with a recovery hint when the workspace checkout is missing."""
|
||||
def _restore_tui_workspace(tui_dir: Path) -> bool:
|
||||
"""Try to restore a missing ``ui-tui/`` from git, returning True on success.
|
||||
|
||||
On Windows an antivirus / NTFS filter driver can leave tracked ``ui-tui/``
|
||||
files deleted in the working tree after ``hermes update`` (HEAD stays
|
||||
intact; the files just vanish — see issue #49145). Those files are tracked,
|
||||
so ``git restore`` puts them back deterministically. Best-effort: returns
|
||||
False (rather than raising) when git is unavailable, this isn't a checkout,
|
||||
or the restore leaves the directory still missing — the caller then prints
|
||||
the manual-recovery message.
|
||||
"""
|
||||
git = shutil.which("git")
|
||||
if not git or not (tui_dir.parent / ".git").exists():
|
||||
return False
|
||||
try:
|
||||
subprocess.run(
|
||||
[git, "restore", "--", tui_dir.name],
|
||||
cwd=str(tui_dir.parent),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
except OSError:
|
||||
return False
|
||||
return tui_dir.is_dir()
|
||||
|
||||
|
||||
def _ensure_tui_workspace(tui_dir: Path) -> None:
|
||||
"""Ensure ``ui-tui/`` exists before any npm/node subprocess uses it as cwd.
|
||||
|
||||
Without this, a missing workspace falls through to ``subprocess.run(...,
|
||||
cwd=<missing ui-tui>)``, which crashes with ``NotADirectoryError``
|
||||
(``WinError 267`` on Windows) instead of a usable message (#49145). We
|
||||
first try to self-heal via ``git restore``; only if that can't recover the
|
||||
directory do we abort with concrete manual-recovery steps.
|
||||
"""
|
||||
if tui_dir.is_dir():
|
||||
return
|
||||
|
||||
if _restore_tui_workspace(tui_dir):
|
||||
if not os.environ.get("HERMES_QUIET"):
|
||||
print(f"Restored missing TUI workspace: {tui_dir}")
|
||||
return
|
||||
|
||||
print(
|
||||
"Error: the TUI workspace is missing from this Hermes checkout.\n"
|
||||
f"Expected directory: {tui_dir}\n"
|
||||
|
|
@ -1699,8 +1741,8 @@ def _make_tui_argv(tui_dir: Path, tui_dev: bool) -> tuple[list[str], Path]:
|
|||
)
|
||||
sys.exit(1)
|
||||
|
||||
if not ext_dir and not tui_dir.is_dir():
|
||||
_exit_missing_tui_workspace(tui_dir)
|
||||
if not ext_dir:
|
||||
_ensure_tui_workspace(tui_dir)
|
||||
|
||||
# 1. Prebuilt bundle (nix / packaged release): just run it.
|
||||
if not tui_dev:
|
||||
|
|
|
|||
|
|
@ -327,16 +327,20 @@ def test_make_tui_argv_decodes_dev_prebuild_with_utf8_replace(
|
|||
_assert_utf8_replace_capture(calls[0][1])
|
||||
|
||||
|
||||
def test_make_tui_argv_exits_with_recovery_hint_when_workspace_missing(
|
||||
def test_make_tui_argv_exits_with_recovery_hint_when_workspace_unrecoverable(
|
||||
tmp_path: Path, main_mod, monkeypatch, capsys
|
||||
) -> None:
|
||||
"""Missing ui-tui + no git checkout → clean error, never touches node/npm."""
|
||||
monkeypatch.delenv("HERMES_TUI_DIR", raising=False)
|
||||
monkeypatch.setattr(main_mod, "_ensure_tui_node", lambda: None)
|
||||
|
||||
def fail_which(_name: str) -> str:
|
||||
# No .git beside ui-tui → _restore_tui_workspace bails, fallback message fires.
|
||||
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", fail_which)
|
||||
monkeypatch.setattr(main_mod.shutil, "which", which)
|
||||
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
main_mod._make_tui_argv(tmp_path / "ui-tui", tui_dev=False)
|
||||
|
|
@ -348,6 +352,47 @@ def test_make_tui_argv_exits_with_recovery_hint_when_workspace_missing(
|
|||
assert "hermes update --force" in err
|
||||
|
||||
|
||||
def test_make_tui_argv_restores_missing_workspace_from_git(
|
||||
tmp_path: Path, main_mod, monkeypatch, capsys
|
||||
) -> None:
|
||||
"""Missing ui-tui in a git checkout self-heals via `git restore` and continues."""
|
||||
monkeypatch.delenv("HERMES_TUI_DIR", raising=False)
|
||||
monkeypatch.delenv("HERMES_QUIET", raising=False)
|
||||
monkeypatch.setattr(main_mod, "_ensure_tui_node", lambda: None)
|
||||
|
||||
tui_dir = tmp_path / "ui-tui"
|
||||
(tmp_path / ".git").mkdir() # mark tmp_path as a checkout
|
||||
|
||||
monkeypatch.setattr(main_mod.shutil, "which", lambda name: f"/usr/bin/{name}")
|
||||
|
||||
restore_calls: list[tuple[list[str], object]] = []
|
||||
|
||||
def fake_run(cmd, *args, **kwargs):
|
||||
# Simulate `git restore -- ui-tui` materialising the directory.
|
||||
if cmd[:2] == ["/usr/bin/git", "restore"]:
|
||||
restore_calls.append((cmd, kwargs.get("cwd")))
|
||||
tui_dir.mkdir(exist_ok=True)
|
||||
(tui_dir / "dist").mkdir()
|
||||
(tui_dir / "dist" / "entry.js").write_text("// bundle")
|
||||
(tui_dir / "package.json").write_text("{}")
|
||||
return types.SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||
|
||||
monkeypatch.setattr(main_mod.subprocess, "run", fake_run)
|
||||
# node_modules present + lockfile-in-sync so we skip the install/build path
|
||||
# and land straight on the node dist/entry.js return.
|
||||
monkeypatch.setattr(main_mod, "_tui_need_npm_install", lambda _root: False)
|
||||
monkeypatch.setattr(main_mod, "_is_termux_startup_environment", lambda: False)
|
||||
|
||||
argv, cwd = main_mod._make_tui_argv(tui_dir, tui_dev=False)
|
||||
|
||||
assert restore_calls, "expected a `git restore` attempt"
|
||||
assert restore_calls[0][0] == ["/usr/bin/git", "restore", "--", "ui-tui"]
|
||||
assert restore_calls[0][1] == str(tmp_path)
|
||||
assert argv[-1] == str(tui_dir / "dist" / "entry.js")
|
||||
assert cwd == tui_dir
|
||||
assert "Restored missing TUI workspace" in capsys.readouterr().out
|
||||
|
||||
|
||||
# ── _workspace_root helper ──────────────────────────────────────────
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue