"""DockerBottleCleanupPlan — concrete subclass of BottleCleanupPlan. PRD 0018 chunk 4: cleanup is centered on compose projects. `docker compose ls` is the source of truth for what's running; the plan carries the projects to `compose down`, plus three fallback buckets for legacy / orphan resources: - stray_containers: pre-compose `bot-bottle-*` containers not attached to any compose project. Cleared via `docker rm -f`. - stray_networks: same idea for networks. Cleared via `docker network rm`. - orphan_state_dirs: per-bottle state dirs under ~/.bot-bottle/state/ that have no live compose project AND no `.preserve` marker. Reaped via `shutil.rmtree`. Compose-managed networks are removed by `compose down --volumes`, so they don't appear in stray_networks for a normal project — only truly leftover ones. """ from __future__ import annotations import sys from dataclasses import dataclass from ...log import info from .. import BottleCleanupPlan @dataclass(frozen=True) class DockerBottleCleanupPlan(BottleCleanupPlan): """Resources DockerBottleBackend.cleanup will remove. Produced by `prepare_cleanup`; sorted so the y/N output is stable.""" projects: tuple[str, ...] stray_containers: tuple[str, ...] stray_networks: tuple[str, ...] orphan_state_dirs: tuple[str, ...] @property def empty(self) -> bool: return ( not self.projects and not self.stray_containers and not self.stray_networks and not self.orphan_state_dirs ) def print(self) -> None: print(file=sys.stderr) for name in self.projects: info(f"compose project: {name}") for name in self.stray_containers: info(f"stray container: {name}") for name in self.stray_networks: info(f"stray network: {name}") for name in self.orphan_state_dirs: info(f"orphan state: {name}") print(file=sys.stderr)