Skip to content

feat(shell): auto-build missing devcontainer with live build log#7

Merged
rosstaco merged 2 commits into
mainfrom
feat/dcode-shell-auto-build
May 12, 2026
Merged

feat(shell): auto-build missing devcontainer with live build log#7
rosstaco merged 2 commits into
mainfrom
feat/dcode-shell-auto-build

Conversation

@rosstaco
Copy link
Copy Markdown
Owner

Summary

Two coupled improvements to dcode shell plus a reusable progress UX
helper.

1. dcode shell builds a missing devcontainer on demand

Today, dcode shell against a brand-new project just exits with no
devcontainer found running
. With this PR, it offers to build & start
the container for you so you don't have to open VS Code first:

$ dcode shell ./brand-new-project
dcode: no devcontainer is running for /…/brand-new-project. Build & start it now? [Y/n] y
# (if devcontainer CLI not installed:)
dcode: install the Dev Containers CLI now from
       https://raw.githubusercontent.com/devcontainers/cli/main/scripts/install.sh
       into ~/.devcontainers (no root needed)? [y/N] y
dcode: downloading Dev Containers CLI installer…
dcode: installed Dev Containers CLI at /Users/you/.devcontainers/bin/devcontainer
dcode: building devcontainer for /…/brand-new-project via /Users/you/.devcontainers/bin/devcontainer
fetching layer abc123…       ← scrolls above
extracting layer abc123…     ← scrolls above
running postCreateCommand…   ← scrolls above
⠹ Building devcontainer (this may take several minutes)…   ← pinned, animating
dcode: devcontainer built and started (abc123def456)
# drops you into the shell as the right user

Uses the official @devcontainers/cli (the same Node CLI VS Code's Dev
Containers extension drives under the hood), so the resulting container
carries the same devcontainer.local_folder /
devcontainer.config_file / devcontainer.metadata labels VS Code
expects — open the project in VS Code later and it'll attach to the
same container.

If the CLI isn't installed, dcode offers to fetch the upstream
install.sh (which bundles its own Node.js, no host Node required).
Decline → exit with a clear hint, no auto-fallback to VS Code polling
(unreliable: docker ps … running is not a readiness signal because
lifecycle hooks may still be in progress).

After devcontainer up succeeds we trust the returned containerId
directly and don't re-call find_container — that would mismatch
on WSL because the labels VS Code expects are Windows paths but
devcontainer up writes WSL paths.

2. Resolve remoteUser from the image's devcontainer.metadata label

Coupled correctness fix that was triggered originally on WSL but
affects all platforms: _resolve_exec_user only read
remoteUser/containerUser from the project's devcontainer.json,
which meant images that set the user via metadata (e.g.
mcr.microsoft.com/devcontainers/javascript-node
remoteUser: node) landed users as root because no -u was passed
to docker exec.

We now also read the container's devcontainer.metadata Docker label
written by devcontainers/cli. The merge mirrors the upstream
mergeConfiguration semantics: per-key reverse walk of metadata
layers (last entry wins for each of remoteUser / containerUser),
then remoteUser is preferred over containerUser. The local
devcontainer.json stays the highest-precedence layer (defensive
duplication for containers that predate the metadata label).

3. Reusable progress spinner with live streaming output

New dcode._progress module:

  • with_spinner(label) — bare context manager for short, non-streaming
    work. Used to wrap the VS Code editor launch in run_dcode so users
    see a brief loader for dcode . too.
  • run_streaming(argv, *, label, console=...) — runs a subprocess with
    PIPE stdout/stderr, pumps each stderr line through
    console.print(... markup=False, highlight=False) from a daemon
    thread while the rich Live spinner stays pinned at the bottom.
    Returns a StreamedResult dataclass with returncode, full
    captured stdout (for downstream JSON parsing), full captured
    stderr, and an error field for pre-launch OSError.
    KeyboardInterrupt is propagated after best-effort terminate. Gracefully
    degrades on non-TTY consoles (Live becomes a one-shot status line and
    stderr lines still pass through).

Used by devcontainer_cli.up() (build logs), devcontainer_cli.install_cli()
(installer download/extract), and core.run_dcode() (editor launch).

dcode doctor

New check in the Container panel:

  • ok — Dev Containers CLI: /usr/local/bin/devcontainer (0.86.0)
  • warn — Dev Containers CLI: not on PATH or at ~/.devcontainers/bin/devcontainer …
    with the curl install hint.

