ComposeDetector & ComposeClient¶
ComposeDetector¶
Discovers Compose projects from container labels. Optionally merges with compose file config to include services with no running containers.
Usage¶
import { ComposeDetector } from 'sidekick-docker-shared';
const detector = new ComposeDetector();
const containers = await client.listContainers();
const projects = detector.detect(containers);
Detection Methods¶
- Label-based (primary) — reads
com.docker.compose.projectandcom.docker.compose.servicefrom container labels - File-based (secondary) — runs
docker compose configto discover the full service list from compose files
The detector merges both sources, so you see running containers and planned services in a single view.
Return Type¶
Returns an array of ComposeProject objects, each containing:
name— project nameservices— array ofComposeServiceobjects with name, state, container ID (if running), and configurationworkingDir,configFile,configFiles— optional recorded project location and files in override order;configFileremains the first file for compatibilitystatus—running,partial, orstopped, accounting for all discovered replicas
Services include optional replicas, runningReplicas, and totalReplicas fields. Each ComposeReplica records containerId, state, image, and ports. The service's existing containerId refers to a deterministic representative, preferring a running replica. Planned services have an empty replica list and zero counts.
ComposeClient¶
Wraps docker compose CLI commands for project lifecycle management.
Usage¶
import { ComposeClient } from 'sidekick-docker-shared';
const compose = new ComposeClient();
await compose.up('my-project');
await compose.down('my-project');
await compose.restart('my-project', 'web');
Methods¶
| Method | Signature | Description |
|---|---|---|
up |
(project: string, cwd?: string | ComposeCommandOptions) => Promise<ComposeExecResult> |
Start a Compose project (docker compose up -d) |
down |
(project: string, cwd?: string | ComposeCommandOptions) => Promise<ComposeExecResult> |
Stop a Compose project (docker compose down) |
restart |
(project: string, service?: string, cwd?: string | ComposeCommandOptions) => Promise<ComposeExecResult> |
Restart a project or specific service |
stop |
(project: string, service?: string, cwd?: string | ComposeCommandOptions) => Promise<ComposeExecResult> |
Stop a project or specific service |
start |
(project: string, service?: string, cwd?: string | ComposeCommandOptions) => Promise<ComposeExecResult> |
Start existing service containers |
streamLogs |
(project: string, service?: string, tail?: number, signal?: AbortSignal, since?: string) => AsyncIterable<LogEntry> |
Follow logs; default tail is 100; since (RFC 3339) resumes after a reconnect |
Lifecycle methods resolve to a ComposeExecResult (exitCode, stdout, stderr); use throwIfComposeFailed(result, operation) to turn a nonzero exit into a ComposeError. String cwd arguments remain supported. ComposeCommandOptions accepts { cwd?: string; configFiles?: string[] }, passing every file with -f in the supplied order.
Use resolveComposeOptions(source, fallback?) to resolve recorded paths and reject missing files before an action:
import { ComposeClient, resolveComposeOptions, throwIfComposeFailed } from 'sidekick-docker-shared';
const compose = new ComposeClient();
const options = resolveComposeOptions(project, workspaceDirectory);
const result = await compose.up(project.name, options);
throwIfComposeFailed(result, 'Up');
streamLogs has its own abort signal, preserves split UTF-8 and trailing output, and throws when the command fails. The lifecycle options object does not change its signature.
All operations shell out to docker compose rather than using the Docker API directly, because compose orchestration logic lives in the compose CLI.
new ComposeClient(extraEnv?, options?) — extraEnv points every spawned docker compose at the same endpoint as the API client (see dockerCliEnv); options sets deadlines: queryTimeoutMs (logs/ps, default 30s) and actionTimeoutMs (lifecycle commands, default 15 min, since up may pull or build). A command killed by its deadline resolves with exit code 124 (TIMEOUT_EXIT_CODE) and a "timed out" line on stderr.
runProcess¶
runProcess(cmd, args, { cwd?, env?, timeoutMs?, signal?, killGraceMs? }) runs a command to completion and resolves with { exitCode, stdout, stderr }. It is the process runner behind ComposeClient and ComposeFileReader: output is decoded with a StringDecoder per stream (split multi-byte characters survive), a process past its deadline gets SIGTERM then SIGKILL, and spawn failures (e.g. no docker binary) reject.
CachedComposeFileReader¶
Wraps ComposeFileReader (docker compose config, 10s deadline) so the config is only re-read when its inputs could have changed:
import { CachedComposeFileReader, ComposeFileReader } from 'sidekick-docker-shared';
const reader = new CachedComposeFileReader(new ComposeFileReader(cliEnv));
const config = await reader.read(process.cwd()); // spawns once
await reader.read(process.cwd()); // cached: compose files unchanged
await reader.read(process.cwd(), { force: true }); // re-read now
No compose file → null without spawning; unchanged files (by a stat signature) → the cached result, re-read anyway after maxAgeMs (default 5 min) to catch include:/extends: edits; concurrent reads share one spawn; timeouts and other transient failures are not cached. DockerStateStore uses it by default.