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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ src/test/container-*/**/src/**/README.md
/src-tauri/target/
/frontend/dist/
/frontend/node_modules/
/frontend/public/devcontainer-engine.js
/frontend/public/devcontainer-engine.js.map
.deno/
26 changes: 19 additions & 7 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,32 @@ issue body; the steps below are the unit of PR-by-PR delivery.

## Phase 1 (cont.) — MVP

- [ ] **Step 5 — `apple_containers` impl**: `pull`, `create`, `start`, `exec`,
- [x] **Step 5 — `apple_containers` impl**: `pull`, `create`, `start`, `exec`,
`logs`, `stop`, `remove`. Behind feature flag `apple-containers-live`
for tests that drive the real `container` CLI.
- [ ] **Step 6 — FileHost bridge wired end-to-end**: WebView-side adapter
- [x] **Step 6 — FileHost bridge wired end-to-end**: WebView-side adapter
(`frontend/src/devcontainer-engine/index.ts`) calls Tauri commands
that read a real `.devcontainer/devcontainer.json`; the spec slice
returns a fully substituted config to Rust.
- [ ] **Step 7 — Lifecycle orchestrator** in Rust: parsed config →
- [x] **Step 7 — Lifecycle orchestrator** in Rust: parsed config →
`ContainerSpec` → `ContainerRuntime`. Run lifecycle hooks via
`portable-pty`, stream logs/events to the WebView.
- [ ] **Step 8 — MVP UI**: dashboard, "Open folder", workspace detail with
- [x] **Step 8 — MVP UI**: dashboard, "Open folder", workspace detail with
Up/Stop/Rebuild/Terminal/Logs (xterm.js).
- [x] **Step 8b — `build:` support**: `ContainerRuntime::build()` plus an
Apple Containers impl that shells out to `container build`, streams
stdout/stderr into the dashboard log pane, and surfaces captured
stderr on failure. Lifecycle dispatches build-vs-pull and emits a
`building` status. Paths in `build.dockerfile` / `build.context`
resolve relative to `.devcontainer/`, matching the upstream spec.
Synthesized tag is `devcontainer-<workspace-slug>:latest`. See
[docs/devcontainer-config-support.md](docs/devcontainer-config-support.md).
- [x] **Step 8c — `dockerComposeFile` policy**: explicitly rejected with
an actionable error. The app's container model is **one container
per workspace folder** (wikis, agents, databases, ML inference,
…); multi-container topologies are composed at the dashboard
level, not via Compose. This is a **product decision, not a
deferral** — Step 10's compose bullet is removed.

## Phase 1 cleanup

Expand All @@ -46,8 +60,7 @@ issue body; the steps below are the unit of PR-by-PR delivery.

- [ ] **Step 10 — Features + templates**: replace the Node `tar`/OCI logic
with Rust (`oci-distribution`, `tar`, `flate2`). Templates browser.
`dockerComposeFile` support (when Apple Containers' compose story is
settled, or via a Rust compose shim). Dotfiles bootstrap.
Dotfiles bootstrap.

## Phase 3 — Other backends and platforms

Expand All @@ -57,7 +70,6 @@ issue body; the steps below are the unit of PR-by-PR delivery.

- Apple Containers programmatic API surface (Swift/C) — keep an eye out so
`apple_containers.rs` can switch from CLI shelling to direct API calls.
- Compose semantics in Apple Containers.
- OCI auth parity with the upstream CLI for Features.
- Sync cadence with `devcontainers/cli`; the spec slice's directory layout
is intentionally close to upstream so periodic merges are cheap.
6 changes: 6 additions & 0 deletions devcontainers-cli.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
"folders": [
{
"path": "."
},
{
"path": "../wiki3-app"
},
{
"path": "../../Wiki3"
}
]
}
43 changes: 42 additions & 1 deletion docs/building-on-macos.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,25 @@ scripts/mac-dev.sh
# == cd src-tauri && cargo tauri dev
```

This honors `tauri.conf.json`'s `beforeDevCommand` (`yarn --cwd ../frontend
This honors `tauri.conf.json`'s `beforeDevCommand` (`yarn --cwd frontend
dev` on `http://localhost:1420`), so Vite + the Rust host come up together
with hot reload.

