boxdown 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +135 -12
  2. package/assets/devcontainer/README.md +49 -17
  3. package/assets/devcontainer/devcontainer.json +24 -7
  4. package/assets/devcontainer/hooks/initialize.sh +32 -0
  5. package/assets/devcontainer/hooks/post-create.sh +59 -12
  6. package/assets/devcontainer/hooks/post-start.sh +21 -4
  7. package/assets/devcontainer/ssh-config-install.sh +12 -3
  8. package/assets/devcontainer/start.sh +721 -44
  9. package/assets/devcontainer/utils/coding-agent-cli-update.sh +267 -3
  10. package/assets/devcontainer/utils/deps-install.sh +68 -0
  11. package/assets/devcontainer/utils/git-config-bootstrap.sh +128 -0
  12. package/assets/devcontainer/utils/git-signing-bootstrap.sh +56 -0
  13. package/assets/devcontainer/utils/python-bootstrap.sh +69 -0
  14. package/assets/devcontainer/utils/ssh-bootstrap.sh +9 -0
  15. package/dist/bin/cli.cjs +1 -1
  16. package/dist/bin/cli.mjs +1 -1
  17. package/dist/main-Df4E8ARj.cjs +5498 -0
  18. package/dist/main-XMBsKjIK.mjs +5458 -0
  19. package/dist/main-XMBsKjIK.mjs.map +1 -0
  20. package/dist/main.cjs +5 -2
  21. package/dist/main.d.cts +495 -4
  22. package/dist/main.d.cts.map +1 -1
  23. package/dist/main.d.mts +495 -4
  24. package/dist/main.d.mts.map +1 -1
  25. package/dist/main.mjs +2 -2
  26. package/docs/README.md +1 -0
  27. package/docs/architecture.md +32 -0
  28. package/docs/development.md +13 -6
  29. package/docs/features/README.md +2 -0
  30. package/docs/features/commit-signing.md +23 -0
  31. package/docs/features/generated-config-and-state.md +57 -4
  32. package/docs/features/github-auth-refresh.md +12 -0
  33. package/docs/features/lifecycle.md +97 -11
  34. package/docs/features/setup.md +66 -0
  35. package/docs/features/ssh-config-and-proxy.md +228 -7
  36. package/docs/features/start-and-shell.md +40 -5
  37. package/docs/superpowers/plans/2026-07-11-default-commit-signing.md +110 -0
  38. package/docs/superpowers/plans/2026-07-11-progress-ci-regression.md +125 -0
  39. package/docs/superpowers/specs/2026-07-11-default-commit-signing-design.md +396 -0
  40. package/docs/superpowers/specs/2026-07-11-progress-ci-regression-design.md +43 -0
  41. package/docs/testing.md +35 -2
  42. package/docs/todo.md +2 -2
  43. package/package.json +1 -1
  44. package/src/claude-app-config.ts +304 -0
  45. package/src/cli-style.ts +43 -0
  46. package/src/codex-app-config.ts +656 -0
  47. package/src/config.ts +68 -10
  48. package/src/constants.ts +5 -0
  49. package/src/devcontainer.ts +500 -64
  50. package/src/doctor.ts +195 -30
  51. package/src/git-signing.ts +87 -0
  52. package/src/interactive-prompts.ts +692 -0
  53. package/src/list.ts +16 -2
  54. package/src/logging.ts +154 -0
  55. package/src/main.ts +1213 -63
  56. package/src/metadata.ts +52 -3
  57. package/src/package-info.ts +18 -0
  58. package/src/paths.ts +50 -10
  59. package/src/process.ts +80 -2
  60. package/src/progress.ts +593 -0
  61. package/src/purge.ts +208 -0
  62. package/src/shell.ts +19 -0
  63. package/src/ssh-config.ts +134 -16
  64. package/src/ssh-install-targets.ts +111 -0
  65. package/src/ssh-key.ts +53 -13
  66. package/src/status.ts +5 -0
  67. package/dist/main-BuEptwlL.cjs +0 -1707
  68. package/dist/main-ZFTrSVgt.mjs +0 -1685
  69. package/dist/main-ZFTrSVgt.mjs.map +0 -1
package/README.md CHANGED
@@ -28,7 +28,7 @@ npm install -g boxdown
28
28
  You can also run it without installing:
29
29
 
30
30
  ```sh
31
- npx boxdown start
31
+ npx boxdown setup
32
32
  ```
33
33
 
34
34
  ## Usage
@@ -36,22 +36,46 @@ npx boxdown start
36
36
  From any project repository on your host:
37
37
 
38
38
  ```sh
39
- npx boxdown start
39
+ npx boxdown setup
40
40
  ```
41
41
 
