Compose: Expand WSLC Compose with project identity, async operations, and listing - #41526
Compose: Expand WSLC Compose with project identity, async operations, and listing#41526David Bennett (dkbennett) wants to merge 8 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
There are correctness and robustness issues in the new cancellation/GIT callback handling and a few user-facing/size-limit behaviors that should be fixed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This pull request refactors WSLC Compose from a path-keyed cached session model into a project-identity-based async operation model, and adds a wslc compose list/ls surface that discovers managed projects via Docker labels.
Changes:
- Replaces
IWSLCComposeSessionwithBeginComposeOperation/IComposeOperation(completion event, cancellation, typed result) and introducesComposeNormalizer+ComposeReconciler. - Adds managed Compose project listing via
IWSLCSession::ListComposeProjectsand a newwslc compose list/lsCLI command with table/JSON/quiet output. - Updates unit + e2e tests to validate lifecycle behavior, cancellation, unsupported input rejection, and listing behavior.
File summaries
| File | Description |
|---|---|
| test/windows/WSLCComposeTests.cpp | Refactors tests to use async compose operations, project keys, and validates ordered progress + cancellation/validation behavior. |
| test/windows/wslc/WSLCCLICommandUnitTests.cpp | Updates CLI surface test to expect the new compose list subcommand. |
| test/windows/wslc/e2e/WSLCE2EComposeListTests.cpp | Adds e2e coverage for compose list/ls output modes and managed-only project filtering. |
| src/windows/wslcsession/WSLCSession.h | Replaces compose session API with compose operation + project listing; adds reconciler member. |
| src/windows/wslcsession/WSLCSession.cpp | Implements BeginComposeOperation, ListComposeProjects, and adds compose labels + cancel-aware container creation. |
| src/windows/wslcsession/WSLCComposeSession.h | Removes legacy cached compose session COM object. |
| src/windows/wslcsession/WSLCComposeSession.cpp | Removes legacy cached compose session COM object implementation. |
| src/windows/wslcsession/WSLCComposeOperation.h | Adds async compose operation COM object definition (completion event, cancel, result). |
| src/windows/wslcsession/WSLCComposeOperation.cpp | Implements async compose worker thread, request capture/validation, progress events, and result publication. |
| src/windows/wslcsession/ComposeSpec.h | Extends spec model (service name) and changes parsing to accept in-memory content. |
| src/windows/wslcsession/ComposeSpec.cpp | Parses YAML from captured bytes; fails closed on unsupported fields; captures service identity. |
| src/windows/wslcsession/ComposeReconciler.h | Introduces reconciler boundary for per-project locking and action execution. |
| src/windows/wslcsession/ComposeReconciler.cpp | Implements create/up/start/stop semantics with cancel-aware locking and observation. |
| src/windows/wslcsession/ComposeNormalizer.h | Adds normalization/validation boundary for documents, selection, and project key rules. |
| src/windows/wslcsession/ComposeNormalizer.cpp | Normalizes project identity, validates selection, enforces size/NUL constraints, and generates default container names. |
| src/windows/wslcsession/ComposeLabels.h | Centralizes Compose and WSLC-managed label keys/values. |
| src/windows/wslcsession/CMakeLists.txt | Wires new compose sources/headers and removes the old compose session implementation. |
| src/windows/wslc/tasks/ComposeTasks.h | Adds task entrypoints for compose lifecycle + list pipeline. |
| src/windows/wslc/tasks/ComposeTasks.cpp | Implements CLI task pipeline for create/up/start/attach/stop and list output rendering. |
| src/windows/wslc/services/ComposeService.h | Updates CLI service API to use async operations + adds project listing. |
| src/windows/wslc/services/ComposeService.cpp | Implements document capture, async operation execution/wait + compose project listing formatting. |
| src/windows/wslc/services/ComposeProgressCallback.h | Adds CLI-side progress callback interface implementation for compose operations. |
| src/windows/wslc/services/ComposeProgressCallback.cpp | Renders compose progress events and stream forwarding (placeholder output). |
| src/windows/wslc/services/ComposeModel.h | Adds CLI model for compose project list output (NDJSON-friendly). |
| src/windows/wslc/core/ExecutionContextData.h | Adds execution context storage for compose project listing results. |
| src/windows/wslc/commands/ComposeUpCommand.cpp | Moves compose up to task pipeline implementation. |
| src/windows/wslc/commands/ComposeStopCommand.cpp | Moves compose stop to task pipeline and keeps timeout argument handling. |
| src/windows/wslc/commands/ComposeStartCommand.cpp | Moves compose start to task pipeline implementation. |
| src/windows/wslc/commands/ComposeListCommand.cpp | Adds new compose list / ls command and task pipeline. |
| src/windows/wslc/commands/ComposeCreateCommand.cpp | Moves compose create to task pipeline implementation. |
| src/windows/wslc/commands/ComposeCommand.h | Adds ComposeListCommand to the compose command set. |
| src/windows/wslc/commands/ComposeCommand.cpp | Simplifies compose command into subcommand registration only; removes inline implementations. |
| src/windows/wslc/commands/ComposeAttachCommand.cpp | Moves compose attach to task pipeline implementation. |
| src/windows/service/inc/wslc.idl | Defines compose request/result/progress types and replaces IWSLCComposeSession with new async interfaces + list API. |
| msipackage/package.wix.in | Registers new compose COM interfaces in proxy/stub MSI configuration. |
| localization/strings/en-US/Resources.resw | Adds localized help strings for wslc compose list. |
Review details
- Files reviewed: 36/36 changed files
- Comments generated: 5
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
ranm-msft
left a comment
There was a problem hiding this comment.
Went through this properly rather than skimming. Moving off the path-keyed session onto identity plus an async operation is the right call, and the labels and fail-closed parsing are the parts I was most worried about, so those are good. Two things I want resolved before it goes to Pooja.
The cancellation guarantee in the description does not actually hold. ComposeReconciler::Execute calls CheckCancelled after the switch, and RunOperation calls it again once Execute has returned. If Ctrl-C lands while the last Start or Stop is still blocked inside the container call, the mutation completes and we throw ERROR_CANCELLED on top of it. Run only assigns executionResult when RunOperation returns normally, so the caller gets status Cancelled, an empty project key and zero affected containers while the containers are genuinely up. That is precisely the case the description says is prevented. Past the mutation boundary, cancellation should lose.
Second, listing and lifecycle disagree about where truth lives. ListComposeProjects rebuilds from container labels, which is the right answer. But ComposeReconciler::m_projects is in-process only and nothing ever rehydrates it from those labels, so stop, start and attach on a project the service just listed can come back ERROR_NOT_FOUND, and up builds an empty ProjectState and never adopts or removes the containers that are already there. Either adopt by label on a miss, or say plainly in the design that lifecycle is scoped to the session instance.
There was a problem hiding this comment.
🟡 Changes recommended
Ctrl-C cancellation handling can miss/race before the cancel event is created, and the service lacks a documented compose document size limit check before copying request content into memory.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
src/windows/wslc/core/CLIExecutionContext.cpp:23
- RecordCancellationRequest only signals cancellation if CancelEvent is already created; if Ctrl-C arrives before (or races with) CreateCancelEvent, CancellationCount becomes 1 but the event is never signaled, and the next Ctrl-C will fall through to the default handler instead of cancelling the active operation.
bool CLIExecutionContext::RecordCancellationRequest() noexcept
{
const auto cancellationCount = CancellationCount.fetch_add(1, std::memory_order_relaxed) + 1;
return cancellationCount == 1 && CancelEvent && SetEvent(CancelEvent.get());
}
- Files reviewed: 40/40 changed files
- Comments generated: 2
- Review effort level: Lite
Thanks for the detailed review! Both issues are now addressed. Cancellation now matches Docker Compose's non-transactional behavior: completed mutations remain applied, pending mutations do not begin after cancellation, and retrying the idempotent operation converges from discoverable partial state. A late cancellation cannot overwrite a successfully completed final mutation, while cancellation of an attached Labels are indeed the truth and lifecycle operations now rediscover managed containers from Compose labels on every invocation rather than relying on process-local state, including after service or session recreation. Regression coverage was added for late cancellation, partial-state recovery, and label-based lifecycle discovery. |
There was a problem hiding this comment.
🟡 Changes recommended
Ctrl+C cancellation can be dropped due to event-creation timing, and the stated 16 MiB compose document limit is not currently enforced in the service normalization path.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
src/windows/wslc/core/CLIExecutionContext.cpp:33
- RecordCancellationRequest currently returns FALSE when the corresponding event has not been created yet (CancelEvent/ForceCancelEvent are null), which means Ctrl-C may not be treated as handled and the cancellation request is effectively dropped. Since CancellationCount still increments, later Ctrl-C presses can also skip signaling the intended event, preventing graceful/force cancellation from working reliably.
const auto cancellationCount = CancellationCount.fetch_add(1, std::memory_order_relaxed) + 1;
if (cancellationCount == 1)
{
return CancelEvent && SetEvent(CancelEvent.get());
}
- Files reviewed: 43/43 changed files
- Comments generated: 3
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
The force-cancel (second Ctrl+C) path cancels the stop operation but still requires success, and per-project lock caching currently grows unbounded in the service process.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
src/windows/wslc/services/ComposeService.cpp:252
- When the force-cancel event (second Ctrl+C) is signaled, this branch calls operation->Cancel() but still requires the operation to return WSLCComposeOperationStatusSucceeded / S_OK. Since the service treats Cancel() as cooperative cancellation (likely returning ERROR_CANCELLED), this will typically throw and log an error instead of cleanly escalating to the force-kill path.
Consider treating the force-cancel event as “stop waiting and escalate” (run forceAction) without cancelling the stop operation, so it can still complete successfully after the containers are killed.
src/windows/wslcsession/ComposeReconciler.cpp:89
- ResolveProjectLock() stores per-project locks in m_projectLocks as shared_ptrs and never removes them. Over time, running compose operations against many distinct project keys can cause unbounded growth of this map within a long-lived service process.
A simple mitigation is to prune entries whose shared_ptr use_count() is 1 (only held by the map) each time ResolveProjectLock runs, keeping the cache bounded to “in-flight” projects.
- Files reviewed: 43/43 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
There are a couple of correctness/reliability issues in the new async compose flow (force-cancel wait behavior and an unchecked size->ULONG cast) that should be addressed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
src/windows/wslcsession/WSLCComposeOperation.cpp:105
- GetResult() casts m_affectedContainers.size() to ULONG without checking for overflow. If the vector ever exceeds ULONG_MAX, the cast truncates and the allocated buffer size/count become inconsistent, risking memory corruption for callers.
- Files reviewed: 43/43 changed files
- Comments generated: 1
- Review effort level: Lite
ranm-msft
left a comment
There was a problem hiding this comment.
Re-reviewed against the latest push. The label-based rediscovery point from my last pass is addressed - Execute now discovers project containers up front for every action, so listing and lifecycle finally agree on where truth lives. The post-final-mutation cancellation case is improved too. Cancellation can still interrupt partway through a multi-container operation, so the partial-result concern below survives.
Three things.
compose rm reports success without doing the work. Remove filters out WslcContainerStateRunning before the delete loop, then Execute calls containers.clear() unconditionally. On an all-running project that is a successful no-op. On a mixed project it is worse: the stopped containers are deleted, the running ones are left, and the result is cleared so it reports success with zero affected containers. Docker refuses here and makes you pass --stop. I would error when any targeted container is running.
up destroys before it knows it can replace. Up force-deletes every discovered container, then CreateComposeContainers deletes the project network, and only then creates containers and pulls missing images at WSLC_E_IMAGE_NOT_FOUND. A registry hiccup, a bad tag or a port conflict therefore turns a healthy running project into nothing. The scope_exit cleanup only unwinds what this call created; it cannot bring back what Up already deleted. I know desired-versus-observed diffing is P2 and I am not asking for it here. I am asking that the destructive step not run before replacement is known to be viable, or that recreate-only semantics be stated explicitly in the description.
Small one, related to cancellation: m_projectKey is only assigned from executionResult, which stays default-constructed if Execute throws. So a cancelled up that already deleted half the project reports Cancelled with an empty project key, discarding identity we knew before we started. Worth setting the key before entering the reconciler.
|
Correction to my first point above: I said Docker refuses to remove running containers and requires The part I'd still keep on the list is the reporting rather than the eligibility:
Prompting and the Points 2 and 3 stand as written. |
There was a problem hiding this comment.
🔵 Needs a closer look
The new ComposeProgressCallback::OnStreamsReady mishandles non-file/pipe stream handle types (notably sockets), which can break stream relay and handle ownership/closure semantics.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
src/windows/wslc/services/ComposeProgressCallback.cpp:157
- OnStreamsReady treats WSLCHandle payloads as plain Win32 HANDLEs (wil::unique_handle + ReadFile-based RelayHandle) regardless of handle type. If the service ever provides WSLCHandleTypeSocket, this will both (1) attempt ReadFile on a socket and (2) close the socket with CloseHandle instead of closesocket, leading to broken output and handle misuse. This should either reject socket streams explicitly or handle them with socket-aware IO primitives and ownership wrappers (wil::unique_socket / ReadSocketMessageHandle).
src/windows/wslcsession/ComposeSpec.cpp:220 - ComposeSpec::Parse currently incurs an extra full-buffer copy by doing
YAML::Load(std::string{content})wherecontentis already astd::stringin the caller (ComposeNormalizer). For large compose files this doubles peak memory and adds avoidable CPU. Consider changingComposeSpec::Parse(and its helpers) to takeconst std::string&and callYAML::Load(content)directly, updating the single caller accordingly.
- Files reviewed: 43/43 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
It introduces a new COM surface area and async lifecycle/cancellation model, and there is at least one confirmed correctness gap in compose container identity validation/sorting that should be addressed before approval.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
src/windows/wslcsession/WSLCSession.cpp:2481
- DiscoverComposeContainers() uses the Docker Compose
com.docker.compose.container-numberlabel as an opaque string and sorts byservice:container-numberlexicographically. This misorders containers once the container number reaches multiple digits (e.g. 10 sorts before 2) and also accepts non-numeric container-number values as valid WSLC state, which can make resource identity validation fail open.
- Files reviewed: 46/46 changed files
- Comments generated: 0 new
- Review effort level: Lite
|
Feedback addressed up to this point. |
There was a problem hiding this comment.
🔵 Needs a closer look
ComposeSpec parsing currently relies on YAML conversion exceptions for some invalid inputs (non-scalar keys / non-scalar name values), which can bypass the intended structured “invalid compose file” validation messaging and should be fixed before approval.
Review details
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
src/windows/wslcsession/ComposeSpec.cpp:275
- This loop reads
setting.first.as<std::string>()without verifying the YAML key is a scalar. Non-scalar keys will throw a YAML::Exception and surface a generic parser error instead of a targeted validation message. Add an explicitIsScalar()check before converting the key.
This issue also appears on line 285 of the same file.
src/windows/wslcsession/ComposeSpec.cpp:289
nameNode.as<std::string>()assumes thename/container_namevalue is a scalar. If a user provides a non-scalar value (e.g., list/map), YAML conversion will throw and the user will get a low-level YAML error rather than a clear "invalid compose file" validation message. ValidateIsScalar()before callingas<std::string>()and fail with a targeted error.
- Files reviewed: 46/46 changed files
- Comments generated: 0 new
- Review effort level: Lite
ranm-msft
left a comment
There was a problem hiding this comment.
Re-reviewed at 356465ce. All three items from my last pass are resolved, and the first one is resolved better than I asked for it.
Remove. I asked you to error when a targeted container is running. That was the wrong ask - Docker does not reject the operation, it treats running containers as ineligible - and what landed matches Docker instead: stopped containers are removed, running ones are left alone, and the CLI prints No stopped containers when nothing was eligible. The part I care about more is that the result is accurate now. Execute returns the observed entries from Remove rather than clearing the result even when eligible stopped containers had actually been removed.
Project key. Fixed at the right level: local to Run, passed by reference, assigned before the reconciler runs. So once normalization has produced the key, a later cancellation or reconciler failure still reports it.
Up. The warning block covers it. I offered documentation as the alternative to reordering, and "the previous deployment is not restored" states the accepted behavior plainly.
One question on the remove loop:
const HRESULT deleteResult = container->Delete(WSLCDeleteFlagsNone);
THROW_IF_FAILED_EXCEPT(deleteResult, RPC_E_DISCONNECTED);
result[index].State = WslcContainerStateDeleted;Can Delete return RPC_E_DISCONNECTED here only after the container has committed Deleted? PrepareDisconnectComWrapper caches m_state as it stands at disconnect time before nulling the impl, and GetState serves that cache afterwards, so a wrapper disconnected for some other reason can still be holding Created or Exited. If that is reachable, this marks the entry Deleted without having established the container is gone - which is the narrow version of the reporting problem I raised last time, now visible through the new return value.
No objections from me otherwise. Happy to take another look after any further changes.
Summary
This change evolves the initial
wslc composeproof of concept from a configuration-path-based session model into an identity-based, asynchronous project lifecycle model.The primary changes are:
IWSLCComposeSessionobjects with asynchronousIComposeOperationrequests.upcancellation.wslc compose list/lswith table, JSON, quiet, and all-project output.wslc compose removewithdeleteandrmaliases; its current project-wide behavior is equivalent todocker compose rm --force.start,attach,stop, andremoveto address a project by either Compose file path or listed project name.These changes provide clearer ownership boundaries, safer failure behavior, and service contracts that can support further Compose functionality without continuing to expand the original path-keyed proof-of-concept model.
Sample using Compose project listing, stable identity, and asynchronous operations:
PR Checklist
Detailed Description of the Pull Request / Additional comments
Project identity, discovery, and ownership
Compose projects now have an explicit normalized project key. The key comes from an explicit project name when provided, or from the project directory name otherwise.
Default container names follow the project and service identity:
Created containers receive Docker Compose identity labels:
com.docker.compose.projectcom.docker.compose.servicecom.docker.compose.container-numbercom.docker.compose.oneoffCreated networks receive the project and network labels. Containers and networks also receive WSLC ownership and schema labels:
com.microsoft.wslc.compose.managed=truecom.microsoft.wslc.compose.metadata-version=1This distinguishes WSLC-managed resources from unrelated Docker resources and gives the service a versioned boundary for interpreting its metadata.
Labels are now the authoritative source for project discovery. Lifecycle operations rediscover managed containers, including stopped containers, rather than depending on process-local project state. Process-local reconciliation state contains synchronization only.
Project keys and resource identity are validated before use. Missing, duplicate, invalid, or unsupported metadata is not treated as valid WSLC state.
Asynchronous operation contract
The previous cached
IWSLCComposeSessioninterface has been replaced by:WSLCComposeOperationRequestcaptures:IComposeOperationprovides:GetCompletionEvent()for non-polling completion waits.Cancel()for cooperative cancellation.GetResult()for the final status, HRESULT, project key, and affected containers.The service captures and validates the request before launching the worker. The worker runs asynchronously in an MTA and retains the session for its lifetime. Progress callbacks are optional and are marshaled through the COM Global Interface Table when present.
The callback contract supports status, resource progress, diagnostics, and stream notifications. Status events remain available to service clients, while the CLI hides generic validation, planning, execution, and success phases in favor of resource-level progress.
The new interfaces are registered in the MSI proxy/stub configuration.
Cancellation, partial state, and retry
Cancellation is cooperative and non-transactional. Completed mutations remain applied, pending mutations do not begin after cancellation is observed, and a retry rediscovers actual state and converges from that state.
The service checks for cancellation:
An operation is reported as cancelled only when execution actually returns
ERROR_CANCELLED, preventing a late cancellation request from converting an already completed mutation into a cancelled result.Attached
compose upfollows Docker's staged cancellation behavior:The CLI separates arbitrary attached container output from its cancellation message and reports:
Graceful stop remains best-effort across project containers, and failures are surfaced rather than silently discarded.
Resource progress
Compose operations emit ordered resource progress with stable operation and resource-kind codes. The current CLI renders informational stderr lines for:
Examples:
This intentionally uses simple line-oriented output for the current iteration rather than Docker's live TTY table.
Normalization and fail-closed validation
Compose document handling has moved behind a dedicated service-side normalizer.
The request captures the document bytes and caller-resolved context before asynchronous execution. The service validates schema versions, paths, project names, project keys, document counts, and embedded NUL characters. The previous arbitrary 16 MiB document limit was removed; only the
ULONGCOM transport limit remains.The parser fails closed for unsupported fields rather than silently ignoring them. The accepted top-level fields are:
servicesversionfield, treated as a compatibility no-opThe currently accepted service fields are:
namecontainer_nameimageenvironmentworking_dircommandvolumesportsUnsupported external references are rejected before discovery or mutation, including:
includeenv_filelabel_fileextends.fileProfiles, explicit service selection, and dependency inclusion return
E_NOTIMPLconsistently until those semantics are implemented. Failing before mutation prevents partially created projects when the service cannot interpret the complete request correctly.Lifecycle behavior
Compose lifecycle handling passes through a dedicated
ComposeReconcilerboundary. A per-project lock serializes mutations while allowing unrelated projects to proceed independently, and lock acquisition remains cancellation-aware.The command behaviors are:
createcreates project containers without starting them. If the project already has managed containers, it leaves them unchanged without evaluating configuration drift.startstarts existing containers and preserves their container IDs.upapplies the supplied document by force-removing current containers, recreating the default network, creating replacement containers, starting them, and attaching output.attachattaches output from existing project containers without changing lifecycle state.stopstops all project containers using the requested timeout.remove/delete/rmis currently equivalent to project-widedocker compose rm --force: it removes stopped project containers without prompting, leaves running containers untouched, preserves project networks and volumes, and reportsNo stopped containerswhen nothing is eligible.Warning
The
createandupreconciliation lifecycle is intentionally incomplete in this iteration. Docker Compose reuses unchanged resources, starts existing stopped containers duringup, and selectively recreates resources when their configuration or image changes. The currentcreatepath is an existing-project no-op without drift evaluation. The currentuppath is destructive: it force-removes the existing managed project containers and deletes the default network before creating replacements, so if network creation, image pull, or container creation fails, the previous deployment is not restored. Project volumes are preserved. Follow-up work must add the required restart and selective-reconciliation capabilities before these commands can match Docker's lifecycle behavior.Note
The current
removebehavior covers the non-interactive, project-widedocker compose rm --forcecase only. Follow-up PRs are required for confirmation prompts, service selection,--force,--stop,--volumes, and the related command structure.createanduprequire a Compose file.start,attach,stop, andremoveaccept either an existing Compose file or an exact project name returned bycompose list --all. Invalid values produce a localized not-found error before the service operation begins.Note
This project-identity argument behavior does not yet match Docker Compose. Docker separates Compose files (
-f/--file) from project identity (-p/--project-name) through Compose-level options inherited by its subcommands. Supporting that model requires structural changes to how the WSLC CLI handles command-group global arguments. A future PR will resolve this difference; the current dual-purpose lifecycle argument enables functional end-to-end coverage for both file-based and identity-based project operations.Each operation returns the project key and the observed state of its affected containers. This removes the need for the CLI to retain authoritative COM object state between commands.
Compose project listing
This change adds:
Supported options are:
--allto include projects without running containers.--format jsonfor machine-readable output.--quietfor project names only.Project listing is reconstructed from Docker container labels rather than cached CLI state. Container states are aggregated into summaries such as
created(1),running(1), andexited(1).Discovery requires both WSLC-managed provenance and a valid Compose project label. Docker-only Compose resources are excluded. Containers with missing or unsupported WSLC metadata versions are logged and skipped without preventing other valid projects from being listed.
CLI organization
Compose commands use the standard CLI task pipeline and have separate command implementations. The subcommands are presented alphabetically:
attachcreatelist/lsremove/delete/rmstartstopupThis keeps argument declaration, session resolution, project/file resolution, document capture, service invocation, output formatting, and cancellation handling separated instead of concentrating all behavior in
ComposeCommand.cpp.Important
The CLI commands in this proof of concept are functional end-to-end lifecycle surfaces, not final Docker-compatible command contracts. Targeted follow-up PRs are expected to align inherited Compose-level options, command-specific options, prompts, service selection, output details, and lifecycle semantics with Docker Compose.
Validation Steps Performed
The non-fast test run redeployed the package and test distro. Result: 22 passed, 0 failed.
Coverage includes:
upandattachbehavior.remove,delete, andrmcommand forms.docker compose rm --forcebehavior, includingNo stopped containerswhen all project containers are running.