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
@@ -4,10 +4,12 @@ This directory explains Boxdown's user-facing capabilities and the main internal
4
4
  flows behind them.
5
5
 
6
6
  - [Start command](./start-and-shell.md)
7
+ - [Setup command](./setup.md)
7
8
  - [Lifecycle commands](./lifecycle.md)
8
9
  - [SSH config and proxy workflow](./ssh-config-and-proxy.md)
9
10
  - [GitHub auth refresh](./github-auth-refresh.md)
10
11
  - [Generated config and state](./generated-config-and-state.md)
12
+ - [Commit signing](./commit-signing.md)
11
13
 
12
14
  Most features share the same first step: resolve a target workspace, derive
13
15
  workspace-specific state paths, generate a devcontainer override config, and
@@ -0,0 +1,23 @@
1
+ # Commit signing
2
+
3
+ New Boxdown environments attempt SSH commit signing by default. Boxdown forwards
4
+ the host SSH agent, selects a signing identity only when there is one
5
+ unambiguous candidate, and keeps the private key on the host.
6
+
7
+ If the agent is unavailable, has no identities, or has multiple identities
8
+ that Boxdown cannot distinguish, Boxdown warns and configures unsigned commits.
9
+ It never guesses a key and does not block setup or commits.
10
+
11
+ The container can request operations from identities exposed by the forwarded
12
+ agent. Use a dedicated signing identity or an agent that confirms sensitive
13
+ operations when that exposure is unacceptable.
14
+
15
+ GitHub verification is separate from signing. Upload the selected public key as
16
+ a GitHub SSH signing key once to receive the Verified badge:
17
+
18
+ ```bash
19
+ gh ssh-key add /path/to/signing-key.pub --type signing --title "Boxdown commit signing"
20
+ ```
21
+
22
+ The same key may already be registered for GitHub SSH authentication; GitHub
23
+ requires a second registration with type `signing`.
@@ -35,6 +35,21 @@ Boxdown-specific override is not set.
35
35
  SSH private keys live in persistent data. A public-key-only runtime directory is
36
36
  used for the container mount.
37
37
 
38
+ Boxdown-managed lifecycle command output is appended to one per-workspace log
39
+ file:
40
+
41
+ ```text
42
+ ~/.local/share/boxdown/workspaces/<workspace-hash>/boxdown.log
43
+ ```
44
+
45
+ The log is append-only across lifecycle runs. Each run starts a timestamped
46
+ section and records Boxdown-managed command output, Docker/devcontainer child
47
+ process output, and command exit codes. Full interactive shell, agent, and
48
+ tunnel session bytes are not tee'd into the log.
49
+
50
+ The log lives with the workspace's persistent data so `boxdown status` can show
51
+ its path and `boxdown purge` removes it with the rest of the workspace data.
52
+
38
53
  Each touched workspace also records inventory metadata at:
39
54
 
