diff --git a/src/hqt/cli.py b/src/hqt/cli.py index 7069f7f..55cbebb 100644 --- a/src/hqt/cli.py +++ b/src/hqt/cli.py @@ -121,6 +121,47 @@ def doctor(): click.echo("No harnesses found on PATH") +def _build_session_service(): + """Wire a SessionService the same way HqtApp.on_mount does (for CLI use).""" + from hqt.config import get_settings + from hqt.db.engine import ensure_db, get_engine, get_session_factory + from hqt.harnesses.registry import discover_harnesses + from hqt.sessions.service import SessionService + from hqt.tmux.manager import TmuxManager + from hqt.tmux.runner import TmuxRunner + + settings = get_settings() + ensure_db(settings) + factory = get_session_factory(get_engine(settings)) + runner = TmuxRunner(settings.tmux_path, settings.tui_session_name) + return SessionService(factory, TmuxManager(runner), discover_harnesses()) + + +@main.command(name="tool") +@click.argument("tool") +@click.argument("window") +def tool_cmd(tool, window): + """Run TOOL for the session in tmux WINDOW. + + TOOL is nvim/lazygit/shell (opens a styled tool window) or "clone" (a fresh + harness with the same project + model). WINDOW is the tmux window name (the + hqt- key, e.g. from #{window_name}). + """ + import asyncio + + from hqt.errors import ServiceError + + svc = _build_session_service() + try: + if tool == "clone": + asyncio.run(svc.clone_session_for_window(window)) + else: + asyncio.run(svc.open_tool_window_for_window(window, tool)) + except ServiceError as err: + click.echo(str(err), err=True) + raise SystemExit(1) + + @main.command(name="list") def list_cmd(): """List projects and sessions.""" diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..652116b --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,65 @@ +from click.testing import CliRunner + +from hqt import cli +from hqt.config import Settings + + +def test_tool_cmd_opens_tool_window(monkeypatch, tmp_path): + monkeypatch.setattr( + "hqt.config.get_settings", lambda: Settings(db_path=tmp_path / "t.db") + ) + calls = {} + + async def fake_for_window(self, window, tool): + calls["tool"] = (window, tool) + return "@9" + + monkeypatch.setattr( + "hqt.sessions.service.SessionService.open_tool_window_for_window", + fake_for_window, + ) + + result = CliRunner().invoke(cli.main, ["tool", "lazygit", "hqt-5"]) + + assert result.exit_code == 0, result.output + assert calls["tool"] == ("hqt-5", "lazygit") + + +def test_tool_cmd_clone_dispatches_to_clone(monkeypatch, tmp_path): + monkeypatch.setattr( + "hqt.config.get_settings", lambda: Settings(db_path=tmp_path / "t.db") + ) + calls = {} + + async def fake_clone(self, window): + calls["clone"] = window + return "RESULT" + + monkeypatch.setattr( + "hqt.sessions.service.SessionService.clone_session_for_window", fake_clone + ) + + result = CliRunner().invoke(cli.main, ["tool", "clone", "hqt-5"]) + + assert result.exit_code == 0, result.output + assert calls["clone"] == "hqt-5" + + +def test_tool_cmd_reports_service_error(monkeypatch, tmp_path): + from hqt.errors import ServiceError + + monkeypatch.setattr( + "hqt.config.get_settings", lambda: Settings(db_path=tmp_path / "t.db") + ) + + async def boom(self, window, tool): + raise ServiceError("'hqt-5' is not an hqt session window") + + monkeypatch.setattr( + "hqt.sessions.service.SessionService.open_tool_window_for_window", boom + ) + + result = CliRunner().invoke(cli.main, ["tool", "nvim", "hqt-5"]) + + assert result.exit_code == 1 + assert "not an hqt session window" in result.output