42
- Boxdown builds or reuses a Dev Container for the current directory, then opens a shell inside it. The target repository stays clean; Boxdown writes generated configuration and SSH keys under user cache/data directories instead of copying `.devcontainer/` into the project.
42
+ Boxdown builds or reuses a Dev Container for the current directory and installs
43
+ an SSH alias for remote tools. The target repository stays clean; Boxdown writes
44
+ generated configuration and SSH keys under user cache/data directories instead
45
+ of copying `.devcontainer/` into the project.
46
+
47
+ Startup commands print concise progress by default and hide raw Docker,
48
+ Dev Containers, and lifecycle hook logs unless a step fails. Add `--verbose` to
49
+ stream the full command output while setup/start work runs. Boxdown also keeps
50
+ one append-only per-workspace command log under its data directory; `boxdown
51
+ status` shows the exact path. Interactive shell, agent, and tunnel session bytes
52
+ are not tee'd into the log.
53
+
54
+ Open an interactive shell inside the container when you need one:
55
+
56
+ ```sh
57
+ npx boxdown start
58
+ ```
43
59
 
44
60
  Boxdown ships and invokes its own `@devcontainers/cli` dependency. It does not require a host/global Dev Containers CLI install.
45
61
 
46
62
  ### Portless SSH
47
63
 
48
- Install an SSH alias for the current project:
64
+ `boxdown setup` installs an SSH alias for the current project. To only install
65
+ or update that alias without starting the devcontainer, use the lower-level SSH
66
+ command:
49
67
 
50
68
  ```sh
51
- npx boxdown ssh-config install
69
+ npx boxdown ssh install
52
70
  ```
53
71
 
54
- By default this creates a `<repo-name>-devcontainer` SSH host. Validate it with:
72
+ This creates a `<repo-name>-devcontainer` SSH host. When run in an interactive
73
+ terminal, Boxdown also asks whether to install optional targets such as Codex
74
+ and Claude.
75
+ Non-interactive runs skip optional targets and print the explicit `--target`
76
+ form to use in scripts.
77
+
78
+ Validate the SSH alias with:
55
79
 
56
80
  ```sh
57
81
  ssh <repo-name>-devcontainer 'whoami && pwd'
@@ -59,32 +83,95 @@ ssh <repo-name>-devcontainer 'whoami && pwd'
59
83
 
60
84
  Use the same alias in Cursor, Claude, Codex, or any SSH-capable tool.
61
85
 
86
+ To also add the project to Codex's remote project sidebar or Claude's SSH
87
+ remote list, pass one or more targets during setup or select them from the
88
+ lower-level SSH prompt:
89
+
90
+ ```sh
91
+ npx boxdown setup --target codex
92
+ npx boxdown setup --target claude
93
+ ```
94
+
95
+ The lower-level SSH command also supports the same targets for scripts:
96
+
97
+ ```sh
98
+ npx boxdown ssh install --target codex
99
+ npx boxdown ssh install --target claude
100
+ ```
101
+
102
+ Restart the target app after installing it so it applies the updated remote
103
+ project config.
104
+
105
+ From the target project directory, forward a dev server running inside the
106
+ container to your host browser:
107
+
108
+ ```sh
109
+ npx boxdown tunnel --port 3030
110
+ ```
111
+
112
+ If `--port` is omitted in an interactive terminal, Boxdown asks which port or
113
+ port mappings to forward and defaults to the generated devcontainer published
114
+ port when available. Non-interactive runs still require `--port`.
115
+
116
+ This keeps a foreground SSH tunnel open until you press Ctrl-C. The host and
117
+ Codex in-app browser can then open `http://localhost:3030/`. Repeat `--port`
118
+ or use `<local:remote>` mappings when needed:
119
+
120
+ ```sh
121
+ npx boxdown tunnel --port 3030 --port 8080:3031
122
+ ```
123
+
124
+ Use `--workspace <path>` only when running the command from a different
125
+ directory. Repeat it with `down` to remove multiple workspace containers in one
126
+ command. When `down` runs from a directory that is not a known Boxdown
127
+ workspace, interactive terminals show a workspace picker instead.
128
+
129
+ Remove Boxdown's managed SSH host block and matching Codex/Claude app entries
130
+ when you no longer need the alias:
131
+
132
+ ```sh
133
+ npx boxdown ssh uninstall
134
+ ```
135
+
62
136
  ### Commands
63
137
 
64
138
  ```sh
139
+ boxdown setup
65
140
  boxdown start
66
141
  boxdown codex
67
142
  boxdown claude
68
- boxdown cc
69
143
  boxdown opencode
70
144
  boxdown antigravity
71
145
  boxdown list
72
146
  boxdown status
73
147
  boxdown stop
74
148
  boxdown down
149
+ boxdown purge
75
150
  boxdown doctor
76
- boxdown ssh-config install
151
+ boxdown ssh install
152
+ boxdown ssh uninstall
77
153
  boxdown ssh-proxy
154
+ boxdown tunnel --port 3030
78
155
  boxdown refresh-gh-token
79
156
  boxdown refresh-gh-token-running
80
157
  ```
81
158
 
82
159
  `boxdown shell` remains supported as an alias for `boxdown start`, but
83
160
  documentation uses `start` as the canonical command.
