from __future__ import annotations import os import shlex from uuid import uuid4 import subprocess from dataclasses import dataclass from .registry import repository_paths, validate_targets class TmuxError(RuntimeError): pass @dataclass class Endpoint: session: str pid: int repos: list[str] instance_id: str = "" @property def endpoint_id(self) -> str: return f"tmux-amq-{self.pid}" @property def instance_key(self) -> str: return self.instance_id or self.endpoint_id class TmuxManager: def __init__(self, session: str = "tamq"): self.session = session def _run(self, *args: str, check: bool = True) -> str: result = subprocess.run(["tmux", *args], text=True, capture_output=True, check=False) if check and result.returncode: raise TmuxError(result.stderr.strip() or "tmux command failed") return result.stdout.strip() def ensure(self, repos: list[str], command: str = "codex") -> Endpoint: if not repos: raise TmuxError("at least one repository is required") try: validate_targets(repos) except Exception as exc: raise TmuxError(str(exc)) from exc result = subprocess.run(["tmux", "has-session", "-t", self.session], capture_output=True) if result.returncode != 0: self._run("new-session", "-d", "-s", self.session, "-n", "__tamq_boot", "-c", os.getcwd(), "sleep", "infinity") pid_text = self._run("display-message", "-p", "-t", self.session, "#{pid}") try: pid = int(pid_text) except ValueError as exc: raise TmuxError(f"unable to determine tmux PID: {pid_text!r}") from exc endpoint = Endpoint(self.session, pid, list(dict.fromkeys(repos)), f"tmux-amq-{pid}-{uuid4().hex[:12]}") paths = repository_paths() windows = self._run("list-windows", "-t", self.session, "-F", "#{window_name}").splitlines() for index, repo in enumerate(endpoint.repos): if repo not in windows: if index == 0 and "__tamq_boot" in windows: self._run("rename-window", "-t", f"{self.session}:__tamq_boot", repo) windows[windows.index("__tamq_boot")] = repo else: try: repo_path = paths[repo] except KeyError as exc: raise TmuxError(f"gita path missing for registered repository: {repo}") from exc self._run("new-window", "-d", "-t", self.session, "-n", repo, "-c", repo_path, "sleep", "infinity") tap = ["tamq", "tap", "--repo", repo, "--endpoint", endpoint.endpoint_id, "--", command] self._run("send-keys", "-t", f"{self.session}:{repo}", shell_join(tap), "C-m") return endpoint def shell_join(parts: list[str]) -> str: return " ".join(shlex.quote(part) for part in parts)