adff1263d8
CLI and dashboard now share one cross-backend abstraction for listing + launching bottles, so adding a backend (docker / smolmachines) lights up in both places without separate wiring. Backend abstraction: - New `ActiveBottle` dataclass (`backend_name`, `slug`, `agent_name`, `started_at`, `services`) replaces the docker-specific `ActiveAgent`. Same field surface for the existing dashboard consumers; `ActiveAgent` becomes a typed alias for source-compat. - New `BottleBackend.enumerate_active() -> Sequence[ActiveBottle]` replaces the old `list_active() -> None` (which printed and returned nothing). Docker implements it via the existing compose query; smolmachines implements it via `smolvm machine ls --json` cross-referenced with each bundle container's `CLAUDE_BOTTLE_SIDECAR_DAEMONS` env (`backend/smolmachines/ enumerate.py`). - New `enumerate_active_bottles()` and `known_backend_names()` module-level helpers fold every backend into one call. - `get_bottle_backend(name=None)` takes an optional explicit name (precedence: arg > $CLAUDE_BOTTLE_BACKEND > "docker"). CLI: - `./cli.py list active` enumerates every backend, prints tab-separated `<backend>\t<slug>\t<agent>\t<services>`. The smolmachines bottle the user was looking for now shows up. - `./cli.py start` grows `--backend=<docker|smolmachines>` (choices pulled live from `known_backend_names()`). Threaded through `prepare_with_preflight(backend_name=...)` so the resume path picks up the flag too. Dashboard: - Active agents pane lists both backends (the row formatter now prefixes `[docker]` / `[smolmachines]`). - New-agent flow inserts a backend picker modal between agent pick and preflight (`_backend_picker_modal`). Short-circuits when only one backend is registered. - `discover_active_agents()` collapses to `enumerate_active_bottles()`; `_parse_services_by_project` and `_query_services_by_project` move to `backend/docker/cleanup.py` where the docker enumerator owns them. Tests: parser + enumerate-active tests relocated to `test_docker_enumerate_active.py`. New `test_backend_selection.py` covers `get_bottle_backend`, `known_backend_names`, `enumerate_active_bottles`. New `test_cli_start_backend_flag.py` covers `--backend`'s argparse shape + the explicit-over-env precedence. 605 unit tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
79 lines
2.4 KiB
Python
79 lines
2.4 KiB
Python
"""SmolmachinesBottleBackend — the smolmachines implementation of
|
|
BottleBackend (PRD 0023)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
from typing import Generator, Sequence
|
|
|
|
from .. import ActiveBottle, BottleBackend, BottleSpec
|
|
from . import enumerate as _enumerate
|
|
from . import launch as _launch
|
|
from . import prepare as _prepare
|
|
from .bottle import SmolmachinesBottle
|
|
from .bottle_cleanup_plan import SmolmachinesBottleCleanupPlan
|
|
from .bottle_plan import SmolmachinesBottlePlan
|
|
from .provision import ca as _ca
|
|
from .provision import git as _git
|
|
from .provision import prompt as _prompt
|
|
from .provision import skills as _skills
|
|
from .provision import supervise as _supervise
|
|
|
|
|
|
class SmolmachinesBottleBackend(
|
|
BottleBackend["SmolmachinesBottlePlan", "SmolmachinesBottleCleanupPlan"]
|
|
):
|
|
"""smolmachines backend. Selected by
|
|
`CLAUDE_BOTTLE_BACKEND=smolmachines`."""
|
|
|
|
name = "smolmachines"
|
|
|
|
def _resolve_plan(
|
|
self, spec: BottleSpec, *, stage_dir: Path
|
|
) -> SmolmachinesBottlePlan:
|
|
return _prepare.resolve_plan(spec, stage_dir=stage_dir)
|
|
|
|
@contextmanager
|
|
def launch(
|
|
self, plan: SmolmachinesBottlePlan
|
|
) -> Generator[SmolmachinesBottle, None, None]:
|
|
with _launch.launch(plan, provision=self.provision) as bottle:
|
|
yield bottle
|
|
|
|
def provision_ca(
|
|
self, plan: SmolmachinesBottlePlan, target: str
|
|
) -> None:
|
|
_ca.provision_ca(plan, target)
|
|
|
|
def provision_prompt(
|
|
self, plan: SmolmachinesBottlePlan, target: str
|
|
) -> str | None:
|
|
return _prompt.provision_prompt(plan, target)
|
|
|
|
def provision_skills(
|
|
self, plan: SmolmachinesBottlePlan, target: str
|
|
) -> None:
|
|
_skills.provision_skills(plan, target)
|
|
|
|
def provision_git(
|
|
self, plan: SmolmachinesBottlePlan, target: str
|
|
) -> None:
|
|
_git.provision_git(plan, target)
|
|
|
|
def provision_supervise(
|
|
self, plan: SmolmachinesBottlePlan, target: str
|
|
) -> None:
|
|
_supervise.provision_supervise(plan, target)
|
|
|
|
def prepare_cleanup(self) -> SmolmachinesBottleCleanupPlan:
|
|
return SmolmachinesBottleCleanupPlan()
|
|
|
|
def cleanup(self, plan: SmolmachinesBottleCleanupPlan) -> None:
|
|
del plan
|
|
# Nothing to clean in chunks 1-3 — see
|
|
# SmolmachinesBottleCleanupPlan docstring.
|
|
|
|
def enumerate_active(self) -> Sequence[ActiveBottle]:
|
|
return _enumerate.enumerate_active()
|