161
+ `boxdown cc` remains supported as an alias for `boxdown claude`, but
162
+ documentation uses `claude` as the canonical command.
163
+
164
+ `boxdown setup` begins with a host-readiness preflight before it prompts for
165
+ SSH targets, writes workspace state, or starts Docker work. Required failures
166
+ such as an unavailable Docker daemon stop setup with an actionable summary.
167
+ When a local Docker image is available, the preflight also performs a no-pull,
168
+ no-start bind-mount probe for the workspace and Boxdown-managed mount paths.
169
+ Run `boxdown doctor` directly for the complete diagnostic report; an unavailable
170
+ best-effort mount probe is reported as a warning and does not block setup.
84
171
 
85
- The coding-agent commands start or reuse the devcontainer, refresh the selected
86
- CLI, then launch it directly inside the container. Use `--` to pass arguments to
87
- the agent:
172
+ Container bring-up installs Codex and Claude Code by default. The OpenCode and
173
+ Antigravity commands stay available, but install/update those CLIs only when you
174
+ launch them. Use `--` to pass arguments to the selected agent:
88
175
 
89
176
  ```sh
90
177
  boxdown claude -- --continue
@@ -94,18 +181,54 @@ List Boxdown-known devcontainer environments from any directory:
94
181
 
95
182
  ```sh
96
183
  boxdown list
184
+ boxdown list --details
97
185
  boxdown list --json
186
+ boxdown list --format json
98
187
  ```
99
188
 
189
+ Human `boxdown list` output shows `STATE`, `REPO`, `PATH`, and `CONTAINER`.
190
+ Use `boxdown list --details` when you need full copyable paths and SSH aliases
191
+ in human output. Use `boxdown list --json` or `boxdown list --format json` for
192
+ the same structured inventory.
193
+
100
194
  Shared options:
101
195
 
102
196
  ```sh
103
- --workspace <path> # target project directory, defaults to cwd
197
+ --workspace <path> # target project directory, defaults to cwd; repeatable with down; purge also accepts list values
104
198
  --alias <name> # SSH alias, defaults to <repo-name>-devcontainer
199
+ --target <name> # with setup/ssh install, optional target; repeatable; supported: codex, claude
200
+ --port <port> # tunnel port for `boxdown tunnel`; repeatable
105
201
  --recreate # recreate the devcontainer before starting
106
202
  --json # JSON output for status and list