To crank up Rust-side logging, set `RUST_LOG` before launching:

```sh
RUST_LOG=devcontainers_app_lib=debug scripts/mac-dev.sh
```

Lifecycle stages (`pull`, `build`, `create`, `start`, `exec`, hooks) emit
`tracing` events at `info`, with the `container` CLI's stderr captured at
`error` on failure. The same details are forwarded to the WebView as
`devcontainer://log` events so they show up in the in-app terminal.

For what each `devcontainer.json` field maps to (and what's intentionally
unsupported, like `dockerComposeFile`), see
[devcontainer-config-support.md](devcontainer-config-support.md).

## 4. Release build (`.app` / `.dmg`)

`tauri.conf.json` has `"bundle": { "active": false, ... }`, so bundle
Expand Down Expand Up @@ -86,3 +101,29 @@ Real signing/notarization is out of scope until later in the roadmap.
Use [.devcontainer/](../.devcontainer/) when you want to reproduce the CI
matrix — Rust lints, Deno bundle, spec typecheck — in a clean Linux env.
Use the macOS host scripts above when you want to **run** the app.

## Notes for future-us

### How the WebView loads the engine bundle

The deno-built spec slice (`dist/devcontainer-engine.js`) is copied by
[scripts/build-engine.ts](../scripts/build-engine.ts) into
`frontend/public/devcontainer-engine.js`. Files under `frontend/public/`
are served verbatim by Vite (and copied verbatim into `frontend/dist/`
at build time), which is what we want — the engine bundle has its own
Node polyfills baked in via esbuild, and we don't want Vite/Rollup
rewriting any of it.

Vite 7 deliberately refuses to **`import()`** modules out of `/public/`,
even with `/* @vite-ignore */`, because public assets are not meant to go
through the module pipeline. So [frontend/src/main.ts](../frontend/src/main.ts)
`fetch()`s the bundle as text, wraps it in a `Blob`, and dynamic-imports
the resulting `blob:` URL. The blob URL is opaque to Vite and the
browser treats it as a fully-formed ES module.

If you ever need to tighten the Tauri WebView CSP in
[src-tauri/tauri.conf.json](../src-tauri/tauri.conf.json), make sure
`script-src` keeps `blob:` (or `'unsafe-inline'` is already broad enough
to include it via the dev `csp_dev` override). Without it, the engine
dynamic import will fail in the packaged release build with a CSP
violation.
131 changes: 131 additions & 0 deletions docs/devcontainer-config-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Supported `devcontainer.json` configuration

This doc tracks what the Devcontainers.app host actually understands today,
and how it maps each top-level field onto the Apple `container` CLI. Keep
it in sync with [src-tauri/src/devcontainer/translate.rs](../src-tauri/src/devcontainer/translate.rs)
and [src-tauri/src/devcontainer/lifecycle.rs](../src-tauri/src/devcontainer/lifecycle.rs).

## Container model: one container per repo

The app is intentionally **one container per workspace folder**. A
"workspace" here is usually a wiki/repo, but can also be a long-running
service (agents, databases, ML inference, …). This shapes a few choices:

- `dockerComposeFile` is **explicitly rejected** with a clear error. We do
not plan to add Compose support — multi-container topologies should be
expressed as multiple workspaces, each with its own `devcontainer.json`,
composed at the app/dashboard level rather than at the container layer.
- The synthesized image tag for `build`-based configs is
`devcontainer-<workspace-slug>:latest`, scoped to the workspace.
- Container names are derived from the workspace folder name via
`sanitize_entity_name`.

## Image source: `image` *or* `build` (exactly one)

### `image: "<ref>"`

Pulled with `container image pull <ref>` before create. Parsed via
`parse_image_ref` so registry / repository / tag / digest are tracked
separately.

### `build: { ... }`

Built locally with `container build` before create. Supported sub-fields:

| Field | Behavior |
| ------------ | ---------------------------------------------------------------------- |
| `dockerfile` | Path **relative to `.devcontainer/`**. Defaults to `Dockerfile`. |
| `context` | Path **relative to `.devcontainer/`**. Defaults to `.` (the `.devcontainer/` folder itself). |
| `args` | Forwarded as `--build-arg KEY=VALUE`. Sorted for deterministic argv. |
| `target` | Forwarded as `--target <stage>`. |
| `cacheFrom` | Parsed but not yet forwarded — Apple `container build` has no flag. |

