#!/usr/bin/env python3 import hashlib import os import subprocess import sys import time from pathlib import Path class OpenCodeContainer: IMAGE = "opencode-container:latest" PROJECT_ID_LEN = 12 START_TIMEOUT = 3.0 def __init__(self): self.project_path = Path.cwd().resolve() self.project_id = self._project_id(self.project_path) self.container_name = f"oc-{self.project_path.name}-{self.project_id}" self.docker_context_dir = Path(__file__).resolve().parent # ========================= # Public entrypoint # ========================= def run(self, args: list[str]) -> None: if not self.image_exists(): self.build_image() if not self.container_exists(): self.create_container() if not self.start_container(): print("Recreating container due to failed start") self.remove_container() self.create_container() if not self.start_container(): raise RuntimeError("Container failed to start after recreation") try: self.exec_opencode(args) finally: self.stop_container() # ========================= # Image handling # ========================= def image_exists(self) -> bool: return subprocess.run( ["docker", "image", "inspect", self.IMAGE], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ).returncode == 0 def build_image(self) -> None: print(f"Building image '{self.IMAGE}'") self._run([ "docker", "build", "-t", self.IMAGE, str(self.docker_context_dir), ]) # ========================= # Container lifecycle # ========================= def create_container(self) -> None: uid = os.getuid() gid = os.getgid() print(f"Creating container '{self.container_name}'") self._run([ "docker", "create", "--name", self.container_name, "--user", f"{uid}:{gid}", "--volume", f"{self.project_path}:{self.project_path}", self.IMAGE, "sh", "-c", "sleep infinity", ]) def start_container(self) -> bool: subprocess.run( ["docker", "start", self.container_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) return self.wait_until_running() def stop_container(self) -> None: if self.container_running(): self._run(["docker", "stop", "-t", "0", self.container_name]) def remove_container(self) -> None: subprocess.run( ["docker", "rm", "-f", self.container_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) # ========================= # Execution # ========================= def exec_opencode(self, args: list[str]) -> None: subprocess.run([ "docker", "exec", "-it", "-w", str(self.project_path), self.container_name, "opencode", *args, ], check=True) # ========================= # State checks # ========================= def container_exists(self) -> bool: return subprocess.run( ["docker", "inspect", self.container_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ).returncode == 0 def container_running(self) -> bool: result = subprocess.run( ["docker", "inspect", "-f", "{{.State.Running}}", self.container_name], capture_output=True, text=True, ) return result.returncode == 0 and result.stdout.strip() == "true" def wait_until_running(self) -> bool: deadline = time.time() + self.START_TIMEOUT while time.time() < deadline: if self.container_running(): return True time.sleep(0.1) return False # ========================= # Helpers # ========================= @staticmethod def _project_id(path: Path) -> str: return hashlib.sha256(str(path).encode()).hexdigest()[:12] @staticmethod def _run(cmd: list[str]) -> None: subprocess.run(cmd, check=True) def main() -> None: OpenCodeContainer().run(sys.argv[1:]) if __name__ == "__main__": main()