203
+ --format json # JSON output for status and list; equivalent to --json
204
+ --details # detailed human output for list
205
+ ```
206
+
207
+ Use `boxdown purge` when you want to remove the workspace's Boxdown-managed
208
+ environment residue: the devcontainer, its exact recorded Docker image, managed
209
+ SSH/Codex/Claude entries, command log, and Boxdown cache/data for that
210
+ workspace. It does not delete the local repository directory or files inside it.
211
+ Interactive terminals ask for confirmation before purging.
212
+
213
+ For `purge`, `--workspace` accepts the `PATH` or unambiguous `REPO` value
214
+ shown by `boxdown list`. It also accepts exact `SSH ALIAS` values from
215
+ `boxdown status`, `boxdown list --details`, or JSON list output:
216
+
217
+ ```sh
218
+ boxdown purge
219
+ boxdown purge --workspace my-repo-devcontainer
220
+ boxdown purge --workspace my-repo
221
+ boxdown purge --workspace /path/to/my-repo
107
222
  ```
108
223
 
224
+ When `boxdown purge` runs without `--workspace` from a directory that is not a
225
+ tracked Boxdown workspace, interactive terminals show a multi-select list of all
226
+ tracked workspaces, including missing/stale entries. The focused row highlights
227
+ the state token: `running` is green, `exited` is yellow, and `absent`,
228
+ `missing`, or `unknown` are red. Non-interactive runs fail safely from
229
+ untracked directories; scripts should call `boxdown purge --workspace <value>`
230
+ for each workspace.
231
+
109
232
  ## Contributing
110
233
 
111
234
  Please consult [CONTRIBUTING](./CONTRIBUTING.md) for guidelines on contributing to this project.
@@ -1,11 +1,11 @@
1
1
  # Dev container
2
2
 
3
- Run this project in a **consistent Node.js 24 + TypeScript** environment without installing toolchains on your machine. Dependencies install automatically; your repo is the workspace inside the container.
3
+ Run this project in a **consistent Node.js 24 + TypeScript** environment without installing toolchains on your machine. The container starts from the slim Node 24 Debian image, then adds only the shared tools Boxdown needs. Dependencies install automatically; your repo is the workspace inside the container.
4
4
 
5
5
  ## Why use it?
6
6
 
7
- - **Same stack for everyone** — Node 24, pnpm, and tooling match CI and collaborators.
8
- - **Fast onboarding** — Open the folder in a container; `pnpm install` and local git tweaks run once after create.
7
+ - **Same stack for everyone** — Node 24 plus pinned features provide Git, uv, ripgrep, GitHub CLI, and common shell utilities; Debian Python is installed during post-create.
8
+ - **Fast onboarding** — Open the folder in a container; dependency installation and local git tweaks run once after create. pnpm-based workspaces bootstrap `pnpm@11` when needed.
9
9
  - **Host secrets, container dev** — `ANTHROPIC_API_KEY` and `SNYK_TOKEN` are passed from your Mac/Linux session into the container when set locally (see below).
10
10
  - **Optional CLI workflow** — Use `start.sh` if you prefer a terminal-driven container instead of only the editor.
11
11
  - **Portless SSH workflow** — Install a normal SSH host alias that proxies into the devcontainer without publishing an SSH port.
@@ -14,15 +14,27 @@ Run this project in a **consistent Node.js 24 + TypeScript** environment without
14
14
 
15
15
  | File | Role |
16
16
  | ---- | ---- |
17
- | `devcontainer.json` | Image, mounts (e.g. your `~/.gitconfig`), lifecycle commands, env forwarding. |
17
+ | `devcontainer.json` | Slim Node base image, pinned feature set/order, Boxdown state mounts, lifecycle commands, env forwarding. |
18
18
  | `start.sh` | Brings the dev container up with the Dev Containers CLI, then opens a shell **inside** the container or acts as an SSH `ProxyCommand`. |
19
19
  | `ssh-config-install.sh` | Installs/updates a host SSH config alias for Cursor, Claude, or plain `ssh`. |
20
20
  | `hooks/initialize.sh` | Runs on the host before container create/start; prepares the env file and optional secrets. |
21
- | `hooks/post-create.sh` | Runs once after the container is created — e.g. installs [APM](https://github.com/microsoft/apm) (Agent Package Manager), coding-agent CLIs, and OpenSSH server. |
21
+ | `hooks/post-create.sh` | Runs once after the container is created — e.g. installs OpenSSH server, Debian Python, [APM](https://github.com/microsoft/apm) (Agent Package Manager), and default coding-agent CLIs. |
22
22
  | `hooks/post-start.sh` | Runs on each container start; refreshes runtime state such as SSH host keys and authorized keys. |
23
+ | `utils/git-config-bootstrap.sh` | Container-side Git config copy/sanitization helper used by lifecycle scripts. |
24
+ | `utils/python-bootstrap.sh` | Container-side Debian Python runtime helper used by lifecycle scripts. |
23
25
  | `utils/ssh-bootstrap.sh` | Container-side OpenSSH install/runtime helper used by lifecycle scripts. |
24
26
  | `utils/coding-agent-cli-update.sh` | Shared install/update helper for Codex, OpenCode, Claude Code, and Antigravity CLI. |
25
- | `utils/deps-install.sh` | Dependency installation helper used by `hooks/post-create.sh`. |
27
+ | `utils/deps-install.sh` | Dependency installation helper used by `hooks/post-create.sh`; bootstraps pnpm/yarn when the slim base does not provide them. |
28
+
29
+ ## Base image
30
+
31
+ Boxdown uses `node:24-trixie-slim` as the base image to keep the shared image smaller than the full Dev Containers TypeScript/Node image. The devcontainer then installs the required operating-system tools through pinned Dev Container features. `common-utils` and `git` run first so later features and lifecycle hooks can rely on shell basics, `sudo`, package metadata, Git, and related utilities.
32
+
33
+ Python is installed during `postCreateCommand` from Debian apt packages (`python3`, `python3-venv`, `python3-pip`, and `pipx`). On Debian trixie this currently provides Python 3.13. Boxdown intentionally avoids the Dev Containers Python feature because it added a large Python runtime/dev-tooling layer, including bundled environments for tools such as mypy, black, pylint, pytest, bandit, pipenv, and flake8.
34
+
35
+ uv remains a separate pinned feature. It provides `uv`/`uvx`, but it does not provide the system Python runtime by default.
36
+
37
+ JavaScript package managers are handled at workspace setup time: npm comes from the Node image, pnpm is installed as `pnpm@11` for pnpm projects, and Yarn is enabled through Corepack when needed.
26
38
 
27
39
  ## Usage
28
40
 
@@ -40,7 +52,7 @@ From the **repository root** on your host:
40
52
  bash .devcontainer/start.sh
41
53
  ```
42
54
 
43
- Requires Docker running. The script resolves Boxdown's packaged `@devcontainers/cli` dependency and uses that binary to `up` the workspace and `exec` into `bash`; it does not install or use a host/global Dev Containers CLI package.
55
+ Requires Docker running. The script resolves Boxdown's packaged `@devcontainers/cli` dependency and uses that binary to `up` the workspace and `exec` into `bash`; it does not install or use a host/global Dev Containers CLI package. Startup output is concise by default; pass `--verbose` to stream raw Docker, Dev Containers, and hook logs.
44
56
 
45
57
  ### GitHub CLI auth from host `gh`
46
58
 
@@ -71,10 +83,13 @@ use HTTPS and ask the container's `gh` for credentials during `git fetch`,
71
83
  `git pull`, and `git push`. This is intentionally tied to the explicit refresh
72
84
  command; regular SSH remote connections do not copy GitHub credentials.
73
85
 
