## Implementation Plan: Add `devplace containers prune-stopped` command ### Summary Add a new CLI subcommand `prune-stopped` that: 1. Iterates all non‑deleted container instances. 2. For each instance with `status == ST_STOPPED` and `desired_state == DESIRED_STOPPED`, calls `api.mark_for_removal()`. 3. Then runs one full reconciliation pass (`ContainerService().run_once()`) so the marked instances are immediately removed via `backend.rm()` + `store.delete_instance()`. 4. Logs the number of containers marked for removal. No changes to listing surfaces, the reconcile loop, or stored procedures are needed – the existing `ST_REMOVING` → removal pipeline handles everything after marking. ### Files to Modify 1. **`devplacepy/cli/containers.py`** (main implementation) - Add `cmd_containers_prune_stopped(args)` function following the same pattern as `cmd_containers_prune`. - Register it in `register_containers()` as subcommand `"prune-stopped"`. 2. **`devplacepy/cli/__init__.py`** (import registration) - Add `from .containers import cmd_containers_prune_stopped` to `__all__` or the relevant import block. 3. **`devplacepy/templates/docs/services-containers.html`** (documentation) - Add `prune-stopped` to the CLI usage line (e.g., `list | reconcile | prune | prune-builds | prune-stopped | gc-workspaces`). ### Detailed Code – `cmd_containers_prune_stopped` in `cli/containers.py` ```python def cmd_containers_prune_stopped(args): """Remove all stopped containers that have completed their purpose.""" async def run(): from ..services.containers import store, api from ..services.containers.service import ContainerService instances = store.all_instances() marked = 0 for inst in instances: if inst["status"] == store.ST_STOPPED and inst["desired_state"] == store.DESIRED_STOPPED: api.mark_for_removal(inst, actor=("system", "prune-stopped")) marked += 1 if marked: await ContainerService().run_once() print(f"Prune stopped: marked {marked} container(s) for removal.") else: print("No stopped containers to prune.") async_runner(run()) ``` **Registration** – inside `register_containers()` (near existing subcommand registrations): ```python p = sub.add_parser("prune-stopped", help="Remove all stopped containers (completed purpose)") p.set_defaults(func=cmd_containers_prune_stopped) ``` **Import in `cli/__init__.py`** – ensure `cmd_containers_prune_stopped` is exported (e.g., add to the existing tuple of command functions). **Documentation update** – change the CLI usage line in `services-containers.html` from: ``` devplace containers list | reconcile | prune | prune-builds | gc-workspaces ``` to: ``` devplace containers list | reconcile | prune | prune-builds | prune-stopped | gc-workspaces ``` ### Verification 1. **Existing tests pass**: `pip install -e '.[dev]' -q && make test-unit` (exit code 0) 2. **Linting succeeds**: `pip install -q ruff && ruff check .` (exit code 0) 3. **New command is discoverable**: `devplace containers --help` shows `prune-stopped`. 4. **Functional correctness** (manual runtime check, not required for CI but for acceptance): - Start with at least one stopped container (`devplace containers list` shows status “stopped”). - Run `devplace containers prune-stopped`. - Confirm output reports the number of marked containers. - After the next reconciliation cycle (or immediately because `run_once()` was called), the container no longer appears in `devplace containers list`. - Disk space is freed (visible via `docker ps -a` or `df` on the store volume). ### Definition of Done - [ ] `cmd_containers_prune_stopped` is implemented in `devplacepy/cli/containers.py`. - [ ] Subcommand is registered in `register_containers()`. - [ ] Import updated in `devplacepy/cli/__init__.py`. - [ ] Documentation in `devplacepy/templates/docs/services-containers.html` lists the new subcommand. - [ ] Running `devplace containers prune-stopped` with no arguments succeeds and only removes instances with `status == ST_STOPPED` and `desired_state == DESIRED_STOPPED`. - [ ] The existing unit test suite (`make test-unit`) passes with no regressions. - [ ] `ruff check .` passes with no new lint errors.