Path resolution mirrors the upstream spec: both `dockerfile` and `context`
are anchored at the directory containing `devcontainer.json`. So in
`/repo/.devcontainer/devcontainer.json`:

```jsonc
{ "build": { "dockerfile": "Dockerfile", "context": ".." } }
```

…builds from `/repo/` with `--file /repo/.devcontainer/Dockerfile`.

The resolved `configFilePath` is forwarded from the JS engine bundle
(`frontend/src/devcontainer-engine/index.ts`) to Rust through
`ParsedDevContainer.config_file_path`, which is what the lifecycle uses
to anchor the relative paths.

### Neither `image` nor `build`

Validation rejects the config. There is no silent fallback to a default
base image — the previous `mcr.microsoft.com/devcontainers/base:ubuntu`
default has been removed.

## Lifecycle: build vs pull dispatch

`LifecycleOrchestrator::resolve_image` decides per-workspace what to do:

```
parsed.build.is_some() → emit "building" status
container build --tag devcontainer-<slug>:latest \
--file <abs dockerfile> \
[--build-arg k=v]... \
[--target stage] \
<abs context>
stream stdout/stderr line-by-line into the
dashboard log pane via LogChunk
on success → use the synthesized tag
parsed.image → emit "pulling" status
container image pull <ref>
on success → use the parsed ImageRef
```

After `resolve_image` returns an `ImageRef`, `to_container_spec` takes
that ref as a parameter — image resolution and spec translation are no
longer entangled.

### Failure surfaces

`apple_containers::build` captures stderr while it streams it. On a
non-zero exit, the error wraps the full `container build …` argv plus
the trimmed stderr, so the dashboard error pill shows what went wrong
without forcing the user to scroll the log.

The `building` status flows through the `EventSink` seam exactly like
`pulling`, so xterm-side rendering and the test `CapturingSink` see the
same events.

## Other top-level fields

| Field | State |
| ---------------------------------- | --------------------------------------------------------------------- |
| `name` | Used to derive container name; sanitized. |
| `workspaceFolder`, `workspaceMount`| Mapped onto a host bind mount of the workspace dir. |
| `mounts` | Forwarded to `container run --mount`. |
| `containerEnv`, `remoteEnv` | Forwarded as `--env KEY=VALUE`. |
| `runArgs` | Appended verbatim to `container run`. |
| `forwardPorts` | Tracked, surfaced in the UI; no automatic publish yet. |
| `postCreateCommand`, `postStartCommand`, `postAttachCommand`, `initializeCommand`, `onCreateCommand`, `updateContentCommand` | Run via `portable-pty`; output streamed to the log pane. |
| `features` | Parsed but not yet installed. OCI-fetch + install layer pending. |
| `customizations` | Forwarded to the WebView; host ignores it. |
| `dockerComposeFile` | **Rejected** with an actionable error message. |

## Tests guarding this behavior

In [src-tauri/src/devcontainer/lifecycle.rs](../src-tauri/src/devcontainer/lifecycle.rs) `mod tests`:

- `up_emits_pulling_creating_running_in_order` — happy path for `image`.
- `up_pull_failure_surfaces_error_status_and_log` — pull failure path.
- `up_with_dockerfile_build_invokes_runtime_build_and_emits_building_status`
— happy path for `build`, asserts the resolved Dockerfile/context paths
and the `devcontainer-<slug>:latest` tag, plus state sequence
`building → creating → created → running`.
- `up_with_build_failure_surfaces_error_status_and_log` — build failure
path: error message includes captured stderr, error status emitted.
- `up_with_compose_config_reports_unsupported_error` — compose rejection
message stays stable.

In [src-tauri/src/container/apple_containers/cli.rs](../src-tauri/src/container/apple_containers/cli.rs):

- `build_args_*` tests pin the exact argv ordering for `container build`,
including deterministic `--build-arg` sort order.
5 changes: 4 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"@tauri-apps/api": "^2.0.0"
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-dialog": "^2.0.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0"
},
"devDependencies": {
"typescript": "^5.9.3",
Expand Down
Loading
Loading