mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-13 14:02:16 +00:00
opentui(phase3): launcher integration — HERMES_TUI_ENGINE dual-engine
hermes --tui launches the native OpenTUI engine (Bun) when HERMES_TUI_ENGINE=opentui (env) or display.tui_engine=opentui (config); Ink stays the default and the shipping path is untouched. - _resolve_tui_engine() (env > config > ink); refuses opentui on Windows/Termux (no Bun) -> falls back to ink with a notice. - _make_opentui_argv() -> [bun, src/entry.real.tsx] (no build step). - _bun_bin() with HERMES_BUN override. - Branch at top of _make_tui_argv BEFORE _ensure_tui_node (Bun-only host must not bootstrap Node). - Gate _launch_tui NODE_OPTIONS/--max-old-space-size on engine==ink (Bun is JSC; the V8 flag errors/ignores). Verified end-to-end via tmux: real hermes --tui -> Bun -> OpenTUI -> real Python gateway streamed a real reply. No-flag default still ink.
This commit is contained in:
parent
24f74eb888
commit
2bd9c9b881
741 changed files with 17733 additions and 79889 deletions
|
|
@ -3,8 +3,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
|
@ -18,7 +16,6 @@ from hermes_cli.plugins_cmd import (
|
|||
_repo_name_from_url,
|
||||
_resolve_git_executable,
|
||||
_resolve_git_url,
|
||||
_resolve_subdir_within,
|
||||
_sanitize_plugin_name,
|
||||
)
|
||||
|
||||
|
|
@ -100,127 +97,35 @@ class TestSanitizePluginName:
|
|||
|
||||
|
||||
class TestResolveGitUrl:
|
||||
"""Shorthand and full-URL resolution, with optional subdirectory."""
|
||||
"""Shorthand and full-URL resolution."""
|
||||
|
||||
def test_owner_repo_shorthand(self):
|
||||
url, subdir = _resolve_git_url("owner/repo")
|
||||
url = _resolve_git_url("owner/repo")
|
||||
assert url == "https://github.com/owner/repo.git"
|
||||
assert subdir is None
|
||||
|
||||
def test_https_url_passthrough(self):
|
||||
url, subdir = _resolve_git_url("https://github.com/x/y.git")
|
||||
url = _resolve_git_url("https://github.com/x/y.git")
|
||||
assert url == "https://github.com/x/y.git"
|
||||
assert subdir is None
|
||||
|
||||
def test_ssh_url_passthrough(self):
|
||||
url, subdir = _resolve_git_url("git@github.com:x/y.git")
|
||||
url = _resolve_git_url("git@github.com:x/y.git")
|
||||
assert url == "git@github.com:x/y.git"
|
||||
assert subdir is None
|
||||
|
||||
def test_http_url_passthrough(self):
|
||||
url, subdir = _resolve_git_url("http://example.com/repo.git")
|
||||
url = _resolve_git_url("http://example.com/repo.git")
|
||||
assert url == "http://example.com/repo.git"
|
||||
assert subdir is None
|
||||
|
||||
def test_file_url_passthrough(self):
|
||||
url, subdir = _resolve_git_url("file:///tmp/repo")
|
||||
url = _resolve_git_url("file:///tmp/repo")
|
||||
assert url == "file:///tmp/repo"
|
||||
assert subdir is None
|
||||
|
||||
def test_invalid_single_word_raises(self):
|
||||
with pytest.raises(ValueError, match="Invalid plugin identifier"):
|
||||
_resolve_git_url("justoneword")
|
||||
|
||||
def test_shorthand_with_subdir(self):
|
||||
url, subdir = _resolve_git_url("owner/repo/my-plugin")
|
||||
assert url == "https://github.com/owner/repo.git"
|
||||
assert subdir == "my-plugin"
|
||||
|
||||
def test_shorthand_with_nested_subdir(self):
|
||||
url, subdir = _resolve_git_url("owner/repo/path/to/plugin")
|
||||
assert url == "https://github.com/owner/repo.git"
|
||||
assert subdir == "path/to/plugin"
|
||||
|
||||
def test_shorthand_with_subdir_trailing_slash(self):
|
||||
url, subdir = _resolve_git_url("owner/repo/my-plugin/")
|
||||
assert url == "https://github.com/owner/repo.git"
|
||||
assert subdir == "my-plugin"
|
||||
|
||||
def test_https_url_with_subdir(self):
|
||||
url, subdir = _resolve_git_url("https://github.com/owner/repo.git/my-plugin")
|
||||
assert url == "https://github.com/owner/repo.git"
|
||||
assert subdir == "my-plugin"
|
||||
|
||||
def test_https_url_with_nested_subdir(self):
|
||||
url, subdir = _resolve_git_url(
|
||||
"https://github.com/owner/repo.git/path/to/plugin"
|
||||
)
|
||||
assert url == "https://github.com/owner/repo.git"
|
||||
assert subdir == "path/to/plugin"
|
||||
|
||||
def test_url_with_fragment_subdir(self):
|
||||
url, subdir = _resolve_git_url("https://github.com/owner/repo.git#my-plugin")
|
||||
assert url == "https://github.com/owner/repo.git"
|
||||
assert subdir == "my-plugin"
|
||||
|
||||
def test_file_url_with_fragment_subdir(self):
|
||||
url, subdir = _resolve_git_url("file:///tmp/repo#path/to/plugin")
|
||||
assert url == "file:///tmp/repo"
|
||||
assert subdir == "path/to/plugin"
|
||||
|
||||
def test_ssh_url_with_fragment_subdir(self):
|
||||
url, subdir = _resolve_git_url("git@github.com:owner/repo.git#sub")
|
||||
assert url == "git@github.com:owner/repo.git"
|
||||
assert subdir == "sub"
|
||||
|
||||
|
||||
# ── _resolve_subdir_within ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestResolveSubdirWithin:
|
||||
"""Subdirectory resolution stays within the clone and rejects traversal."""
|
||||
|
||||
def test_valid_subdir(self, tmp_path):
|
||||
(tmp_path / "my-plugin").mkdir()
|
||||
result = _resolve_subdir_within(tmp_path, "my-plugin")
|
||||
assert result == (tmp_path / "my-plugin").resolve()
|
||||
|
||||
def test_valid_nested_subdir(self, tmp_path):
|
||||
(tmp_path / "a" / "b" / "c").mkdir(parents=True)
|
||||
result = _resolve_subdir_within(tmp_path, "a/b/c")
|
||||
assert result == (tmp_path / "a" / "b" / "c").resolve()
|
||||
|
||||
def test_rejects_dot_dot_escape(self, tmp_path):
|
||||
clone = tmp_path / "clone"
|
||||
clone.mkdir()
|
||||
(tmp_path / "secret").mkdir()
|
||||
with pytest.raises(PluginOperationError, match="escapes the repository"):
|
||||
_resolve_subdir_within(clone, "../secret")
|
||||
|
||||
def test_rejects_absolute_path_escape(self, tmp_path):
|
||||
clone = tmp_path / "clone"
|
||||
clone.mkdir()
|
||||
# An absolute path resolves outside the clone root.
|
||||
with pytest.raises(PluginOperationError, match="escapes the repository"):
|
||||
_resolve_subdir_within(clone, "/etc")
|
||||
|
||||
def test_rejects_symlink_escape(self, tmp_path):
|
||||
clone = tmp_path / "clone"
|
||||
clone.mkdir()
|
||||
outside = tmp_path / "outside"
|
||||
outside.mkdir()
|
||||
(clone / "link").symlink_to(outside)
|
||||
with pytest.raises(PluginOperationError, match="escapes the repository"):
|
||||
_resolve_subdir_within(clone, "link")
|
||||
|
||||
def test_rejects_missing_subdir(self, tmp_path):
|
||||
with pytest.raises(PluginOperationError, match="does not exist"):
|
||||
_resolve_subdir_within(tmp_path, "nope")
|
||||
|
||||
def test_rejects_file_not_dir(self, tmp_path):
|
||||
(tmp_path / "afile").write_text("x")
|
||||
with pytest.raises(PluginOperationError, match="not a directory"):
|
||||
_resolve_subdir_within(tmp_path, "afile")
|
||||
def test_invalid_three_parts_raises(self):
|
||||
with pytest.raises(ValueError, match="Invalid plugin identifier"):
|
||||
_resolve_git_url("a/b/c")
|
||||
|
||||
|
||||
# ── _resolve_git_executable ─────────────────────────────────────────────────
|
||||
|
|
@ -793,90 +698,3 @@ class TestNoAutoActivation:
|
|||
# The old code had: "Even with default config, check if a plugin registered one"
|
||||
# The fix removes this. Verify it's gone.
|
||||
assert "Even with default config, check if a plugin registered one" not in source
|
||||
|
||||
|
||||
# ── End-to-end subdirectory install ──────────────────────────────────────────
|
||||
|
||||
|
||||
class TestSubdirInstallE2E:
|
||||
"""Install a plugin that lives in a subdirectory of a real local git repo."""
|
||||
|
||||
@staticmethod
|
||||
def _make_repo_with_subdir_plugin(repo_root: Path) -> None:
|
||||
"""Create a git repo where the plugin lives in ``./my-plugin/`` and the
|
||||
repo root holds unrelated docs/tests."""
|
||||
import subprocess as sp
|
||||
|
||||
repo_root.mkdir(parents=True, exist_ok=True)
|
||||
# Root-level noise: docs + tests that should NOT be installed.
|
||||
(repo_root / "README.md").write_text("# Monorepo docs\n")
|
||||
(repo_root / "tests").mkdir()
|
||||
(repo_root / "tests" / "test_x.py").write_text("def test_x():\n pass\n")
|
||||
# The actual plugin in a subdirectory.
|
||||
plugin_dir = repo_root / "my-plugin"
|
||||
plugin_dir.mkdir()
|
||||
(plugin_dir / "plugin.yaml").write_text(
|
||||
"name: my-plugin\nmanifest_version: 1\ndescription: A subdir plugin\n"
|
||||
)
|
||||
(plugin_dir / "__init__.py").write_text("# plugin entry\n")
|
||||
|
||||
env = {
|
||||
**os.environ,
|
||||
"GIT_AUTHOR_NAME": "t",
|
||||
"GIT_AUTHOR_EMAIL": "t@t",
|
||||
"GIT_COMMITTER_NAME": "t",
|
||||
"GIT_COMMITTER_EMAIL": "t@t",
|
||||
}
|
||||
sp.run(["git", "init", "-q"], cwd=repo_root, check=True, env=env)
|
||||
sp.run(["git", "add", "-A"], cwd=repo_root, check=True, env=env)
|
||||
sp.run(
|
||||
["git", "commit", "-q", "-m", "init"],
|
||||
cwd=repo_root,
|
||||
check=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
def test_installs_only_the_subdir_plugin(self, tmp_path, monkeypatch):
|
||||
if shutil.which("git") is None:
|
||||
pytest.skip("git not available")
|
||||
|
||||
from hermes_cli import plugins_cmd as pc
|
||||
|
||||
repo_root = tmp_path / "monorepo"
|
||||
self._make_repo_with_subdir_plugin(repo_root)
|
||||
|
||||
plugins_dir = tmp_path / "installed"
|
||||
plugins_dir.mkdir()
|
||||
monkeypatch.setattr(pc, "_plugins_dir", lambda: plugins_dir)
|
||||
|
||||
identifier = f"file://{repo_root}#my-plugin"
|
||||
target, manifest, name = pc._install_plugin_core(identifier, force=False)
|
||||
|
||||
# Installed under the plugin's own name, not the repo name.
|
||||
assert name == "my-plugin"
|
||||
assert manifest.get("name") == "my-plugin"
|
||||
assert target == (plugins_dir / "my-plugin").resolve()
|
||||
|
||||
# The plugin's files are present...
|
||||
assert (target / "plugin.yaml").exists()
|
||||
assert (target / "__init__.py").exists()
|
||||
# ...and the repo-root noise is NOT.
|
||||
assert not (target / "README.md").exists()
|
||||
assert not (target / "tests").exists()
|
||||
|
||||
def test_missing_subdir_raises(self, tmp_path, monkeypatch):
|
||||
if shutil.which("git") is None:
|
||||
pytest.skip("git not available")
|
||||
|
||||
from hermes_cli import plugins_cmd as pc
|
||||
|
||||
repo_root = tmp_path / "monorepo"
|
||||
self._make_repo_with_subdir_plugin(repo_root)
|
||||
|
||||
plugins_dir = tmp_path / "installed"
|
||||
plugins_dir.mkdir()
|
||||
monkeypatch.setattr(pc, "_plugins_dir", lambda: plugins_dir)
|
||||
|
||||
identifier = f"file://{repo_root}#does-not-exist"
|
||||
with pytest.raises(PluginOperationError, match="does not exist"):
|
||||
pc._install_plugin_core(identifier, force=False)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue