Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copilot Instructions for dcode
# Copilot Instructions for InDevContainer

## Build & Test

Expand All @@ -13,13 +13,39 @@ uv run pytest -k test_resolves_worktree_with_relative_gitdir
uv run pytest -k TestResolveWorktree
```

Linting is via `ruff` (`uv run ruff check`). Versioning is automated: `hatch-vcs` derives the version from the latest git tag, and [release-please](https://github.com/googleapis/release-please) manages tags + the changelog from [Conventional Commits](https://www.conventionalcommits.org/). **Do not** edit a version field in `pyproject.toml` or `src/dcode/__init__.py` — both are derived from the git tag at install/build time via `importlib.metadata`.
Linting is via `ruff` (`uv run ruff check`). Versioning is automated: `hatch-vcs` derives the version from the latest git tag, and [release-please](https://github.com/googleapis/release-please) manages tags + the changelog from [Conventional Commits](https://www.conventionalcommits.org/). **Do not** edit a version field in `pyproject.toml` or `src/indevcontainer/__init__.py` — both are derived from the git tag at install/build time via `importlib.metadata`.

## Architecture

dcode is a single-module CLI (`src/dcode/cli.py`) that constructs `vscode-remote://dev-container+<hex-encoded-host-path><workspace-folder>` URIs and launches VS Code with `--folder-uri`. The entrypoint is `dcode.cli:main`.
InDevContainer is a Python CLI installed as the `idc` binary (package
`indevcontainer`). It constructs `vscode-remote://dev-container+<hex-encoded-host-path><workspace-folder>` URIs and launches VS Code with `--folder-uri`, or shells/copilot directly into running containers via `docker exec`. The entrypoint is `indevcontainer.cli:main`.

The core flow in `run_dcode()`:
The CLI is subcommand-only — no top-level positional. Subcommands:

* `idc code [-i] <path>` — `indevcontainer.core.run_code`. Constructs the
devcontainer URI and launches VS Code (or VS Code Insiders).
* `idc shell <path> [--shell EXE] [-i]` — `indevcontainer.shell.run_shell`.
`docker exec -it`s into the project's running container with the resolved
terminal profile.
* `idc copilot [<path>] [copilot-args...]` —
`indevcontainer.copilot.run_copilot`. Same container resolution as
`idc shell`, but execs `copilot` inside the container. **This subcommand
bypasses argparse** (see `_split_copilot_args` in `cli.py`) so that
arbitrary flags like `--yolo` / `--resume` forward to `copilot` without
requiring a `--` separator. The first non-flag arg is the project path;
`--` is honored as an optional explicit separator.
* `idc doctor [path]` — `indevcontainer.doctor.run_doctor`.
* `idc update [--check]` — `indevcontainer.update.{run_update, run_update_check}`.

Bare `idc` (no subcommand) prints `--help` and exits 0.

The container-resolution logic shared by `idc shell` and `idc copilot` lives
in `indevcontainer.shell.prepare_container_exec()`, which returns a
`ContainerExec` dataclass (container_id, exec_user, workdir, ssh_sock,
workspace_folder, devcontainer_cfg, main_repo, rel_path) or `None` on error
(after printing a hint to stderr).

The core flow in `run_code()`:

1. **Worktree detection** — `resolve_worktree()` checks if `.git` is a file (not directory), parses the `gitdir:` pointer, and validates the path structure to distinguish worktrees from submodules. When the gitdir contains an absolute path from a different environment (e.g. a container), it falls back to walking ancestor directories for the real `.git` dir.
2. **Config lookup** — `find_devcontainer()` searches for `.devcontainer/devcontainer.json` or `.devcontainer.json` in the target (or main repo for worktrees).
Expand All @@ -28,6 +54,7 @@ The core flow in `run_dcode()`:
## Conventions

- `json5` is used to parse `devcontainer.json` (supports JSONC comments and trailing commas).
- All user-facing messages go to `sys.stderr`; stdout is reserved for machine output.
- All user-facing messages go to `sys.stderr` and are prefixed with `idc:`. Stdout is reserved for machine output.
- Tests use `tmp_path` fixtures with mock filesystem layouts (fake `.git` files/dirs) — no real git repos needed. `subprocess.run` is always patched in integration tests.
- The helper `_make_worktree(tmp_path, name)` in the test file creates a complete fake main-repo + worktree layout for test reuse.
- The CLI binary is `idc`; the Python package is `indevcontainer`. They differ on purpose: the short binary is for users, the longer package name is for `importlib.metadata` and `uv tool` (which use the project name from `pyproject.toml`).
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ __pycache__/
dist/
.pytest_cache/
uv.lock
src/dcode/_version.py
src/indevcontainer/_version.py
.ruff_cache/
174 changes: 102 additions & 72 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,57 @@
# dcode 🚀
# InDevContainer 🚀

Open folders in VS Code devcontainers directly from the CLI.
Run code, shells, and the GitHub Copilot CLI inside VS Code devcontainers — straight from your terminal.

Replace the two-step `code .` → "Reopen in Container" workflow with a single command. ✨
> Renamed from `dcode`. Same project, new name and a broader scope. The CLI binary is now `idc`.

## 📦 Install

```bash
uv tool install git+https://github.com/rosstaco/dcode
uv tool install git+https://github.com/rosstaco/InDevContainer
```

## 🔧 Usage
If you have the old `dcode` tool installed, remove it first:

```bash
# Open current folder in devcontainer
dcode .
uv tool uninstall dcode
```

## 🔧 Quick start

```bash
# Open current folder in VS Code via its devcontainer (was: dcode .)
idc code .

# Open a specific path
dcode /path/to/project
idc code /path/to/project

# Use VS Code Insiders
dcode --insiders .
dcode -i .
idc code -i .

# Drop into an interactive shell inside the running devcontainer
idc shell

# Run the GitHub Copilot CLI inside the running devcontainer
idc copilot --yolo --resume
```

If the folder has no `.devcontainer/devcontainer.json`, falls back to plain `code .`.
If the folder has no `.devcontainer/devcontainer.json`, `idc code` falls back to plain `code <path>`.

## 🛠 Commands

### `dcode <path>`
### `idc code [-i] <path>`

Open `<path>` (default: current directory) in VS Code via the configured devcontainer.
Exit code is forwarded from the spawned editor.
Open `<path>` (default: current directory) in VS Code via the configured devcontainer. Exit code is forwarded from the spawned editor.

### `dcode shell`
### `idc shell <path>`

Open an interactive shell inside the project's running devcontainer.

```bash
dcode shell # current directory
dcode shell ./my-project # specific path
dcode shell --shell zsh # explicit shell executable (overrides settings)
idc shell # current directory
idc shell ./my-project # specific path
idc shell --shell zsh # explicit shell executable (overrides settings)
idc shell -i # resolve VS Code Insiders user settings
```

Shell selection priority (highest first):
Expand All @@ -56,25 +67,43 @@ Shell selection priority (highest first):
5. Container login shell from `getent passwd <user>` (`nologin` and `false` are rejected)
6. Fallback: `/bin/bash`, then `/bin/sh`

`dcode shell` always reads the `.linux` terminal settings because devcontainers
`idc shell` always reads the `.linux` terminal settings because devcontainers
run Linux, even on macOS and WSL hosts. Profile `args` and `env` are honored;
if a profile `path` is a list, the first entry is used. `${...}` substitution in
profile values is not resolved in this version, so those values are passed
through verbatim with a warning.

SSH agent forwarding works automatically when VS Code is open and connected to
the devcontainer. `dcode shell` detects the VS Code relay socket at
the devcontainer. `idc shell` detects the VS Code relay socket at
`/tmp/vscode-ssh-auth-*.sock` and sets `SSH_AUTH_SOCK` on `docker exec`. If no
socket is found, it prints a hint to open the project in VS Code first.

### `idc copilot [<path>] [copilot args...]`

Exec the GitHub Copilot CLI (`copilot`) inside the project's running devcontainer.
Shares container resolution with `idc shell` (auto-build, auto-start prompts).

```bash
idc copilot # current directory, no copilot args
idc copilot --yolo --resume # cwd; --yolo and --resume go to copilot
idc copilot ./my-project --resume # explicit path; --resume goes to copilot
idc copilot . -- --some-flag # explicit `--` separator (rarely needed)
idc copilot -- weird-positional # escape hatch: first forwarded arg
# would otherwise be parsed as the path
```

The first non-flag argument (if any) is the project path; everything else is forwarded verbatim to `copilot` inside the container. Use `--` if the first forwarded token would otherwise look like a path.

`copilot` must be installed inside the container — `idc copilot` does a fast `command -v copilot` probe first and fails with a hint if it's missing. Add it via a devcontainer Feature, or install it inside the container (for example, `npm install -g @github/copilot`).

### Auto-build: starting a brand-new devcontainer

If you run `dcode shell` in a project whose devcontainer has never been built,
`dcode shell` will offer to build it for you so you don't have to open VS Code
first:
If you run `idc shell` (or `idc copilot`) in a project whose devcontainer has
never been built, `idc` will offer to build it for you so you don't have to
open VS Code first:

```
dcode: no devcontainer is running for /path/to/proj. Build & start it now? [Y/n]
idc: no devcontainer is running for /path/to/proj. Build & start it now? [Y/n]
```

This uses the official **`@devcontainers/cli`** (the same Node.js CLI VS Code's
Expand All @@ -83,12 +112,12 @@ carries the same `devcontainer.local_folder`, `devcontainer.config_file`, and
`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 shell` will offer to install it:
If the CLI isn't installed, `idc` will offer to install it:

```
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]
idc: install the Dev Containers CLI now from
https://raw.githubusercontent.com/devcontainers/cli/main/scripts/install.sh
into ~/.devcontainers (no root needed)? [y/N]
```

This downloads a self-contained install (bundled Node.js runtime included), so
Expand All @@ -104,16 +133,16 @@ npm install -g @devcontainers/cli

Build progress (Docker layer pulls, feature installation, lifecycle hooks)
streams live above a pinned spinner so you can watch what the CLI is doing
without losing the loader UX. The same spinner shows briefly when `dcode .`
launches VS Code so you always know dcode is doing something.
without losing the loader UX. The same spinner shows briefly when `idc code`
launches VS Code so you always know `idc` is doing something.

If you decline the install, `dcode shell` exits with a hint pointing at the
above commands and at `dcode <path>` (which opens VS Code, where the Dev
If you decline the install, `idc` exits with a hint pointing at the
above commands and at `idc code <path>` (which opens VS Code, where the Dev
Containers extension can build the container instead). Auto-build always
prompts and never runs without an interactive TTY.

The shell runs as `remoteUser` from `devcontainer.json` when set, then
`containerUser`. When neither is set in `devcontainer.json`, dcode reads the
`containerUser`. When neither is set in `devcontainer.json`, `idc` reads the
container's `devcontainer.metadata` Docker label (written by the Dev Containers
extension / devcontainers/cli) and applies the same `remoteUser` →
`containerUser` resolution against the merged metadata layers, so users defined
Expand All @@ -123,13 +152,13 @@ image's `USER` applies.

The working directory matches the URI logic: `<workspaceFolder>/<worktree-relative-path>`
for worktrees, otherwise `<workspaceFolder>`. The path is probed with `test -d`;
if it does not exist, `dcode shell` falls back to the base `<workspaceFolder>`
if it does not exist, `idc` falls back to the base `<workspaceFolder>`
with a warning, or omits `-w` entirely if that is missing too.

Limitations:
Limitations (apply to both `idc shell` and `idc copilot`):

- **GPG agent forwarding is not yet supported.** Commit signing inside the shell
will not work unless you've configured your own GPG forwarding via
- **GPG agent forwarding is not yet supported.** Commit signing inside the
container will not work unless you've configured your own GPG forwarding via
`containerEnv` and a bind mount.
- **`remoteEnv` is not applied.** The environment may differ from VS Code's
integrated terminal; a warning is printed when `remoteEnv` is present in
Expand All @@ -140,52 +169,63 @@ Limitations:
Compose service `user`) is not merged; only the raw `devcontainer.json` file
is read. For complex setups, shell selection may differ from VS Code's
resolved view.
- **Requires an interactive terminal.** `dcode shell` exits with an error when
stdin or stdout is not a TTY, such as in piped or scripted contexts.
- **Requires an interactive terminal.** Both `idc shell` and `idc copilot`
exit with an error when stdin or stdout is not a TTY (e.g. piped or
scripted contexts).

Common errors:

- No `devcontainer.json`: exits non-zero and points you at `dcode doctor`.
- No `devcontainer.json`: exits non-zero and points you at `idc doctor`.
- Container not running, in a non-interactive context (e.g. piped): no
matching devcontainer was found and `dcode shell` cannot prompt; run it
interactively, or run `dcode <path>` first.
- Container stopped: `dcode shell` will prompt to start it.
matching devcontainer was found and `idc` cannot prompt; run it
interactively, or run `idc code <path>` first.
- Container stopped: `idc` will prompt to start it.
- Multiple matching containers: clean up the duplicate containers listed in the
error.
- Docker not available: install/start Docker or Docker Desktop and try again.
- Dev Containers CLI not installed and user declined install: see the
*Auto-build* section above for the curl/npm install commands.

To open a folder literally named `shell`, run `dcode ./shell`.
### Naming-collision workaround

`code`, `shell`, `copilot`, `doctor`, and `update` are subcommands, so
`idc code`, `idc shell`, etc. always invoke them. To open a folder literally
named `shell`, `doctor`, or `update`, prefix the path with `./`:

```bash
idc code ./shell
idc code ./doctor
idc code "$(pwd)/update"
```

### `dcode doctor [path]`
### `idc doctor [path]`

Diagnose the local environment for dcode and print a "what would `dcode <path>` do here?"
Diagnose the local environment for `idc` and print a "what would `idc code <path>` do here?"
plan summary. Read-only — never patches `settings.json` or spawns the editor.

