Skip to content

feat: add dcode shell subcommand#5

Merged
rosstaco merged 1 commit into
mainfrom
feat/shell-subcommand
May 8, 2026
Merged

feat: add dcode shell subcommand#5
rosstaco merged 1 commit into
mainfrom
feat/shell-subcommand

Conversation

@rosstaco
Copy link
Copy Markdown
Owner

@rosstaco rosstaco commented May 7, 2026

Summary

Adds dcode shell [path] [--shell EXECUTABLE] — opens an interactive shell inside the project's running devcontainer, mirroring VS Code's integrated terminal as closely as possible so things like git push and SSH key auth Just Work without re-configuring anything inside the container.

Behavior

  • Container lookup: by devcontainer.local_folder + devcontainer.config_file Docker labels (the same labels VS Code's Dev Containers extension and @devcontainers/cli set on every container they create). Worktree-aware — uses the main repo path so all worktrees of a repo share one container.
  • Shell selection: VS Code's full settings priority chain — workspace .vscode/settings.json > devcontainer.json customizations.vscode.settings > host user settings.json. terminal.integrated.profiles.linux is deep-merged across layers; null deletes a profile (matches VS Code semantics). Falls back to the container's login shell from getent passwd (rejecting nologin/false), then /bin/bash, then /bin/sh.
  • SSH agent forwarding: detects via docker inspect env or by probing /tmp/vscode-ssh-auth*.sock (newest, validated with test -S) and forwards via docker exec -e SSH_AUTH_SOCK=....
  • User: honors remoteUser then containerUser from devcontainer.json; otherwise the container's image USER applies.
  • Working directory: probes <workspaceFolder>/<rel_path> with test -d before passing -w; falls back cleanly if the path doesn't exist in the container.
  • Stopped containers: interactive prompt (Y/n) to docker start the container before exec'ing.
  • Process replacement: os.execvp("docker", ...) so TTY, Ctrl-C, and resize all inherit naturally.
  • Cross-platform: macOS, Linux, and WSL (with Windows-side label conversion for both local_folder and config_file).

Error messages

Distinct, actionable stderr for: missing devcontainer.json, container missing / stopped / ambiguous (multiple matches), Docker CLI/daemon unavailable, and non-interactive (no TTY) contexts.

Limitations (documented in README)

These are intentional v1 deferrals — separate follow-ups will track each:

  • GPG agent forwarding (the GPG socket env contract — GPG_AGENT_INFO vs direct path vs gpgconf — wasn't verified upstream during research; forwarding incorrectly could silently break commit signing)
  • remoteEnv from devcontainer.json is not applied to the exec session (a one-line warning is emitted when present)
  • Variable substitution (${env:VAR}, ${localEnv:VAR}) in terminal profile values is not resolved
  • Devcontainer config inheritance (extends, image-label metadata, Docker Compose service user) is not merged
  • Requires an interactive terminal (no piped command-execution mode)

Implementation

File Change
src/dcode/shell.py NEW (~640 lines) — find_container, resolve_terminal_profile, find_ssh_socket, probe_workdir, detect_login_shell, run_shell + ContainerLookup/ResolvedShell dataclasses
src/dcode/cli.py New shell subparser with shell_path positional (avoids namespace collision with legacy top-level path) and --shell flag (literal executable; whitespace rejected via parser.error)
src/dcode/wsl.py _get_windows_vscode_settings_path renamed to public get_windows_vscode_settings_path (alias preserved for backward-compat)
tests/test_shell.py NEW — 76 tests across 9 classes
tests/test_cli.py +13 tests in TestShellDispatch
README.md New ### dcode shell section under ## 🛠 Commands

Verification

  • uv run ruff check — clean
  • uv run pytest245 passed, 0 failed (was 156 before this PR; +89 new tests)
  • Manual smoke testing on macOS host + Linux devcontainer

Release

Release-As: 0.5.0 footer in the commit overrides the calculated version (would otherwise be 0.4.4 due to bump-patch-for-minor-pre-major: true). Adding a meaningful new subcommand warrants a minor bump.

Try it

cd ~/some-devcontainer-project
dcode shell                   # current dir, auto-detect shell
dcode shell --shell zsh       # explicit override
dcode shell ./other-project   # different path

Container stopped? You'll be prompted to start it. SSH key not working? Open the folder in VS Code first to start the agent relay, then re-run dcode shell.

Open an interactive shell inside the project's running devcontainer with
behavior that mirrors VS Code's integrated terminal as closely as
possible.

Features
* New `dcode shell [path] [--shell EXECUTABLE]` subcommand
* Locates the running container by `devcontainer.local_folder` +
  `devcontainer.config_file` Docker labels (the same labels VS Code
  Dev Containers and `@devcontainers/cli` set on every container)
* Resolves the shell via VS Code's full settings priority chain
  (workspace `.vscode/settings.json` > `devcontainer.json`
  `customizations.vscode.settings` > host user-level `settings.json`),
  with deep-merge of `terminal.integrated.profiles.linux` across layers
  and `null`-as-delete semantics. Falls back to the container's login
  shell from `getent passwd` (rejecting `nologin`/`false`), then
  `/bin/bash`, then `/bin/sh`
* Forwards the SSH agent socket: detects via `docker inspect` env or
  by probing `/tmp/vscode-ssh-auth*.sock` (newest, validated via
  `test -S`) and exports it via `docker exec -e SSH_AUTH_SOCK=...`
* Honors `remoteUser` then `containerUser` from `devcontainer.json` for
  the `docker exec -u` flag
* Probes the working directory with `test -d` before passing `-w`,
  using `<workspaceFolder>/<rel_path>` for worktrees and falling back
  cleanly when the path doesn't exist in the container
* Worktree-aware: uses the main repo path for container lookup so all
  worktrees of a repo share one container (consistent with the URI
  logic in `dcode <path>`)
* Cross-platform: macOS, Linux, and WSL (with Windows-side label
  conversion for both `local_folder` and `config_file`)
* Replaces the Python process with `os.execvp("docker", ...)` so TTY,
  Ctrl-C, and resize behavior all inherit naturally
* Distinct, actionable error messages for: no `devcontainer.json`,
  container missing / stopped / ambiguous, Docker CLI/daemon
  unavailable, and non-interactive (no TTY) contexts
* Interactive prompt to auto-start a stopped container (`docker start
  <id>` after Y/n confirmation; declines or non-TTY contexts exit
  cleanly with guidance)

Limitations (documented in README)
* GPG agent forwarding is not yet supported
* `remoteEnv` is not applied (warning emitted when present)
* `${env:VAR}` and similar substitution in profile args/env is not
  resolved (warning emitted on first encounter)
* Devcontainer config inheritance (`extends`, image-label metadata,
  Docker Compose service `user`) is not merged
* Requires an interactive terminal

Implementation
* New module `src/dcode/shell.py` (~640 lines) with `find_container`,
  `resolve_terminal_profile`, `find_ssh_socket`, `probe_workdir`,
  `detect_login_shell`, `run_shell`, plus `ContainerLookup` and
  `ResolvedShell` dataclasses
* `src/dcode/cli.py`: new `shell` subparser with `shell_path`
  positional (avoids namespace collision with the legacy top-level
  `path`) and `--shell` flag (literal executable; whitespace rejected)
* `src/dcode/wsl.py`: `_get_windows_vscode_settings_path` renamed to
  public `get_windows_vscode_settings_path` (alias preserved for
  backward compatibility)

Tests
* New `tests/test_shell.py`: 76 tests across 9 classes
* `tests/test_cli.py`: +13 tests in `TestShellDispatch`
* All 245 tests pass; ruff clean

To open a folder literally named `shell`, use `dcode ./shell` (mirrors
the existing `dcode ./doctor` workaround).

Release-As: 0.5.0

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rosstaco rosstaco merged commit 9717596 into main May 8, 2026
3 checks passed
@rosstaco rosstaco deleted the feat/shell-subcommand branch May 8, 2026 03:28
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