40
55
  ```text
@@ -44,16 +59,53 @@ Each touched workspace also records inventory metadata at:
44
59
  `boxdown list` reads these metadata files as its source of truth, then enriches
45
60
  entries with best-effort Docker state.
46
61
 
62
+ Metadata may also record the last inspected Docker image ID for the workspace so
63
+ `boxdown purge` can remove that exact image even after the container is gone.
64
+
65
+ ## External App Config
66
+
67
+ External app integration config is not Boxdown workspace state. Boxdown writes
68
+ it only when an SSH install target is selected from an interactive
69
+ `boxdown setup` or `boxdown ssh install` prompt, or requested explicitly with
70
+ `boxdown setup --target <name>` or `boxdown ssh install --target <name>`.
71
+ Non-interactive runs without `--target` install only the SSH alias and leave
72
+ external app config unchanged.
73
+
74
+ When requested, Boxdown writes Codex's app config at:
75
+
76
+ ```text
77
+ ~/.codex/codex-app/config.json
78
+ ```
79
+
80
+ `BOXDOWN_CODEX_APP_CONFIG` overrides this path for tests and local development.
81
+ The Codex entry refers to the Boxdown-managed SSH alias and the canonical
82
+ container-side workspace path `/workspaces/<repo-name>`. Codex owns later
83
+ global-state records and sidebar entries it creates from that config, but
84
+ `boxdown ssh uninstall` removes the matching Codex sidebar cache entry when
85
+ unregistering the project.
86
+
87
+ When requested, Boxdown writes Claude's SSH remote config at:
88
+
89
+ ```text
90
+ ~/Library/Application Support/Claude/ssh_configs.json
91
+ ```
92
+
93
+ `BOXDOWN_CLAUDE_SSH_CONFIGS` overrides this path for tests and local
94
+ development. The Claude entry refers to the Boxdown-managed SSH alias and adds
95
+ that alias to Claude's trusted host list.
96
+
47
97
  ## Generated Changes
48
98
 
49
99
  Boxdown starts from `assets/devcontainer/devcontainer.json` and rewrites:
50
100
 
51
101
  - `name`, to include the workspace basename.
52
- - `initializeCommand`, to call the host asset script with the target workspace.
102
+ - `initializeCommand`, to call the host asset script with the target workspace
103
+ and host Git config snapshot paths.
53
104
  - `postCreateCommand`, to call mounted container assets.
54
105
  - `postStartCommand`, to call mounted container assets.
55
- - `mounts`, to add the read-only asset mount, public-key mount, and host
56
- `~/.agents` mount when that directory exists.
106
+ - `mounts`, to add the read-only asset mount, public-key mount, host Git config
107
+ snapshot mount, host `~/.agents` mount when that directory exists, and host
108
+ `~/.codex/auth.json` read-only mount when that file exists.
57
109
  - `containerEnv`, to point SSH bootstrap at the mounted public key and actual
58
110
  container workspace.
59
111
 
@@ -61,4 +113,5 @@ The target repository is still the Dev Container workspace via
61
113
  `--workspace-folder`.
62
114
 
63
115
  Mounts are create-time container settings. Use `boxdown start --recreate` after
64
- creating or removing host `~/.agents` so Docker receives the updated mount set.
116
+ creating or removing host `~/.agents` or `~/.codex/auth.json` so Docker
117
+ receives the updated mount set.
@@ -5,6 +5,7 @@
5
5
  ```sh
6
6
  boxdown refresh-gh-token
7
7
  boxdown refresh-gh-token-running
8
+ boxdown refresh-gh-token --verbose
8
9
  ```
9
10
 
10
11
  Both commands copy host GitHub CLI auth into the container using the host token
@@ -16,6 +17,9 @@ gh auth token
16
17
 
17
18
  They do not start a browser login or device-code flow.
18
19
 
20
+ The startup and refresh work uses concise progress output by default. Pass
21
+ `--verbose` to stream raw devcontainer and auth command output while debugging.
22
+
19
23
  These commands are explicit on purpose. Normal `boxdown start`, coding-agent
20
24
  launches, and SSH proxy connections do not copy GitHub credentials into the
21
25
  container.
@@ -48,6 +52,8 @@ container's authenticated `gh` for `git fetch`, `git pull`, and `git push`.
48
52
  For GitHub remotes, Boxdown:
49
53
 
50
54
  - rewrites fetch and push URLs to `https://github.com/<owner>/<repo>.git`
55
+ - configures container-global GitHub HTTPS credentials through the container's
56
+ `gh` auth store
51
57
  - resets inherited `credential.https://github.com.helper` entries locally
52
58
  - adds `!gh auth git-credential` as the local GitHub credential helper
53
59
  - adds a repository-specific HTTPS self-rewrite so broader host rewrites like
@@ -57,6 +63,12 @@ For GitHub remotes, Boxdown:
57
63
  The local Git config changes are written to the workspace repository because the
58
64
  host checkout is mounted into the devcontainer.
59
65
 
