"""기존 tmux 세션에 attach하여 CLI 입출력을 중계한다.""" from __future__ import annotations import logging import subprocess import pexpect logger = logging.getLogger(__name__) class PtyManager: """기존 tmux 세션에 attach하고 입출력을 제어한다.""" def __init__(self, session_name: str = "claude", cli_name: str = "claude") -> None: self.session_name = session_name self.cli_name = cli_name self._process: pexpect.spawn | None = None @property def is_alive(self) -> bool: return self._process is not None and self._process.isalive() def _ensure_session_exists(self) -> None: """attach 대상 tmux 세션 존재 여부를 검증한다.""" result = subprocess.run( ["tmux", "has-session", "-t", self.session_name], check=False, capture_output=True, text=True, ) if result.returncode != 0: raise RuntimeError( "tmux 세션이 없습니다. 먼저 실행하세요: " f"tmux new -s {self.session_name} {self.cli_name}" ) def start(self) -> None: """기존 tmux 세션에 attach한다.""" self._ensure_session_exists() logger.info("tmux 세션 attach: %s", self.session_name) self._process = pexpect.spawn( "tmux", ["attach-session", "-t", self.session_name], encoding="utf-8", timeout=None, ) def send(self, text: str, submit: bool = False) -> None: """프로세스에 텍스트 입력을 전달한다.""" if not self.is_alive: raise RuntimeError("프로세스가 실행 중이 아닙니다.") assert self._process is not None logger.debug("입력 전송: %s", text) if submit: self._process.sendline(text) return self._process.send(text) def send_enter(self) -> None: """엔터 키 입력만 전송한다.""" self.send("", submit=True) def read_output(self, timeout: float = 5) -> str: """프로세스의 출력을 읽는다.""" if not self.is_alive: raise RuntimeError("프로세스가 실행 중이 아닙니다.") assert self._process is not None try: self._process.expect(pexpect.TIMEOUT, timeout=timeout) except pexpect.TIMEOUT: pass return self._process.before or "" def stop(self) -> None: """attach 연결만 종료한다(원격 세션은 유지).""" if self._process is not None: logger.info("tmux attach 종료") self._process.close(force=True) self._process = None