This CLI is validated for behavioral parity with the reference TypeScript
@devcontainers/cli (pinned at v0.88.0, see parity/). Where it
deliberately differs, the difference is recorded here — this is the durable record of
intentional departures from the oracle, not a backlog. User-facing additions are
documented in go-only-features.md.
These are intentional behavior differences; each is covered by tests and, where it touches a compared surface, reflected in the parity matrix.
- Go-only commands / flags (full reference in
go-only-features.md):checkandsetup(host preflight/remediation),up --cache-image(boot from a prebuilt image, skip build + feature install),read-configuration --cache-key(deterministic content hash; additive — default output is byte-identical to TS),build --secrets-file(BuildKit build secrets; TSbuildhas no such flag),open(launch VS Code attached to the workspace's dev container via a vscode-remote:// URI),stop/down(pause, or stop+remove, a workspace's container — Compose-aware; the upstream CLI leaves teardown to the editor), and the automatic credential bridge that hands the CLI's resolved auth todocker build. --override-configdeep-merges the override onto the base config, whereas TS replaces the config wholesale (readDocument(overrideConfigFile ?? configFile)). With no readable base, the override stands alone — identical to TS. This lets an orchestrator pass a partial override. Only matrix case for the flag is the error path, so contract + semantic stay green.--terminal-log-filetees the same combined stream as--log-file. TS produces two files (a terminal stream with ANSI and a plain one); this CLI keeps a single log stream (no self-managed PTY — see decisions), so both flags capture the same output (without ANSI). Never a black hole.config.build.cacheFromis honored (wired to--cache-fromafter the flag's values) — matchingsingleContainer.ts. Upstream defines the field; this is a parity fix, noted here because it was previously a dead field.BUILDKIT_INLINE_CACHE=1is omitted when--cache-tois an inline exporter (/type\s*=\s*inline/i), matching TSisBuildxCacheToInline— a parity fix over the earlier unconditional build-arg.
- Platform: Linux only (amd64 / arm64). Windows and macOS are not targets — no
runtime/E2E/release/
windows-latest/ConPTY lane. Theplatform="win32"logic is kept solely for parity with the oracle. (arm64 runtime is validated via a non-gating, QEMU-emulated experimental job.) - Runtime: Docker only. Podman is not supported (no parity guarantee or test).
- Compose: v2 only (
docker compose). Compose v1 (docker-compose) is not supported. exec: inherited terminal (docker exec -itinherits the controlling terminal), no self-managed PTY. The 128+N contract comes from the child process. Interactivedocker execis deliberately kept as a shell-out.- Docker Go SDK:
github.com/moby/moby/{client,api}(the v29 "options-in, result-out" surface), replacing the deprecatedgithub.com/docker/docker. The top-levelgithub.com/moby/moby(v2) module is an internal implementation detail and is deliberately not a dependency. Requires Docker Engine API ≥ v1.44 (Docker v25+). - OCI image:
ghcr.io/spin-stack/devcontainer-cli(source repogithub.com/spin-stack/devcontainer-cli), distroless/static, non-root. Contract: it is a distribution/self-contained-operations artifact, not a batteries-included runner. It carries only the static/devcontainerbinary (plus CA certs), so it serves two uses: (1) extracting the binary onto a host, and (2) running the commands that work fully in-process —read-configuration,features/templatesauthoring, OCI push/pull,--version. The commands that shell out (up/build/exec/Compose) need thedocker/buildx/docker composebinaries and a reachable daemon, which the distroless image deliberately does not bundle; run those on a host that has Docker, ordocker cpthe binary out. The release smoke test (--version) validates the distribution use, not the docker-dependent commands (which cannot run from this image by design). - Self-containment stance. Container/engine operations, Docker-context resolution and
git-root detection run in-process (Go libraries / stdlib), and lifecycle hooks run via
the Docker exec API rather than
docker exec. The following are kept as shell-outs on purpose:docker buildx build(buildx feature breadth + the user's builder/context; a library would regress buildx or pull the heavy buildkit client),docker compose(its output is not in the compared stream, so a library adds large deps for zero parity gain), interactivedocker exec -it, and the credential-helper protocol (external executables by design). These shell-outs receive the command'scontext.Context(viaexec.CommandContext), so a deadline/cancel — orCtrl-C— aborts an in-flightbuild/composesubprocess rather than orphaning it.
Known gaps that are deliberately not closed; each is a conscious trade-off, not an oversight.
- Byte-for-byte tarball parity is unattainable because of
mtimedifferences; tarball contents are compared by parsed structure, not raw bytes. - Cloud registry auth matrix is out of default CI scope. The hermetic auth paths
(401→bearer, credential-helper protocol,
DEVCONTAINERS_OCI_AUTH,GITHUB_TOKEN) are unit-tested; a real ACR (identity/refresh) / ECR / authenticated-GHCR matrix is secrets-gated and non-blocking. Credential helpers are Linux-only (secretservice/pass). - TS→Go metadata interop (build with the TS oracle, read with Go) is skip-guarded in
the hermetic unit tests (which do not compile the oracle); it is exercised end-to-end by
the runtime lane's metadata cases (
container-metadata-success,read-configuration.features-configuration). The Go→Go round-trip and whitespace invariance are unit-tested. - Version banner differs cosmetically: this CLI reports a git hash / CalVer, TS a
semver, and the banner box width depends on the version length. Verbose commands
(features-test / features-info) are compared via
exit_code/ stderr rather than the banner. - Legacy Feature fallback via GitHub Releases is not implemented; feature resolution is OCI-first (the supported path for v2 features).