74
- If your host `.gitconfig` is mounted into the container, Boxdown adds local
75
- repository settings that neutralize incompatible host-only helpers such as
76
- `/opt/homebrew/bin/gh` and broad rewrites such as
77
- `url.git@github.com:.insteadOf=https://github.com/`.
86
+ Boxdown snapshots your host `.gitconfig` into workspace state before container
87
+ creation, mounts that snapshot read-only, and copies it to a normal writable
88
+ `/home/node/.gitconfig` during `postCreateCommand`. The container copy is then
89
+ sanitized to neutralize incompatible host-only helpers such as
90
+ `/opt/homebrew/bin/gh`, broad rewrites such as
91
+ `url.git@github.com:.insteadOf=https://github.com/`, and host-only signing
92
+ settings.
78
93
 
79
94
  If the container is already running and you only want to refresh its GitHub auth:
80
95
 
@@ -92,7 +107,7 @@ first when you need to repair host GitHub auth.
92
107
  From the **repository root** on your host, install or update the SSH alias:
93
108
 
94
109
  ```bash
95
- bash .devcontainer/start.sh ssh-config install
110
+ bash .devcontainer/start.sh ssh install
96
111
  ```
97
112
 
98
113
  By default, this creates:
@@ -112,7 +127,7 @@ Then use the same host alias in Cursor, Claude, or any other SSH client:
112
127
  - **SSH Host:** `<repo-name>-devcontainer`
113
128
  - **SSH Port:** leave empty
114
129
  - **Identity File:** leave empty if the client reads `~/.ssh/config`; otherwise use `.devcontainer/.ssh/id_ed25519`
115
- - **Remote Folder:** select `<repo-name>` from the default home folder, or enter `/workspaces/<repo-name>`
130
+ - **Remote Folder:** enter `/workspaces/<repo-name>`
116
131
 
117
132
  To customize the alias, run:
118
133
 
@@ -129,7 +144,7 @@ If you are a coding agent running on the host OS, prefer the SSH alias workflow.
129
144
  From the repository root on the host:
130
145
 
131
146
  ```bash
132
- bash .devcontainer/start.sh ssh-config install
147
+ bash .devcontainer/start.sh ssh install
133
148
  ssh <repo-name>-devcontainer 'whoami && pwd'
134
149
  ```
135
150
 
@@ -138,15 +153,16 @@ Use the generated host alias when configuring the agent:
138
153
  - **SSH Host:** `<repo-name>-devcontainer`
139
154
  - **SSH Port:** leave empty
140
155
  - **Identity File:** leave empty if the agent honors `~/.ssh/config`; otherwise use `.devcontainer/.ssh/id_ed25519`
141
- - **Remote Folder:** select `<repo-name>` from the default home folder, or enter `/workspaces/<repo-name>`
156
+ - **Remote Folder:** enter `/workspaces/<repo-name>`
142
157
 
143
158
  `start.sh` has two modes:
144
159
 
145
160
  - `bash .devcontainer/start.sh` starts or reuses the devcontainer and opens an interactive shell. Use this for a local terminal session.
146
- - `bash .devcontainer/start.sh ssh-config install` installs or refreshes the host SSH alias and exits.
161
+ - `bash .devcontainer/start.sh ssh install` installs or refreshes the host SSH alias and exits.
147
162
  - `bash .devcontainer/start.sh --ssh-proxy` refreshes the SSH alias, starts or reuses the devcontainer, and then bridges SSH over `docker exec`. Do not keep this running manually in a terminal; it is meant to be launched by OpenSSH as the `ProxyCommand` in the generated SSH config.
148
163
  - `bash .devcontainer/start.sh --refresh-gh-token` starts or reuses the devcontainer, then refreshes container `gh` auth from host `gh` when a token is available.
149
164
  - `bash .devcontainer/start.sh --refresh-gh-token-running` refreshes container `gh` auth from host `gh` only when the devcontainer is already running.
165
+ - Add `--verbose` to any startup mode when debugging raw devcontainer, Docker, or hook output.
150
166
 
151
167
  If the devcontainer does not exist yet, the first SSH connection through `<repo-name>-devcontainer` will create it with `@devcontainers/cli up`, including `initializeCommand`, features, mounts, `postCreateCommand`, and `postStartCommand`. The first connection may take longer while the container is created.
152
168
 
@@ -169,7 +185,23 @@ It is not a GitHub token and does not authenticate `gh` or GitHub Git remotes.
169
185
  - **Global agent config** — Boxdown automatically mounts host `~/.agents`
170
186
  read-only at `/home/node/.agents` when that directory exists. Recreate the
171
187
  devcontainer after creating or removing it so the mount set is refreshed.