Checks: VS Code editor on PATH, Dev Containers extension, Docker daemon,
Dev Containers CLI on PATH (used by `dcode shell` to auto-build a missing
Dev Containers CLI on PATH (used by `idc shell`/`idc copilot` to auto-build a missing
devcontainer), git, WSL setup (distro, Windows-side `settings.json`,
`dev.containers.executeInWSL`), devcontainer discovery + parse, worktree
sanity, dcode version vs latest GitHub release, install method.
sanity, `idc` version vs latest GitHub release, install method.

```bash
dcode doctor # inspect current directory
dcode doctor /some/path # inspect a specific path
idc doctor # inspect current directory
idc doctor /some/path # inspect a specific path
```

Exit codes:

- `0` — no failing checks (warnings allowed)
- `1` — one or more failing checks

### `dcode update`
### `idc update`

Upgrade the installed `dcode` tool via `uv tool upgrade dcode`. Exit code is forwarded
from `uv`. Returns `1` if `uv` is not on PATH or if `dcode` was not installed via
Upgrade the installed `idc` tool via `uv tool upgrade indevcontainer`. Exit code is forwarded
from `uv`. Returns `1` if `uv` is not on PATH or if `idc` was not installed via
`uv tool`.

### `dcode update --check`
### `idc update --check`

Check for an available update without installing it. Prints local version, latest
GitHub release, and the release URL.
Expand All @@ -196,45 +236,35 @@ Exit codes:
- `1` — a newer release is available
- `2` — network or GitHub API error

### Naming-collision workaround

`shell`, `doctor`, and `update` are subcommands, so `dcode shell`, `dcode doctor`,
and `dcode update` always invoke them. To open a folder literally named `shell`,
`doctor`, or `update`, prefix the path:

```bash
dcode ./shell
dcode ./doctor
dcode "$(pwd)/update"
```

## 🌳 Git worktrees

When you run `dcode .` inside a git worktree, it automatically detects the main repo, finds the devcontainer config there, and opens the worktree folder inside the same container. This means all worktrees share a single devcontainer instance — same extensions, same Copilot context, multiple VS Code windows. 🪟🪟🪟
When you run `idc code .` inside a git worktree, it automatically detects the main repo, finds the devcontainer config there, and opens the worktree folder inside the same container. This means all worktrees share a single devcontainer instance — same extensions, same Copilot context, multiple VS Code windows. 🪟🪟🪟

```bash
cd ~/repos/my-project
git worktree add .worktrees/pr-42 pr-42

# Opens pr-42 in the devcontainer defined in my-project
dcode .worktrees/pr-42
idc code .worktrees/pr-42

# Opens pr-99 in the SAME container, different window
git worktree add .worktrees/pr-99 pr-99
dcode .worktrees/pr-99
idc code .worktrees/pr-99
```

> ⚠️ The worktree must live inside the main repo directory tree (e.g. `.worktrees/`) so it's accessible from the container's mounted volume.

## 🧠 How it works

Constructs a `vscode-remote://dev-container+<hex-path>/workspaces/<name>` URI and launches VS Code with `--folder-uri`. VS Code handles the container lifecycle automatically.
`idc code` constructs a `vscode-remote://dev-container+<hex-path>/workspaces/<name>` URI and launches VS Code with `--folder-uri`. VS Code handles the container lifecycle automatically.

For worktrees, the hex-encoded path points to the main repo (so all worktrees resolve to the same container), while the workspace folder is adjusted to open the worktree subfolder inside the container.

`idc shell` and `idc copilot` skip the VS Code URI dance and `docker exec -it` directly into the container that VS Code (or the Dev Containers CLI auto-build) already created — sharing user, workdir, and SSH agent socket so the inner process behaves like VS Code's integrated terminal.

## 🐧 WSL behavior

When `dcode` runs inside WSL, it:
When `idc code` runs inside WSL, it:

1. Builds the URI using a Windows UNC path (`\\wsl.localhost\<distro>\…`) so VS Code on Windows can resolve the folder.
2. Auto-edits your **Windows** VS Code `settings.json` (under `%APPDATA%\Code\User\` or `Code - Insiders`) to set:
Expand All @@ -243,7 +273,7 @@ When `dcode` runs inside WSL, it:

This is required so the Dev Containers extension talks to Docker inside WSL instead of `docker.exe` on Windows. Comments and trailing commas in your `settings.json` are preserved (in-place patching, not a rewrite).

To opt out, pre-set those keys to whatever values you want — `dcode` only writes them when they're missing or differ from the desired values.
To opt out, pre-set those keys to whatever values you want — `idc` only writes them when they're missing or differ from the desired values.

## 🤝 Contributing

Expand Down
Loading
Loading