refactor(bottles): two-phase cleanup parallel to prepare/launch
test / run tests/run_tests.py (pull_request) Successful in 13s

cmd_cleanup used to only sweep running containers via `docker ps`,
missing stopped pipelock sidecars and orphaned networks entirely. On
my host the new version surfaced ~10 stranded networks left behind by
SIGKILLed sessions — the kind of thing the old command implied it was
handling.

New shape, symmetric with start:
- BottleCleanupPlan (abstract, in bottles/__init__.py) with `print` +
  `empty` abstract members.
- DockerBottleCleanupPlan (concrete, in bottles/docker.py) carrying
  the resolved tuples of containers and networks.
- BottlePlatform gains abstract prepare_cleanup() + cleanup(plan).
  DockerBottlePlatform implements both:
    - prepare_cleanup: docker ps -a + docker network ls, both
      filtered to ^claude-bottle-, sorted for stable output.
    - cleanup: docker rm -f containers first (they hold the network
      attachment), then docker network rm.
- cmd_cleanup is now ~25 lines: prepare → print → y/N → cleanup.
This commit is contained in:
2026-05-10 23:14:54 -04:00
parent 4a45c267f3
commit 18d29fc23f
3 changed files with 137 additions and 25 deletions
+12 -23
View File
@@ -1,42 +1,31 @@
"""cleanup: stop and remove all active claude-bottle containers."""
"""cleanup: stop and remove all orphaned claude-bottle resources
(containers + networks) left behind by previous bottles."""
from __future__ import annotations
import subprocess
import sys
from .. import docker as docker_mod
from ..bottles import get_bottle_platform
from ..log import info
from ._common import read_tty_line
def cmd_cleanup(_argv: list[str]) -> int:
docker_mod.require_docker()
result = subprocess.run(
["docker", "ps", "--filter", "name=^claude-bottle-", "--format", "{{.Names}}"],
capture_output=True,
text=True,
)
containers = (result.stdout or "").strip()
if not containers:
info("no active claude-bottle containers")
platform = get_bottle_platform()
plan = platform.prepare_cleanup()
if plan.empty:
info("no claude-bottle resources to clean up")
return 0
print(file=sys.stderr)
for name in containers.splitlines():
info(f"found: {name}")
print(file=sys.stderr)
plan.print()
sys.stderr.write("claude-bottle: remove all of the above? [y/N] ")
sys.stderr.flush()
reply = read_tty_line()
if reply not in ("y", "Y", "yes", "YES"):
info("aborted")
return 0
for name in containers.splitlines():
info(f"removing {name}")
subprocess.run(
["docker", "rm", "-f", name],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
platform.cleanup(plan)
info("done")
return 0