78 lines
2.9 KiB
Python
78 lines
2.9 KiB
Python
|
|
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)
|