boxdown 1.0.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 (58) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +115 -0
  3. package/assets/devcontainer/README.md +177 -0
  4. package/assets/devcontainer/devcontainer.json +83 -0
  5. package/assets/devcontainer/hooks/initialize.sh +55 -0
  6. package/assets/devcontainer/hooks/post-create.sh +67 -0
  7. package/assets/devcontainer/hooks/post-start.sh +34 -0
  8. package/assets/devcontainer/ssh-config-install.sh +172 -0
  9. package/assets/devcontainer/start.sh +485 -0
  10. package/assets/devcontainer/utils/codex-cli-update.sh +9 -0
  11. package/assets/devcontainer/utils/coding-agent-cli-update.sh +360 -0
  12. package/assets/devcontainer/utils/deps-install.sh +87 -0
  13. package/assets/devcontainer/utils/ssh-bootstrap.sh +200 -0
  14. package/dist/bin/cli.cjs +12 -0
  15. package/dist/bin/cli.d.cts +1 -0
  16. package/dist/bin/cli.d.mts +1 -0
  17. package/dist/bin/cli.mjs +14 -0
  18. package/dist/bin/cli.mjs.map +1 -0
  19. package/dist/main-BuEptwlL.cjs +1707 -0
  20. package/dist/main-ZFTrSVgt.mjs +1685 -0
  21. package/dist/main-ZFTrSVgt.mjs.map +1 -0
  22. package/dist/main.cjs +7 -0
  23. package/dist/main.d.cts +24 -0
  24. package/dist/main.d.cts.map +1 -0
  25. package/dist/main.d.mts +24 -0
  26. package/dist/main.d.mts.map +1 -0
  27. package/dist/main.mjs +3 -0
  28. package/docs/README.md +34 -0
  29. package/docs/architecture.md +75 -0
  30. package/docs/conventions.md +29 -0
  31. package/docs/development.md +59 -0
  32. package/docs/features/README.md +14 -0
  33. package/docs/features/generated-config-and-state.md +64 -0
  34. package/docs/features/github-auth-refresh.md +64 -0
  35. package/docs/features/lifecycle.md +64 -0
  36. package/docs/features/ssh-config-and-proxy.md +60 -0
  37. package/docs/features/start-and-shell.md +83 -0
  38. package/docs/testing.md +63 -0
  39. package/docs/todo.md +170 -0
  40. package/package.json +128 -0
  41. package/src/bin/cli.ts +9 -0
  42. package/src/coding-agents.ts +22 -0
  43. package/src/config.ts +81 -0
  44. package/src/constants.ts +9 -0
  45. package/src/devcontainer-cli.ts +57 -0
  46. package/src/devcontainer.ts +394 -0
  47. package/src/doctor.ts +139 -0
  48. package/src/github-git-auth.ts +143 -0
  49. package/src/jsonc.ts +123 -0
  50. package/src/list.ts +102 -0
  51. package/src/main.ts +362 -0
  52. package/src/metadata.ts +84 -0
  53. package/src/paths.ts +117 -0
  54. package/src/process.ts +94 -0
  55. package/src/shell.ts +60 -0
  56. package/src/ssh-config.ts +113 -0
  57. package/src/ssh-key.ts +52 -0
  58. package/src/status.ts +327 -0
@@ -0,0 +1,485 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # @file start.sh
4
+ # @summary Start the Dev Container for this workspace and attach an interactive shell or SSH proxy.
5
+ #
6
+ # Uses Boxdown's packaged @devcontainers/cli to run `devcontainer up` then either
7
+ # `devcontainer exec bash` or a portless SSH ProxyCommand backed by
8
+ # `docker exec ... sshd -i`.
9
+ # Host port publishing comes from **runArgs** in devcontainer.json (e.g. `-p` and
10
+ # `127.0.0.1::<containerPort>` for dynamic host binding). After `devcontainer up`, this
11
+ # script reads that container port from runArgs and prints the mapped URL via `docker port`
12
+ # in shell mode.
13
+ #
14
+ # @usage
15
+ # .devcontainer/start.sh [command] [options]
16
+ #
17
+ # @options
18
+ # --shell Start the dev container and attach an interactive shell (default).
19
+ # --ssh-proxy Start/reuse the dev container and proxy SSH over docker exec.
20
+ # ssh-config install
21
+ # Install/update the host SSH config alias and exit.
22
+ # --refresh-gh-token
23
+ # Start/reuse the dev container, then refresh container GitHub CLI
24
+ # auth from the host gh token when available.
25
+ # --refresh-gh-token-running
26
+ # Refresh container GitHub CLI auth from the host gh token, but only
27
+ # when the dev container is already running.
28
+ # --recreate Remove the existing dev container for this workspace before `up`, so
29
+ # changes to runArgs (or other create-time settings) take effect.
30
+ # --help, -h Print usage and exit.
31
+ #
32
+ # @example
33
+ # .devcontainer/start.sh
34
+ # .devcontainer/start.sh ssh-config install
35
+ # .devcontainer/start.sh --ssh-proxy
36
+ # .devcontainer/start.sh --refresh-gh-token
37
+ # .devcontainer/start.sh --refresh-gh-token-running
38
+ # .devcontainer/start.sh --recreate
39
+ #
40
+
41
+ set -euo pipefail
42
+
43
+ # GUI apps launched outside an interactive shell often do not inherit the user's
44
+ # shell PATH. Include the common macOS/Linux locations needed by Docker and Node.
45
+ export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${PATH:-}"
46
+
47
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
48
+ WORKSPACE_FOLDER="$(cd "$SCRIPT_DIR/.." && pwd -P)"
49
+ REPO_NAME="$(basename "$WORKSPACE_FOLDER")"
50
+ SSH_KEY_DIR="${DEVCONTAINER_SSH_KEY_DIR:-${SCRIPT_DIR}/.ssh}"
51
+ SSH_KEY_PATH="${DEVCONTAINER_SSH_KEY_PATH:-${SSH_KEY_DIR}/id_ed25519}"
52
+ MODE="shell"
53
+ RECREATE=false
54
+ CONTAINER_ID=""
55
+
56
+ usage() {
57
+ cat <<EOF
58
+ Usage: $(basename "$0") [command] [options]
59
+
60
+ Start the dev container for:
61
+ $WORKSPACE_FOLDER
62
+
63
+ By default, open an interactive bash session inside the container.
64
+
65
+ Options:
66
+ --shell Start the dev container and open an interactive shell (default).
67
+ --ssh-proxy Start/reuse the dev container and proxy SSH over docker exec.
68
+ Intended for SSH ProxyCommand; stdout is reserved for SSH traffic.
69
+ --refresh-gh-token
70
+ Start/reuse the dev container, then refresh container GitHub CLI
71
+ auth from the host gh token when available.
72
+ --refresh-gh-token-running
73
+ Refresh container GitHub CLI auth from the host gh token, but only
74
+ when the dev container is already running.
75
+ --recreate Remove the existing dev container for this workspace before starting,
76
+ so Docker picks up new settings (e.g. runArgs / port mappings).
77
+ --help, -h Show this help and exit.
78
+
79
+ Commands:
80
+ ssh-config install
81
+ Install/update the host SSH config alias and exit.
82
+
83
+ Notes:
84
+ Port publishing uses runArgs (Docker -p ...::<containerPort>). The script greps that
85
+ container port from devcontainer.json for the post-up URL hint.
86
+
87
+ The --ssh-proxy mode uses a repo-local identity at:
88
+ $SSH_KEY_PATH
89
+
90
+ The GitHub token refresh modes never start a host gh login flow. If host gh is
91
+ missing, logged out, or cannot provide a token, refresh is skipped.
92
+ EOF
93
+ }
94
+
95
+ log() {
96
+ if [ "$MODE" = "ssh-proxy" ]; then
97
+ printf '%s\n' "$*" >&2
98
+ else
99
+ printf '%s\n' "$*"
100
+ fi
101
+ }
102
+
103
+ die() {
104
+ printf '%s\n' "$*" >&2
105
+ exit 1
106
+ }
107
+
108
+ devcontainer_cli() {
109
+ local cli_bin
110
+ local package_root
111
+
112
+ package_root="$(cd "$SCRIPT_DIR/../.." && pwd -P)"
113
+ cli_bin="$(
114
+ PACKAGE_ROOT="$package_root" node -e '
115
+ const { existsSync, readFileSync } = require("node:fs");
116
+ const { createRequire } = require("node:module");
117
+ const { dirname, resolve } = require("node:path");
118
+
119
+ const packageRoot = process.env.PACKAGE_ROOT;
120
+ const requireFromBoxdown = createRequire(`${packageRoot}/package.json`);
121
+ const packageJsonPath = requireFromBoxdown.resolve("@devcontainers/cli/package.json");
122
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
123
+ const bin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.devcontainer;
124
+
125
+ if (!bin) process.exit(1);
126
+
127
+ const cliPath = resolve(dirname(packageJsonPath), bin);
128
+ if (!existsSync(cliPath)) process.exit(1);
129
+
130
+ process.stdout.write(cliPath);
131
+ '
132
+ )" || die "Boxdown's packaged @devcontainers/cli dependency is missing. Reinstall boxdown."
133
+
134
+ node "$cli_bin" "$@"
135
+ }
136
+
137
+ # Container-side TCP port published via runArgs, e.g. "127.0.0.1::3000" (Docker -p host::ctr).
138
+ publish_container_port_from_devcontainer_json() {
139
+ local f="$SCRIPT_DIR/devcontainer.json"
140
+ if [ ! -r "$f" ]; then
141
+ return 1
142
+ fi
143
+ sed 's|//.*||' "$f" | sed -nE 's/.*"[0-9.]+::([0-9]+)".*/\1/p' | sed -n '1p'
144
+ }
145
+
146
+ parse_container_id_from_up_output() {
147
+ sed -nE 's/.*"containerId"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' | sed -n '1p'
148
+ }
149
+
150
+ find_running_container_id() {
151
+ docker ps --filter "label=devcontainer.local_folder=${WORKSPACE_FOLDER}" --format '{{.ID}}' | sed -n '1p'
152
+ }
153
+
154
+ ensure_host_ssh_key() {
155
+ if ! command -v ssh-keygen >/dev/null 2>&1; then
156
+ die "ssh-keygen is required for devcontainer SSH setup."
157
+ fi
158
+
159
+ mkdir -p "$SSH_KEY_DIR"
160
+ chmod 0700 "$SSH_KEY_DIR"
161
+
162
+ if [ ! -f "$SSH_KEY_PATH" ]; then
163
+ log "Generating devcontainer SSH identity: $SSH_KEY_PATH"
164
+ ssh-keygen -t ed25519 -f "$SSH_KEY_PATH" -N "" -C "${REPO_NAME}-devcontainer" >/dev/null
165
+ fi
166
+
167
+ if [ ! -f "${SSH_KEY_PATH}.pub" ]; then
168
+ ssh-keygen -y -f "$SSH_KEY_PATH" > "${SSH_KEY_PATH}.pub"
169
+ fi
170
+
171
+ chmod 0600 "$SSH_KEY_PATH"
172
+ chmod 0644 "${SSH_KEY_PATH}.pub"
173
+ }
174
+
175
+ install_ssh_config_alias() {
176
+ if [ ! -r "${SCRIPT_DIR}/ssh-config-install.sh" ]; then
177
+ die "Missing SSH config installer: ${SCRIPT_DIR}/ssh-config-install.sh"
178
+ fi
179
+
180
+ DEVCONTAINER_SSH_HOST_ALIAS="${DEVCONTAINER_SSH_HOST_ALIAS:-${REPO_NAME}-devcontainer}" \
181
+ bash "${SCRIPT_DIR}/ssh-config-install.sh" "$@"
182
+ }
183
+
184
+ start_devcontainer() {
185
+ local up_output
186
+ local up_args
187
+
188
+ if [ "$MODE" = "ssh-proxy" ] && [ "$RECREATE" = false ]; then
189
+ CONTAINER_ID="$(find_running_container_id)"
190
+ if [ -n "$CONTAINER_ID" ]; then
191
+ log "Using running devcontainer for: $WORKSPACE_FOLDER"
192
+ return 0
193
+ fi
194
+ fi
195
+
196
+ log "Starting devcontainer for: $WORKSPACE_FOLDER"
197
+
198
+ up_args=(--workspace-folder "$WORKSPACE_FOLDER")
199
+ if [ "$RECREATE" = true ]; then
200
+ up_args+=(--remove-existing-container)
201
+ log "Removing existing dev container so create-time settings (e.g. runArgs) apply."
202
+ fi
203
+
204
+ if [ "$MODE" = "ssh-proxy" ]; then
205
+ if ! up_output="$(devcontainer_cli up "${up_args[@]}" </dev/null 2>&1)"; then
206
+ [ -z "$up_output" ] || log "$up_output"
207
+ return 1
208
+ fi
209
+ elif ! up_output="$(devcontainer_cli up "${up_args[@]}" 2>&1)"; then
210
+ [ -z "$up_output" ] || log "$up_output"
211
+ return 1
212
+ fi
213
+
214
+ log "$up_output"
215
+
216
+ CONTAINER_ID="$(printf '%s\n' "$up_output" | parse_container_id_from_up_output)"
217
+ if [ -z "$CONTAINER_ID" ]; then
218
+ CONTAINER_ID="$(find_running_container_id)"
219
+ fi
220
+
221
+ if [ -z "$CONTAINER_ID" ]; then
222
+ die "Could not resolve devcontainer ID for: $WORKSPACE_FOLDER"
223
+ fi
224
+ }
225
+
226
+ host_gh_token_or_empty() {
227
+ local token
228
+
229
+ if ! command -v gh >/dev/null 2>&1; then
230
+ return 0
231
+ fi
232
+
233
+ if ! token="$(gh auth token 2>/dev/null)"; then
234
+ return 0
235
+ fi
236
+
237
+ if [ -z "$token" ]; then
238
+ return 0
239
+ fi
240
+
241
+ printf '%s\n' "$token"
242
+ }
243
+
244
+ refresh_container_gh_auth() {
245
+ local token
246
+
247
+ token="$(host_gh_token_or_empty)"
248
+ if [ -z "$token" ]; then
249
+ return 0
250
+ fi
251
+
252
+ if ! printf '%s\n' "$token" | devcontainer_cli exec \
253
+ --workspace-folder "$WORKSPACE_FOLDER" \
254
+ -- gh auth login --hostname github.com --git-protocol https --with-token --insecure-storage >/dev/null 2>&1; then
255
+ log "Warning: could not refresh GitHub CLI auth inside the devcontainer."
256
+ return 0
257
+ fi
258
+
259
+ if ! configure_container_github_git_auth; then
260
+ log "Warning: GitHub CLI auth refreshed, but GitHub Git auth was not configured for this workspace."
261
+ fi
262
+
263
+ if devcontainer_cli exec \
264
+ --workspace-folder "$WORKSPACE_FOLDER" \
265
+ -- gh auth status --hostname github.com >/dev/null 2>&1; then
266
+ log "GitHub CLI auth refreshed inside the devcontainer."
267
+ else
268
+ log "Warning: GitHub CLI auth refresh completed, but verification failed."
269
+ fi
270
+ }
271
+
272
+ configure_container_github_git_auth() {
273
+ devcontainer_cli exec \
274
+ --workspace-folder "$WORKSPACE_FOLDER" \
275
+ -- bash -s >/dev/null 2>&1 <<'EOF'
276
+ set -euo pipefail
277
+
278
+ canonical_github_remote_url() {
279
+ local remote_url="$1"
280
+ local owner repo rest
281
+
282
+ case "$remote_url" in
283
+ git@github.com:*)
284
+ rest="${remote_url#git@github.com:}"
285
+ owner="${rest%%/*}"
286
+ repo="${rest#*/}"
287
+ ;;
288
+ ssh://git@github.com/*)
289
+ rest="${remote_url#ssh://git@github.com/}"
290
+ owner="${rest%%/*}"
291
+ repo="${rest#*/}"
292
+ ;;
293
+ https://github.com/*)
294
+ rest="${remote_url#https://github.com/}"
295
+ owner="${rest%%/*}"
296
+ repo="${rest#*/}"
297
+ ;;
298
+ https://*@github.com/*)
299
+ rest="${remote_url#https://*@github.com/}"
300
+ owner="${rest%%/*}"
301
+ repo="${rest#*/}"
302
+ ;;
303
+ *)
304
+ return 1
305
+ ;;
306
+ esac
307
+
308
+ repo="${repo%/}"
309
+ repo="${repo%.git}"
310
+
311
+ if [ -z "$owner" ] || [ -z "$repo" ] || [ "$owner" = "$repo" ] || [[ "$repo" == */* ]]; then
312
+ return 1
313
+ fi
314
+
315
+ printf 'https://github.com/%s/%s.git\n' "$owner" "$repo"
316
+ }
317
+
318
+ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
319
+ exit 0
320
+ fi
321
+
322
+ remote_lines="$(git config --local --get-regexp '^remote\..*\.url$' || true)"
323
+ if [ -z "$remote_lines" ]; then
324
+ exit 0
325
+ fi
326
+
327
+ configured_urls=()
328
+ while IFS= read -r line; do
329
+ key="${line%% *}"
330
+ raw_url="${line#* }"
331
+ remote_name="${key#remote.}"
332
+ remote_name="${remote_name%.url}"
333
+
334
+ if canonical_url="$(canonical_github_remote_url "$raw_url")"; then
335
+ git remote set-url "$remote_name" "$canonical_url"
336
+ git remote set-url --push "$remote_name" "$canonical_url"
337
+ configured_urls+=("$canonical_url")
338
+ fi
339
+ done <<< "$remote_lines"
340
+
341
+ if [ "${#configured_urls[@]}" -eq 0 ]; then
342
+ exit 0
343
+ fi
344
+
345
+ git config --local --unset-all credential.https://github.com.helper >/dev/null 2>&1 || true
346
+ git config --local --add credential.https://github.com.helper ''
347
+ git config --local --add credential.https://github.com.helper '!gh auth git-credential'
348
+
349
+ for canonical_url in "${configured_urls[@]}"; do
350
+ git config --local --replace-all "url.${canonical_url}.insteadOf" "$canonical_url"
351
+ done
352
+ EOF
353
+ }
354
+
355
+ run_refresh_gh_token() {
356
+ start_devcontainer
357
+ refresh_container_gh_auth
358
+ }
359
+
360
+ run_refresh_gh_token_running() {
361
+ CONTAINER_ID="$(find_running_container_id)"
362
+ if [ -z "$CONTAINER_ID" ]; then
363
+ die "No running devcontainer found for: $WORKSPACE_FOLDER"
364
+ fi
365
+
366
+ refresh_container_gh_auth
367
+ }
368
+
369
+ print_port_hint() {
370
+ local container_port
371
+ local host_binding
372
+
373
+ container_port="$(publish_container_port_from_devcontainer_json || true)"
374
+ if [ -z "$container_port" ]; then
375
+ echo "Warning: could not find a runArgs publish port (expected a quoted string like \"127.0.0.1::<port>\")." >&2
376
+ echo " Skipping docker port URL hint." >&2
377
+ return 0
378
+ fi
379
+
380
+ host_binding="$(docker port "$CONTAINER_ID" "${container_port}/tcp" 2>/dev/null | sed -n '1p')" || true
381
+ if [ -n "$host_binding" ]; then
382
+ printf '\nDev server available at: http://%s\n\n' "$host_binding"
383
+ else
384
+ echo "Warning: container is running but port ${container_port}/tcp is not mapped." >&2
385
+ fi
386
+ }
387
+
388
+ open_shell() {
389
+ echo "Dropping into container shell..."
390
+ # Resolve TERM to something the container's terminfo knows about.
391
+ # Terminals like kitty, ghostty, alacritty set custom TERM values the container won't have.
392
+ # Fall back to xterm-256color (truecolor still works via COLORTERM=truecolor).
393
+ if ! infocmp "${TERM:-xterm-256color}" &>/dev/null 2>&1; then
394
+ TERM=xterm-256color
395
+ fi
396
+ TERM="${TERM:-xterm-256color}" devcontainer_cli exec --workspace-folder "$WORKSPACE_FOLDER" -- env TERM="${TERM:-xterm-256color}" COLORTERM=truecolor bash
397
+ }
398
+
399
+ ensure_container_sshd_runtime() {
400
+ devcontainer_cli exec --workspace-folder "$WORKSPACE_FOLDER" -- bash .devcontainer/utils/ssh-bootstrap.sh runtime </dev/null >&2
401
+ }
402
+
403
+ run_ssh_proxy() {
404
+ install_ssh_config_alias --quiet >&2
405
+ ensure_host_ssh_key
406
+ start_devcontainer
407
+ ensure_container_sshd_runtime
408
+
409
+ exec docker exec -i "$CONTAINER_ID" /usr/sbin/sshd -i \
410
+ -o LogLevel=QUIET \
411
+ -o PubkeyAuthentication=yes \
412
+ -o PasswordAuthentication=no \
413
+ -o KbdInteractiveAuthentication=no \
414
+ -o AllowTcpForwarding=yes \
415
+ -o AllowStreamLocalForwarding=yes \
416
+ -o PermitTTY=yes
417
+ }
418
+
419
+ run_shell() {
420
+ start_devcontainer
421
+ print_port_hint
422
+ open_shell
423
+ }
424
+
425
+ while [ $# -gt 0 ]; do
426
+ case "$1" in
427
+ --help | -h)
428
+ usage
429
+ exit 0
430
+ ;;
431
+ --shell)
432
+ MODE="shell"
433
+ shift
434
+ ;;
435
+ --ssh-proxy)
436
+ MODE="ssh-proxy"
437
+ shift
438
+ ;;
439
+ ssh-config)
440
+ MODE="ssh-config-install"
441
+ if [ "${2:-}" = "install" ]; then
442
+ shift 2
443
+ elif [ "$#" -eq 1 ] || [ "${2#-}" != "$2" ]; then
444
+ shift
445
+ else
446
+ die "Unknown command: ssh-config ${2}"
447
+ fi
448
+ ;;
449
+ --refresh-gh-token)
450
+ MODE="refresh-gh-token"
451
+ shift
452
+ ;;
453
+ --refresh-gh-token-running)
454
+ MODE="refresh-gh-token-running"
455
+ shift
456
+ ;;
457
+ --recreate)
458
+ RECREATE=true
459
+ shift
460
+ ;;
461
+ *)
462
+ echo "Unknown option: $1" >&2
463
+ echo "Run with --help for usage." >&2
464
+ exit 1
465
+ ;;
466
+ esac
467
+ done
468
+
469
+ case "$MODE" in
470
+ shell)
471
+ run_shell
472
+ ;;
473
+ ssh-proxy)
474
+ run_ssh_proxy
475
+ ;;
476
+ ssh-config-install)
477
+ install_ssh_config_alias
478
+ ;;
479
+ refresh-gh-token)
480
+ run_refresh_gh_token
481
+ ;;
482
+ refresh-gh-token-running)
483
+ run_refresh_gh_token_running
484
+ ;;
485
+ esac
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Compatibility wrapper for the shared coding-agent CLI updater.
4
+
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+
9
+ exec bash "${SCRIPT_DIR}/coding-agent-cli-update.sh" "${1:-maybe-update}" codex