172
- - **Agent config on the host** — Uncomment the `mounts` entries in `devcontainer.json` to bind `~/.claude`, `~/.gemini`, or `~/.codex` into the container so coding agents see your existing settings.
188
+ - **Codex auth on the host** — Boxdown automatically mounts host
189
+ `~/.codex/auth.json` read-only at `/home/node/.codex/auth.json` when that file
190
+ exists. Recreate the devcontainer after creating or removing it so the mount
191
+ set is refreshed.
192
+ - **Other agent config on the host** — Uncomment the `mounts` entries in
193
+ `devcontainer.json` to bind directories such as `~/.claude` or `~/.gemini`
194
+ into the container so coding agents see your existing settings.
195
+ - **Coding-agent defaults** — Container create/start installs or refreshes
196
+ Codex and Claude Code by default. OpenCode and Antigravity CLI remain
197
+ available through `boxdown opencode` and `boxdown antigravity`, but install
198
+ lazily only when those commands are launched.
199
+ - **Agent CLI cleanup** — After a successful coding-agent CLI install/update,
200
+ Boxdown removes stale agent artifacts: old Codex standalone releases, old
201
+ Claude Code versions, OpenCode installer temp directories, and Antigravity
202
+ staging cache. Codex keeps only the active standalone release by default; set
203
+ `BOXDOWN_CODEX_STANDALONE_RELEASES_KEEP_PREVIOUS` to keep extra rollback
204
+ releases.
173
205
  - **1Password / other CLIs** — Follow the commented blocks in `devcontainer.json` and `hooks/post-create.sh` if you need them; keep the image lean by default.
174
206
 
175
207
  ---