66
+ Separately, Boxdown snapshots the host `.gitconfig` into workspace state and
67
+ copies it to a writable `/home/node/.gitconfig` during container creation. That
68
+ container copy is sanitized so tools cloning from directories such as `/tmp` use
69
+ HTTPS GitHub auth and do not inherit host-only helpers, GitHub SSH rewrites, or
70
+ signing programs that are unavailable inside Linux.
71
+
60
72
  If host `gh` is missing, logged out, or cannot return a token, the refresh is a
61
73
  no-op.
62
74
 
@@ -4,33 +4,48 @@
4
4
 
5
5
  ```sh
6
6
  boxdown list
7
+ boxdown list --details
7
8
  boxdown list --json
9
+ boxdown list --format json
8
10
  boxdown status
9
11
  boxdown status --json
12
+ boxdown status --format json
10
13
  boxdown stop
11
14
  boxdown down
15
+ boxdown purge
12
16
  boxdown doctor
13
17
  ```
14
18
 
15
- Workspace-targeting commands accept `--workspace <path>`. `status` also accepts
16
- `--alias <name>` so its output can match a custom SSH host alias.
19
+ Workspace-targeting commands accept `--workspace <path>`. `down` also accepts
20
+ repeated `--workspace` flags to remove multiple workspace containers in order.
21
+ `purge --workspace` also accepts the `PATH`, `SSH ALIAS`, or unambiguous `REPO`
22
+ value shown by `boxdown list`. `purge` does not accept repeated `--workspace`
23
+ flags. `status` accepts `--alias <name>` so its output can match a custom SSH
24
+ host alias.
17
25
 
18
26
  ## List
19
27
 
20
28
  `list` shows every workspace with Boxdown metadata under the data root,
21
29
  regardless of the directory where the command is run. Human output includes
22
- `STATE`, `REPO`, `PATH`, `SSH ALIAS`, and `CONTAINER` columns.
30
+ `STATE`, `REPO`, `PATH`, and `CONTAINER` columns.
23
31
 
24
- `list --json` prints the same inventory as structured JSON. Docker state is
25
- best-effort: if Docker is unavailable, entries still print and their container
26
- state is `unknown`. If a recorded repository path no longer exists, the entry is
27
- shown as `missing`.
32
+ `list --details` prints one workspace per block with full copyable `path`,
33
+ `ssh alias`, and `container` values.
34
+
35
+ `list --json` and `list --format json` print the same inventory as structured
36
+ JSON. Docker state is best-effort and includes recorded SSH aliases: if Docker
37
+ is unavailable, entries still print and their container state is `unknown`. If a
38
+ recorded repository path no longer exists, the entry is shown as `missing`.
39
+ Container states come from Docker, so active containers usually show `running`,
40
+ stopped containers can show `exited`, and workspaces with no matching container
41
+ show `absent`.
28
42
 
29
43
  ## Status
30
44
 
31
45
  `status` is read-only. It reports the resolved workspace, intended cache/data
32
- paths, generated config path, SSH key paths, SSH alias, and the matching Docker
33
- container state without recording the workspace in Boxdown metadata.
46
+ paths, generated config path, command log path, SSH key paths, SSH alias, and
47
+ the matching Docker container state without recording the workspace in Boxdown
48
+ metadata.
34
49
 
35
50
  Human output distinguishes intended values from detected state. For example, an
36
51
  SSH alias can be shown as a computed default even when the matching Boxdown SSH
@@ -46,7 +61,7 @@ workspace container is running. Missing setup or a stopped/absent container is
46
61
  reported in the output and exits nonzero. Installing an SSH alias is optional
47
62
  and is not required for a healthy status exit.
48
63
 
49
- ## Stop and Down
64
+ ## Stop, Down, and Purge
50
65
 
51
66
  `stop` stops the workspace devcontainer when it is running. If the container is
52
67
  already stopped or absent, it prints a short message and exits 0.
@@ -54,11 +69,82 @@ already stopped or absent, it prints a short message and exits 0.
54
69
  `down` removes the workspace devcontainer with Docker. It does not remove
55
70
  Boxdown cache, generated config, data directories, or SSH keys.
56
71
 
72
+ Lifecycle commands append managed output to the workspace command log under the
73
+ workspace data directory. `--verbose` only controls terminal streaming; it does
74
+ not disable or enable log persistence. Foreground interactive shell, agent, and
75
+ tunnel session bytes are not tee'd into the log.
76
+
77
+ `purge` removes the workspace devcontainer with Docker, force-removes the exact
78
+ Docker image ID Boxdown can inspect or has recorded for the workspace, removes
79
+ Boxdown-managed SSH/Codex/Claude entries for the computed, recorded, and
80
+ provided aliases, and deletes the workspace's Boxdown cache/data directories,
81
+ including the per-workspace command log. It does not delete the target
82
+ repository directory or files inside it.
83
+
84
+ `purge --workspace <value>` first treats `<value>` as a filesystem path. If that
85
+ path does not exist, it looks for an exact `PATH`, then `SSH ALIAS`, then `REPO`
86
+ match in Boxdown metadata. `PATH` and `REPO` are shown by `boxdown list`; use
87
+ `boxdown status`, `boxdown list --details`, or JSON list output for SSH aliases.
88
+ `REPO` must match exactly one workspace; if multiple known workspaces share the
89
+ same repo basename, use `PATH` or `SSH ALIAS`.
90
+
91
+ ```sh
92
+ boxdown purge
93
+ boxdown purge --workspace my-repo-devcontainer
94
+ boxdown purge --workspace my-repo
95
+ boxdown purge --workspace /path/to/my-repo
96
+ ```
97
+
98
+ To remove multiple workspace containers, repeat `--workspace`:
99
+
100
+ ```sh
101
+ boxdown down --workspace /path/to/repo-a --workspace /path/to/repo-b
102
+ ```
103
+
104
+ When `down` runs without `--workspace` from a directory that is not a known
105
+ Boxdown workspace, interactive terminals show a multi-select list of known
106
+ workspaces with existing repository paths. Non-interactive runs keep the
107
+ current-directory behavior.
108
+
109
+ Batch `down` continues after individual workspace failures, but exits nonzero if
110
+ any requested workspace cannot be resolved or removed.
111
+
112
+ When `purge` runs without `--workspace` from a tracked Boxdown workspace, it
113
+ purges that workspace after confirmation. When it runs without `--workspace`
114
+ from an untracked directory, interactive terminals show a multi-select list of
115
+ all tracked workspaces from `boxdown list`, including `running`, `exited`,
116
+ `absent`, `missing`, and `unknown` states. After selection, `purge` asks for one
117
+ destructive confirmation for the selected batch, then purges each selected
118
+ workspace in order. Batch purge continues after individual workspace failures,
119
+ but exits nonzero if any selected workspace fails.
120
+
121
+ In the interactive purge selector, the focused row color-codes only the state
122
+ token: `running` is green, `exited` is yellow, and `absent`, `missing`, or
123
+ `unknown` are red. These colors are terminal UI affordances only; scripts should
124
+ rely on text or JSON state values.
125
+
126
+ Interactive `purge` runs ask for confirmation before removing devcontainer,
127
+ image, SSH/Codex integration, cache, and data state. Non-interactive purge runs
128
+ do not prompt so existing targeted scripts keep working. Non-interactive
129
+ `purge` from an untracked directory fails safely instead of treating the current
130
+ directory as a workspace.
131
+
57
132
  ## Doctor
58
133
 
59
134
  `doctor` validates required host prerequisites: Node, Docker CLI, Docker
60
135
  daemon access, SSH tools, packaged devcontainer assets, and Boxdown's packaged
61
- `@devcontainers/cli` dependency.
136
+ `@devcontainers/cli` dependency. It also uses a local Docker image, when one is
137
+ available, to create and immediately remove disposable containers that verify
138
+ Docker can bind-mount the workspace, Boxdown assets, and Boxdown runtime-state
139
+ paths. The probe never pulls an image or starts container code.
140
+
141
+ Known bind-mount/share failures are errors with the affected host path and
142
+ Docker Desktop remediation guidance. If Docker has no local image or the probe
143
+ cannot complete for another reason, doctor reports a warning instead.
144
+
145
+ `boxdown setup` runs the required doctor checks before it prompts, writes
146
+ workspace metadata, creates SSH keys, generates devcontainer configuration, or
147
+ starts Docker. Required failures stop setup; warnings remain non-blocking.
62
148
 
63
149
  GitHub CLI auth is optional. Missing or unauthenticated `gh` is reported as a
64
150
  warning rather than a failure.
@@ -0,0 +1,66 @@
1
+ # Setup Command
2
+
3
+ ## Command
4
+
5
+ ```sh
6
+ boxdown setup
7
+ boxdown setup --target codex
8
+ boxdown setup --target claude
9
+ boxdown setup --recreate
10
+ ```
11
+
12
+ `setup` prepares the current workspace for remote tools without opening an
13
+ interactive shell. It accepts:
14
+
15
+ ```sh
16
+ --workspace <path>
17
+ --alias <name>
18
+ --recreate
19
+ --target <name>
20
+ --verbose
21
+ ```
22
+
23
+ ## Flow
24
+
25
+ 1. Resolve the workspace to a real absolute path.
26
+ 2. Ensure per-workspace SSH key material exists.
27
+ 3. Generate a Boxdown-owned devcontainer config.
28
+ 4. Run `devcontainer up --workspace-folder <repo> --override-config <config>`.
29
+ 5. Install or update the Boxdown-managed SSH alias.
30
+ 6. Optionally install selected SSH targets such as Codex or Claude.
31
+
32
+ `setup` prints plain progress sections by default. Docker, Dev Containers CLI,
33
+ and lifecycle hook output is captured and only summarized if a command fails.
34
+ Pass `--verbose` to stream the raw build and hook logs to the terminal.
35
+
36
+ Boxdown also appends the managed setup output to the workspace command log at:
37
+
38
+ ```text
39
+ ~/.local/share/boxdown/workspaces/<workspace-hash>/boxdown.log
40
+ ```
41
+
42
+ The log is written regardless of `--verbose`; the flag only changes terminal
43
+ streaming.
44
+
45
+ When `--target codex` is provided, Boxdown writes the Codex app config entry for
46
+ the same alias and container-side project path used by:
47
+
48
+ ```sh
49
+ boxdown ssh install --target codex
50
+ ```
51
+
52
+ When `--target claude` is provided, Boxdown writes the Claude app SSH remote
53
+ entry for the same alias used by:
54
+
55
+ ```sh
56
+ boxdown ssh install --target claude
57
+ ```
58
+
59
+ When no target is provided, Boxdown uses the same optional target prompt as
60
+ `boxdown ssh install`. In non-interactive shells, setup skips target
61
+ registration unless `--target` is provided.
62
+
63
+ `setup` does not open a shell, launch a coding-agent CLI, or keep a tunnel in the
64
+ foreground. Use `boxdown start`, `boxdown codex`, or `boxdown tunnel` for those
65
+ foreground workflows. Those commands log Boxdown-managed startup steps, but do
66
+ not tee full interactive shell, agent, or tunnel session bytes into the log.
@@ -3,12 +3,20 @@
3
3
  ## Commands
4
4
 
5
5
  ```sh
6
- boxdown ssh-config install
6
+ boxdown setup --target codex
7
+ boxdown setup --target claude
8
+ boxdown ssh install
9
+ boxdown ssh install --target codex
10
+ boxdown ssh install --target claude
11
+ boxdown ssh uninstall
7
12
  boxdown ssh-proxy
13
+ boxdown tunnel --port 3030
8
14
  ```
9
15
 
10
- `boxdown ssh-config` is accepted as a convenience shortcut for
11
- `boxdown ssh-config install`, but docs use the explicit install form.
16
+ `boxdown ssh` is accepted as a convenience shortcut for
17
+ `boxdown ssh install`, but docs use the explicit install form.
18
+ `boxdown setup` is the higher-level workflow when you also want to start or
19
+ recreate the devcontainer before installing the SSH alias.
12
20
 
13
21
  `ssh-proxy` is primarily an internal command launched by OpenSSH as a
14
22
  `ProxyCommand`.
@@ -31,11 +39,103 @@ The generated SSH config block includes:
31
39
  The block is wrapped in Boxdown markers so repeated installs replace the managed
32
40
  block instead of duplicating it.
33
41
 
42
+ When `boxdown ssh install` runs without `--target` in an interactive terminal,
43
+ Boxdown asks which optional install targets to update in addition to the SSH
44
+ alias. The menu supports selecting multiple targets or skipping all optional
45
+ targets. In non-interactive contexts, Boxdown skips optional targets instead of
46
+ blocking and prints the explicit `--target` form for scripts.
47
+
48
+ `boxdown ssh uninstall` removes the Boxdown-managed marker block for the
49
+ selected alias, the matching Codex app remote project entry, and the matching
50
+ Codex persisted sidebar state for the workspace. It leaves unrelated OpenSSH
51
+ config entries, unrelated Codex remote projects, generated state, and SSH key
52
+ files in place.
53
+
34
54
  `boxdown status` reports whether that Boxdown-managed block is `installed`,
35
55
  `missing`, or `outdated`. It only recognizes blocks wrapped in Boxdown's marker
36
56
  comments; an unrelated OpenSSH `Host` entry with the same alias is not treated
37
57
  as an installed Boxdown alias.
38
58
 
59
+ ## Codex App Target
60
+
61
+ Selecting Codex from the interactive prompt or running
62
+ `boxdown ssh install --target codex` keeps the normal SSH install flow and also
63
+ writes a Codex app remote project entry for the same alias. `--target` is
64
+ repeatable so future optional SSH install targets can be combined in one
65
+ install command.
66
+
67
+ The Codex app config is written to:
68
+
69
+ ```text
70
+ ~/.codex/codex-app/config.json
71
+ ```
72
+
73
+ `BOXDOWN_CODEX_APP_CONFIG` overrides this path for tests and local development.
74
+
75
+ The generated Codex entry points at the canonical container workspace path:
76
+
77
+ ```json
78
+ {
79
+ "sshAlias": "<repo-name>-devcontainer",
80
+ "projects": [
81
+ {
82
+ "remotePath": "/workspaces/<repo-name>",
83
+ "label": "<repo-name>"
84
+ }
85
+ ]
86
+ }
87
+ ```
88
+
89
+ Boxdown merges by SSH alias and normalized remote path, so repeated installs
90
+ update the existing Codex project instead of duplicating it. If an older
91
+ Boxdown install registered `/home/node/<repo-name>`, the next Codex target
92
+ install migrates that entry to `/workspaces/<repo-name>`. Existing known Codex
93
+ config keys are preserved, but unknown keys are not written back because
94
+ Codex's app config parser is strict.
95
+
96
+ Boxdown normalizes matching Codex sidebar state for the same SSH alias when it
97
+ installs the Codex target. Restart Codex after installing the target so Codex
98
+ applies the app config, discovers the SSH alias from normal OpenSSH config, and
99
+ creates or updates its sidebar project entry.
100
+
101
+ ## Claude App Target
102
+
103
+ Selecting Claude from the interactive prompt or running
104
+ `boxdown ssh install --target claude` keeps the normal SSH install flow and also
105
+ writes a Claude app SSH remote entry for the same alias. `--target` is
106
+ repeatable, so `codex` and `claude` can be installed in one command.
107
+
108
+ The Claude SSH config is written to:
109
+
110
+ ```text
111
+ ~/Library/Application Support/Claude/ssh_configs.json
112
+ ```
113
+
114
+ `BOXDOWN_CLAUDE_SSH_CONFIGS` overrides this path for tests and local
115
+ development.
116
+
117
+ The generated Claude entry mirrors the desktop app's SSH remote shape:
118
+
119
+ ```json
120
+ {
121
+ "configs": [
122
+ {
123
+ "name": "<repo-name>",
124
+ "sshHost": "<repo-name>-devcontainer",
125
+ "id": "<uuid>",
126
+ "source": "desktop"
127
+ }
128
+ ],
129
+ "trustedHosts": [
130
+ "<repo-name>-devcontainer"
131
+ ]
132
+ }
133
+ ```
134
+
135
+ Boxdown merges by `sshHost`, preserves an existing Claude remote ID, and adds
136
+ the alias to `trustedHosts`. Restart Claude after installing the target so the
137
+ app applies the SSH remote entry.
138
+
39
139
  ## Proxy Flow
40
140
 
41
141
  When OpenSSH launches `boxdown ssh-proxy`, Boxdown:
@@ -44,15 +144,136 @@ When OpenSSH launches `boxdown ssh-proxy`, Boxdown:
44
144
  2. Ensures the per-workspace host key exists.
45
145
  3. Reuses a running devcontainer when possible.
46
146
  4. Starts the devcontainer when needed.
47
- 5. Runs a throttled coding-agent CLI update preflight inside the container.
147
+ 5. Runs a throttled default coding-agent CLI update preflight inside the
148
+ container.
48
149
  6. Runs the container SSH bootstrap runtime.
49
150
  7. Bridges OpenSSH to `/usr/sbin/sshd -i` through `docker exec -i`.
50
151
 
51
152
  This does not publish an SSH port. The SSH stream travels through Docker exec.
52
153
 
53
- The coding-agent CLI update preflight covers already-running containers, where
54
- `postStartCommand` does not necessarily run before a new SSH session. Its output
55
- is routed to stderr so stdout remains reserved for SSH traffic.
154
+ Boxdown augments child-process `PATH` with common macOS GUI-missing tool
155
+ locations, including `/usr/local/bin`, Homebrew paths, `~/.docker/bin`, and
156
+ Docker Desktop's bundled CLI directory. This lets GUI apps such as Claude launch
157
+ the SSH proxy through OpenSSH even when their launchd environment cannot
158
+ resolve `docker` by default.
159
+
160
+ The default coding-agent CLI update preflight covers Codex and Claude Code in
161
+ already-running containers, where `postStartCommand` does not necessarily run
162
+ before a new SSH session. OpenCode and Antigravity remain lazy installs through
163
+ their direct `boxdown` commands. The preflight output is routed to stderr so
164
+ stdout remains reserved for SSH traffic.
165
+
166
+ SSH proxy startup uses the same concise progress protocol as `setup` and
167
+ `start`, but routes progress and failure summaries to stderr because stdout is
168
+ reserved for the SSH stream. Use `boxdown ssh-proxy --verbose` only for manual
169
+ debugging.
170
+
171
+ ## Local Web Tunnels
172
+
173
+ The SSH proxy supports TCP forwarding, so Boxdown can expose a web server that is
174
+ listening only inside the devcontainer.
175
+
176
+ From the target project directory:
177
+
178
+ ```sh
179
+ boxdown tunnel --port 3030
180
+ ```
181
+
182
+ When `--port` is omitted in an interactive terminal, Boxdown prompts for one or
183
+ more port mappings. Pressing Enter accepts the generated devcontainer published
184
+ port when Boxdown can read one from the config, currently `3000`. Non-TTY and
185
+ CI runs keep requiring an explicit `--port`.
186
+
187
+ From another directory:
188
+
189
+ ```sh
190
+ boxdown tunnel --workspace /path/to/project --port 3030
191
+ ```
192
+
193
+ This starts or reuses the devcontainer, ensures the SSH alias exists, and then
194
+ keeps a foreground SSH tunnel open:
195
+
196
+ ```text
197
+ 127.0.0.1:3030 -> localhost:3030
198
+ ```
199
+
200
+ While the tunnel is running, host browsers and the Codex in-app browser can open
201
+ `http://localhost:3030/`.
202
+
203
+ Tunnel setup also hides raw devcontainer startup logs by default; pass
204
+ `--verbose` before the foreground tunnel opens when debugging startup.
205
+
206
+ Repeat `--port` for multiple forwards. Use `<local:remote>` when the host port
207
+ should differ from the container port:
208
+
209
+ ```sh
210
+ boxdown tunnel --port 3030 --port 8080:3031
211
+ ```
212
+
213
+ The tunnel targets remote `localhost` rather than `127.0.0.1` so servers that
214
+ bind to the container's IPv6 loopback, such as some Slidev dev sessions, still
215
+ work.
216
+
217
+ Devcontainer `forwardPorts` is an editor hint, not a host listener in the Codex
218
+ desktop app. Docker `runArgs` / `appPort` publishing can also expose services,
219
+ but it requires the server to bind to an externally reachable container address
220
+ such as `0.0.0.0` and may require recreating the container.
221
+
222
+ Codex's own detected-port forwarding may create a random host URL such as
223
+ `http://localhost:52548/`. That is expected, but the generated SSH forward may
224
+ target remote `127.0.0.1:<port>`. If the dev server is only listening on remote
225
+ IPv6 loopback (`[::1]:<port>`), the random forwarded URL can reset even though a
226
+ listener exists. In that case, either use `boxdown tunnel --port <port>`, or
227
+ start the dev server with a host flag such as `--host 0.0.0.0`.
228
+
229
+ ## IPv4 and IPv6 Loopback Options
230
+
231
+ The Codex detected-port issue is not an `/etc/hosts` problem. `/etc/hosts` maps
232
+ names such as `localhost` to addresses, but a generated SSH forward that targets
233
+ remote `127.0.0.1:<port>` uses a numeric IP literal. No name lookup happens, so
234
+ changing the container's `localhost` entry would not make `127.0.0.1` reach a
235
+ server that is listening only on `[::1]`.
236
+
237
+ Potential Boxdown-side approaches:
238
+
239
+ 1. Inject a Node.js IPv4 preference.
240
+
241
+ Boxdown could add this to the generated devcontainer environment:
242
+
243
+ ```sh
244
+ NODE_OPTIONS=--dns-result-order=ipv4first
245
+ ```
246
+
247
+ This would likely make Node-based dev servers such as Vite and Slidev resolve
248
+ `localhost` to `127.0.0.1` first, causing Codex's generated
249
+ `127.0.0.1:<port>` forward to work without app-specific flags.
250
+
251
+ This is the smallest built-in fix to evaluate, but it mainly helps Node.js
252
+ tools. It would not normalize loopback behavior for dev servers written in
253
+ other runtimes.
254
+
255
+ 2. Run a Boxdown-managed loopback bridge inside the container.
256
+
257
+ A background helper could detect a listener on `[::1]:<port>` where
258
+ `127.0.0.1:<port>` is not listening, then bind `127.0.0.1:<port>` and proxy
259
+ traffic to `[::1]:<port>`.
260
+
261
+ This would be runtime-agnostic and would let Codex's generated
262
+ `127.0.0.1:<port>` forwards work for more tools. The tradeoff is a more
263
+ complex container-side service: it would need port discovery, race handling,
264
+ conflict handling, lifecycle cleanup, and clear diagnostics.
265
+
266
+ 3. Keep `boxdown tunnel` as the explicit escape hatch.
267
+
268
+ The current `boxdown tunnel --port <port>` command creates Boxdown's own SSH
269
+ local forward and targets remote `localhost:<port>` rather than remote
270
+ `127.0.0.1:<port>`. That covers dev servers bound to IPv6 loopback,
271
+ IPv4 loopback, or all interfaces, as long as the remote `localhost` name
272
+ resolves to a reachable address for that server.
273
+
274
+ This is reliable and fully under Boxdown's control, but it requires an extra
275
+ foreground command. It is useful as a debugging and fallback path, not a
276
+ fully automatic browser-forwarding experience.
56
277
 
57
278
  ## Key Boundary
58
279