"""Bottle handle for Apple's `container` CLI.""" from __future__ import annotations import os import subprocess import sys from typing import Callable, cast from ...agent_provider import PromptMode, prompt_args from .. import Bottle, ExecResult from ..terminal import exec_shell_script from . import pty_forward as _pty_forward _PTY_FORWARD_SCRIPT = _pty_forward.__file__ _TERMINAL_ENV_NAMES = ( "TERM", "COLORTERM", "TERM_PROGRAM", "TERM_PROGRAM_VERSION", "KITTY_WINDOW_ID", "KITTY_PID", "WEZTERM_PANE", "WEZTERM_UNIX_SOCKET", "GHOSTTY_BIN_DIR", "GHOSTTY_RESOURCES_DIR", "ITERM_SESSION_ID", "VTE_VERSION", "KONSOLE_VERSION", "ALACRITTY_WINDOW_ID", ) def _terminal_env_names() -> tuple[str, ...]: return tuple( name for name in _TERMINAL_ENV_NAMES if name == "TERM" or os.environ.get(name) ) class MacosContainerBottle(Bottle): def __init__( self, container: str, teardown: Callable[[], None], prompt_path_in_container: str | None, *, agent_command: str = "claude", agent_prompt_mode: PromptMode = "append_file", agent_provider_template: str = "claude", terminal_title: str = "", terminal_color: str = "", agent_workdir: str = "/home/node", ): self.name = container self._teardown = teardown self.prompt_path = prompt_path_in_container self._agent_prompt_mode = agent_prompt_mode self.agent_command = agent_command self.terminal_title = terminal_title self.terminal_color = terminal_color self.agent_provider_template = agent_provider_template self.agent_workdir = agent_workdir self._closed = False def agent_argv(self, argv: list[str], *, tty: bool = True) -> list[str]: full_argv = list(argv) full_argv.extend( prompt_args( cast(PromptMode, self._agent_prompt_mode), self.prompt_path, argv=full_argv, ) ) container_exec = ["container", "exec"] if tty: container_exec.extend(["--interactive", "--tty"]) # Forward terminal capability hints so TUIs can enable modified-key # protocols. Use bare env names: values stay in the child env, not # on argv, and pty_forward supplies a TERM fallback when needed. for name in _terminal_env_names(): container_exec.extend(["--env", name]) if self.agent_workdir and self.agent_workdir != "/home/node": container_exec.extend(["--workdir", self.agent_workdir]) container_exec.extend([self.name, self.agent_command, *full_argv]) if tty: # Wrap with the raw-mode forwarder: container exec does not put # the host terminal into raw mode itself, so the line discipline # buffers modifier-key sequences until CR. The wrapper sets raw # mode before exec and restores it on exit. return [sys.executable, _PTY_FORWARD_SCRIPT, "--", *container_exec] return container_exec def exec_agent(self, argv: list[str], *, tty: bool = True) -> int: agent_argv = self.agent_argv(argv, tty=tty) script = ( exec_shell_script(agent_argv, self.terminal_title, self.terminal_color) if tty else None ) if script is None: return subprocess.run(agent_argv, check=False).returncode return subprocess.run(["sh", "-lc", script], check=False).returncode def exec(self, script: str, *, user: str = "node") -> ExecResult: result = subprocess.run( ["container", "exec", "--user", user, "--interactive", self.name, "sh", "-s"], input=script, capture_output=True, text=True, check=False, ) return ExecResult( returncode=result.returncode, stdout=result.stdout, stderr=result.stderr, ) def cp_in(self, host_path: str, container_path: str) -> None: subprocess.run( ["container", "cp", host_path, f"{self.name}:{container_path}"], stdout=subprocess.DEVNULL, check=True, ) def close(self) -> None: if self._closed: return self._closed = True self._teardown()