@@ -1,17 +1,35 @@
1
1
  {
2
2
  "name": "Project Devcontainer",
3
- "image": "mcr.microsoft.com/devcontainers/typescript-node:24-trixie",
3
+ "image": "node:24-trixie-slim",
4
4
  "features": {
5
- "ghcr.io/devcontainers/features/python@sha256:fbcad6955caeecc5ad3f7886baf652e25cba5225a6c4c2287c536de2e5607511": {
6
- "version": "3.12",
7
- "installPythonDevDependencies": true
5
+ "ghcr.io/devcontainers/features/common-utils@sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a": {
6
+ "username": "node"
8
7
  },
8
+ "ghcr.io/devcontainers/features/git@sha256:a3e43ff91b9f5f6bd2c14bd510d43e5e698f1266dc41027ba4e04e7e45be607a": {
9
+ "version": "os-provided"
10
+ },
11
+ // Removed from the active feature set after measuring roughly 900MB of
12
+ // layer growth. Keep this reference for future Python 3.12/dev-tooling needs.
13
+ // "ghcr.io/devcontainers/features/python@sha256:fbcad6955caeecc5ad3f7886baf652e25cba5225a6c4c2287c536de2e5607511": {
14
+ // "version": "3.12",
15
+ // "installPythonDevDependencies": true
16
+ // },
9
17
  "ghcr.io/devcontainer-community/devcontainer-features/astral.sh-uv@sha256:36f95b90080248a05aba7b1b3cecf8018bb6d7222f6274f03f050b502abd4fe7": {
10
18
  "shellautocompletion": true
11
19
  },
12
20
  "ghcr.io/devcontainers-extra/features/ripgrep@sha256:0dfc0ac16f0d6aa754d006a138fd4cb4a62c245d308306edf8ad1b7a80b8fdf2": {},
13
21
  "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671": {}
14
22
  },
23
+ "overrideFeatureInstallOrder": [
24
+ "ghcr.io/devcontainers/features/common-utils@sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a",
25
+ "ghcr.io/devcontainers/features/git@sha256:a3e43ff91b9f5f6bd2c14bd510d43e5e698f1266dc41027ba4e04e7e45be607a",
26
+ // Python is installed during post-create from Debian apt to avoid the
27
+ // heavy Dev Containers Python feature layer.
28
+ // "ghcr.io/devcontainers/features/python@sha256:fbcad6955caeecc5ad3f7886baf652e25cba5225a6c4c2287c536de2e5607511",
29
+ "ghcr.io/devcontainer-community/devcontainer-features/astral.sh-uv@sha256:36f95b90080248a05aba7b1b3cecf8018bb6d7222f6274f03f050b502abd4fe7",
30
+ "ghcr.io/devcontainers-extra/features/ripgrep@sha256:0dfc0ac16f0d6aa754d006a138fd4cb4a62c245d308306edf8ad1b7a80b8fdf2",
31
+ "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671"
32
+ ],
15
33
  // Ask the editor to forward these container ports (no Docker appPort).
16
34
  "forwardPorts": [8787, 3005, 3001, 3000],
17
35
 
@@ -51,11 +69,10 @@
51
69
  },
52
70
 
53
71
  "mounts": [
54
- "source=${localEnv:HOME}/.gitconfig,target=/home/node/.gitconfig,type=bind,consistency=cached",
55
72
  // mount host user home directories for agents:
56
73
  // "source=${localEnv:HOME}/.claude,target=/home/node/.claude,type=bind,consistency=cached",
57
- // "source=${localEnv:HOME}/.gemini,target=/home/node/.claude,type=bind,consistency=cached",
58
- // "source=${localEnv:HOME}/.codex,target=/home/node/.codex,type=bind,consistency=cached"
74
+ // "source=${localEnv:HOME}/.gemini,target=/home/node/.claude,type=bind,consistency=cached"
75
+ // Codex auth.json is mounted automatically by Boxdown's generated config when present.
59
76
  ],
60
77
 
61
78
  // Initialize optional host-side secrets before container create/start.
@@ -7,19 +7,51 @@ set -euo pipefail
7
7
  HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
8
  WORKSPACE_DIR="${BOXDOWN_WORKSPACE_FOLDER:-$(cd "${HOOKS_DIR}/../.." && pwd)}"
9
9
  ENV_FILE="${WORKSPACE_DIR}/.env.development"
10
+ HOST_GITCONFIG_PATH="${BOXDOWN_HOST_GITCONFIG_PATH:-${HOME:-}/.gitconfig}"
11
+ HOST_GITCONFIG_SNAPSHOT_PATH="${BOXDOWN_HOST_GITCONFIG_SNAPSHOT_PATH:-}"
10
12
  OP_TOKEN_REFERENCE="op://Private/1Password op CLI Service Account for DevContainers/password"
11
13
 
12
14
  main() {
15
+ progress "Preparing workspace environment file"
13
16
  ensure_env_file_exists
17
+ progress "Snapshotting host Git config"
18
+ snapshot_host_gitconfig
19
+ progress "Checking 1Password service account token"
14
20
  maybe_inject_1password_service_account_token
15
21
  }
16
22
 
23
+ progress() {
24
+ if [[ "${BOXDOWN_PROGRESS:-0}" == "1" ]]; then
25
+ printf 'BOXDOWN_PROGRESS: %s\n' "$*"
26
+ fi
27
+ }
28
+
17
29
  ensure_env_file_exists() {
18
30
  if [[ ! -f "${ENV_FILE}" ]]; then
19
31
  : > "${ENV_FILE}"
20
32
  fi
21
33
  }
22
34
 
35
+ snapshot_host_gitconfig() {
36
+ local snapshot_dir
37
+
38
+ if [[ -z "${HOST_GITCONFIG_SNAPSHOT_PATH}" ]]; then
39
+ echo "initialize.sh: host gitconfig snapshot path is not configured; skipping gitconfig snapshot."
40
+ return 0
41
+ fi
42
+
43
+ snapshot_dir="$(dirname "${HOST_GITCONFIG_SNAPSHOT_PATH}")"
44
+ mkdir -p "${snapshot_dir}"
45
+
46
+ if [[ -f "${HOST_GITCONFIG_PATH}" ]]; then
47
+ cp "${HOST_GITCONFIG_PATH}" "${HOST_GITCONFIG_SNAPSHOT_PATH}"
48
+ chmod 0644 "${HOST_GITCONFIG_SNAPSHOT_PATH}"
49
+ return 0
50
+ fi
51
+
52
+ rm -f "${HOST_GITCONFIG_SNAPSHOT_PATH}"
53
+ }
54
+
23
55
  maybe_inject_1password_service_account_token() {
24
56
  local token
25
57
 
@@ -5,27 +5,68 @@ HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
5
  DEVCONTAINER_DIR="$(cd "${HOOKS_DIR}/.." && pwd)"
6
6
 
7
7
  main() {
8
- configure_local_git
9
- install_openssh_server
10
- install_apm
11
- install_or_update_coding_agent_clis
12
- install_1password_cli
13
- install_snyk_cli
14
- run_deps_install
8
+ run_step "Configuring global Git" configure_global_git
9
+ run_step "Configuring Git commit signing" configure_git_signing
10
+ run_step "Configuring workspace Git" configure_local_git
11
+ run_step "Installing OpenSSH server" install_openssh_server
12
+ run_step "Installing Python runtime" install_python_runtime
13
+ run_step "Installing Agent Package Manager" install_apm
14
+ run_step "Installing coding-agent CLIs" install_or_update_coding_agent_clis
15
+ run_step "Installing 1Password CLI" install_1password_cli
16
+ run_step "Installing Snyk CLI" install_snyk_cli
17
+ run_step "Installing workspace dependencies" run_deps_install
18
+ }
19
+
20
+ progress() {
21
+ if [[ "${BOXDOWN_PROGRESS:-0}" == "1" ]]; then
22
+ printf 'BOXDOWN_PROGRESS: %s\n' "$*"
23
+ fi
24
+ }
25
+
26
+ run_step() {
27
+ local label="$1"
28
+ shift
29
+
30
+ progress "$label"
31
+ "$@"
32
+ }
33
+
34
+ configure_global_git() {
35
+ bash "${DEVCONTAINER_DIR}/utils/git-config-bootstrap.sh"
36
+ }
37
+
38
+ configure_git_signing() {
39
+ bash "${DEVCONTAINER_DIR}/utils/git-signing-bootstrap.sh"
15
40
  }
16
41
 
17
42
  configure_local_git() {
18
43
  # Local git prefs only apply inside a repository; skip when there is no .git (avoids postCreate failure).
19
44
  if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
20
- git config --local commit.gpgsign false
21
- git config --local core.pager 'less -R'
22
- git config --local credential.https://github.com.helper '!gh auth git-credential'
45
+ git config --local --replace-all core.pager 'less -R'
46
+ git config --local --unset-all credential.https://github.com.helper >/dev/null 2>&1 || true
47
+ git config --local --add credential.https://github.com.helper ''
48
+ git config --local --add credential.https://github.com.helper '!gh auth git-credential'
23
49
  fi
24
50
  }
25
51
 
26
52
  install_apm() {
27
53
  # Agent Package Manager: https://github.com/microsoft/apm
28
- curl -sSL https://aka.ms/apm-unix | sh
54
+ local installer
55
+
56
+ installer="$(mktemp)"
57
+ if ! curl -fsSL https://aka.ms/apm-unix -o "${installer}"; then
58
+ rm -f "${installer}"
59
+ echo "post-create: warning: could not download APM installer; skipping APM." >&2
60
+ return 0
61
+ fi
62
+
63
+ if ! sh "${installer}"; then
64
+ rm -f "${installer}"
65
+ echo "post-create: warning: APM installer failed; skipping APM." >&2
66
+ return 0
67
+ fi
68
+
69
+ rm -f "${installer}"
29
70
  }
30
71
 
31
72
  install_or_update_coding_agent_clis() {
@@ -37,6 +78,10 @@ install_openssh_server() {
37
78
  bash "${DEVCONTAINER_DIR}/utils/ssh-bootstrap.sh" install
38
79
  }
39
80
 
81
+ install_python_runtime() {
82
+ bash "${DEVCONTAINER_DIR}/utils/python-bootstrap.sh" install
83
+ }
84
+
40
85
  install_snyk_cli() {
41
86
  # https://docs.snyk.io/snyk-cli/install-the-snyk-cli
42
87
  local url
@@ -64,4 +109,6 @@ run_deps_install() {
64
109
  bash "${DEVCONTAINER_DIR}/utils/deps-install.sh"
65
110
  }
66
111
 
67
- main "$@"
112
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
113
+ main "$@"
114
+ fi
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env bash
2
2
  # post-start: runs after each container start (postStartCommand in devcontainer.json).
3
- # Naming matches post-create.sh; extend with more steps as needed (e.g. source scripts from .devcontainer/utils/).
3
+ set -euo pipefail
4
+
5
+ DEVCONTAINER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
+ bash "${DEVCONTAINER_DIR}/utils/git-signing-bootstrap.sh"
4
7
 
5
8
  set -euo pipefail
6
9
 
@@ -8,9 +11,23 @@ HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
11
  DEVCONTAINER_DIR="$(cd "${HOOKS_DIR}/.." && pwd)"
9
12
 
10
13
  main() {
11
- configure_sshd_runtime
12
- refresh_coding_agent_clis
13
- remove_ephemeral_env_file_if_present
14
+ run_step "Preparing SSH runtime" configure_sshd_runtime
15
+ run_step "Refreshing coding-agent CLIs" refresh_coding_agent_clis
16
+ run_step "Cleaning ephemeral environment file" remove_ephemeral_env_file_if_present
17
+ }
18
+
19
+ progress() {
20
+ if [[ "${BOXDOWN_PROGRESS:-0}" == "1" ]]; then
21
+ printf 'BOXDOWN_PROGRESS: %s\n' "$*"
22
+ fi
23
+ }
24
+
25
+ run_step() {
26
+ local label="$1"
27
+ shift
28
+
29
+ progress "$label"
30
+ "$@"
14
31
  }
15
32
 
16
33
  configure_sshd_runtime() {
@@ -89,11 +89,20 @@ write_ssh_config_block() {
89
89
  chmod 0600 "$SSH_CONFIG"
90
90
 
91
91
  tmp_file="$(mktemp)"
92
- awk -v begin="$BEGIN_MARKER" -v end="$END_MARKER" '
92
+ if ! awk -v begin="$BEGIN_MARKER" -v end="$END_MARKER" '
93
93
  $0 == begin { skip = 1; next }
94
- $0 == end { skip = 0; next }
94
+ $0 == end && skip { skip = 0; next }
95
+ skip && $0 ~ /^# (BEGIN|END) [A-Za-z0-9_.-]+ (boxdown )?devcontainer ssh$/ { exit 43 }
95
96
  !skip { print }
96
- ' "$SSH_CONFIG" > "$tmp_file"
97
+ END {
98
+ if (skip) {
99
+ exit 42
100
+ }
101
+ }
102
+ ' "$SSH_CONFIG" > "$tmp_file"; then
103
+ rm -f "$tmp_file"
104
+ die "Refusing to update SSH config for ${HOST_ALIAS}: found \"${BEGIN_MARKER}\" without matching \"${END_MARKER}\" or with an overlapping managed marker. Repair the config manually before running Boxdown again."
105
+ fi
97
106
 
98
107
  start_script_quoted="$(ssh_config_quote "$START_SCRIPT")"
99
108
  key_path_quoted="$(ssh_config_quote "$SSH_KEY_PATH")"