Tests

318 passing, lint clean.

  • tests/test_progress.py — 12 new tests covering run_streaming
    (real subprocess for integration coverage of capture + streaming),
    exit-code propagation, env/cwd passthrough, OSError handling,
    markup-safety, supplied-console routing.
  • tests/test_devcontainer_cli.py — 24 tests covering
    find_cli/cli_version/install_cli/up/install_hint with
    mocked _progress.run_streaming and urllib.request.urlopen,
    including object-shaped legacy metadata labels and last-entry-wins
    semantics edge cases.
  • tests/test_shell.py — extended with TestPromptYesNo,
    TestObtainOrInstallCli, TestBuildMissingContainer,
    TestRunShellMissingBuild (accept/decline/build-failure/
    metadata-derived user), plus TestInspectContainerMetadata
    and TestResolveExecUser for the metadata-label fix.
  • tests/test_doctor.pyTestCheckDevcontainerCli plus stub
    updates to existing fixtures.
  • All existing tests continue to pass after refactoring shell-state
    handling and editor launch.

Commits

Two file-level commits, each independently buildable:

  1. feat: live progress spinner with streaming subprocess output
    _progress module + tests + core.run_dcode editor wrapper.
  2. feat(shell): auto-build missing devcontainer; resolve user from image metadata — everything else; uses _progress from chore: audit cleanup (WSL fixes, hatch-vcs, ruff, CI, release-please) #1.

Out of scope

  • Auto-rebuild when devcontainer.json changes (always reuses
    existing container if present).
  • Compose-specific UX beyond what devcontainer up supports.
  • Streaming the stopped-container start prompt (only the actual
    long-running CLI invocations get the live spinner).

rosstaco and others added 2 commits May 12, 2026 23:33
Add a reusable rich-Live-based spinner (`dcode._progress`) that pins to
the bottom of the terminal while subprocess output scrolls above it.

* `with_spinner(label)` — bare context manager for short non-streaming
  work (used to wrap the VS Code editor launch in `run_dcode` so users
  see a brief loader while `code` connects to the running window).
* `run_streaming(argv, *, label, console=...)` — runs a subprocess
  with PIPE stdout/stderr, pumps each stderr line through
  `console.print(... markup=False, highlight=False)` from a daemon
  thread while the spinner stays pinned. Returns a `StreamedResult`
  dataclass with returncode, full captured stdout (for downstream
  parsing), full captured stderr, and an `error` field for pre-launch
  OSError. KeyboardInterrupt is propagated after best-effort terminate.

`core.run_dcode` now wraps editor launches in the spinner and captures
the editor's output, only printing it on non-zero exit so stray VS Code
messages don't trample the spinner.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… metadata

When `dcode shell` runs against a project whose devcontainer has never
been built, prompt to build it via the official `@devcontainers/cli`
(the same Node CLI VS Code's Dev Containers extension uses under the
hood). The build's stderr scrolls live above a pinned spinner via
`dcode._progress.run_streaming`, and we trust the `containerId` returned
by `devcontainer up` directly (no docker-label re-lookup, which would
mismatch on WSL).

If the CLI is not installed, prompt to install it via the upstream
`install.sh` (which bundles its own Node runtime into
`~/.devcontainers`, no host Node required). Install download progress
also streams beneath the spinner. If the user declines, exit with a
clear hint pointing at the curl/npm install commands and at
`dcode <path>` as a fallback. Auto-build always prompts and never runs
without an interactive TTY.

Also fix a related correctness bug: `_resolve_exec_user` only read
`remoteUser` / `containerUser` from the project's devcontainer.json,
which meant images that set the user via metadata (e.g.
`mcr.microsoft.com/devcontainers/javascript-node` → `remoteUser: node`)
landed the user as root. We now also read the container's
`devcontainer.metadata` Docker label, walking layers per-key in reverse
to match devcontainers/cli's `mergeConfiguration` precedence, then
prefer `remoteUser` over `containerUser`. `devcontainer.json` stays
the highest-precedence layer.

`dcode doctor` gains a `Dev Containers CLI` check in the Container
panel: ok with version when found on PATH or at
`~/.devcontainers/bin/devcontainer`, warn with the curl install hint
otherwise.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rosstaco rosstaco merged commit 9709c28 into main May 12, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant