boxdown 1.0.0 → 1.2.1

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 (76) hide show
  1. package/README.md +135 -12
  2. package/assets/devcontainer/README.md +65 -21
  3. package/assets/devcontainer/devcontainer.json +27 -15
  4. package/assets/devcontainer/hooks/initialize.sh +76 -22
  5. package/assets/devcontainer/hooks/post-create.sh +70 -12
  6. package/assets/devcontainer/hooks/post-start.sh +20 -13
  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 +109 -0
  13. package/assets/devcontainer/utils/python-bootstrap.sh +69 -0
  14. package/assets/devcontainer/utils/secret-env-bootstrap.sh +22 -0
  15. package/assets/devcontainer/utils/ssh-agent-proxy-bootstrap.sh +27 -0
  16. package/assets/devcontainer/utils/ssh-agent-proxy.mjs +39 -0
  17. package/assets/devcontainer/utils/ssh-bootstrap.sh +9 -0
  18. package/dist/bin/cli.cjs +1 -1
  19. package/dist/bin/cli.mjs +1 -1
  20. package/dist/main-BDgyf2t5.cjs +5758 -0
  21. package/dist/main-J4_2Up3o.mjs +5718 -0
  22. package/dist/main-J4_2Up3o.mjs.map +1 -0
  23. package/dist/main.cjs +5 -2
  24. package/dist/main.d.cts +501 -4
  25. package/dist/main.d.cts.map +1 -1
  26. package/dist/main.d.mts +501 -4
  27. package/dist/main.d.mts.map +1 -1
  28. package/dist/main.mjs +2 -2
  29. package/docs/README.md +1 -0
  30. package/docs/architecture.md +32 -0
  31. package/docs/development.md +13 -6
  32. package/docs/features/README.md +2 -0
  33. package/docs/features/commit-signing.md +94 -0
  34. package/docs/features/generated-config-and-state.md +73 -5
  35. package/docs/features/github-auth-refresh.md +15 -2
  36. package/docs/features/lifecycle.md +103 -11
  37. package/docs/features/setup.md +66 -0
  38. package/docs/features/ssh-config-and-proxy.md +228 -7
  39. package/docs/features/start-and-shell.md +45 -5
  40. package/docs/superpowers/plans/2026-07-11-default-commit-signing.md +110 -0
  41. package/docs/superpowers/plans/2026-07-11-progress-ci-regression.md +125 -0
  42. package/docs/superpowers/plans/2026-07-17-node-ssh-agent-proxy-signing.md +132 -0
  43. package/docs/superpowers/plans/2026-07-17-runtime-secret-environment.md +174 -0
  44. package/docs/superpowers/specs/2026-07-11-default-commit-signing-design.md +416 -0
  45. package/docs/superpowers/specs/2026-07-11-progress-ci-regression-design.md +43 -0
  46. package/docs/superpowers/specs/2026-07-17-node-ssh-agent-proxy-design.md +63 -0
  47. package/docs/superpowers/specs/2026-07-17-runtime-secret-environment-design.md +194 -0
  48. package/docs/testing.md +35 -2
  49. package/docs/todo.md +2 -2
  50. package/package.json +1 -1
  51. package/src/claude-app-config.ts +304 -0
  52. package/src/cli-style.ts +43 -0
  53. package/src/codex-app-config.ts +656 -0
  54. package/src/config.ts +80 -10
  55. package/src/constants.ts +12 -0
  56. package/src/devcontainer.ts +511 -64
  57. package/src/doctor.ts +292 -30
  58. package/src/git-signing.ts +267 -0
  59. package/src/interactive-prompts.ts +692 -0
  60. package/src/list.ts +16 -2
  61. package/src/logging.ts +164 -0
  62. package/src/main.ts +1214 -63
  63. package/src/metadata.ts +52 -3
  64. package/src/package-info.ts +18 -0
  65. package/src/paths.ts +71 -11
  66. package/src/process.ts +80 -2
  67. package/src/progress.ts +593 -0
  68. package/src/purge.ts +216 -0
  69. package/src/shell.ts +25 -0
  70. package/src/ssh-config.ts +134 -16
  71. package/src/ssh-install-targets.ts +111 -0
  72. package/src/ssh-key.ts +53 -13
  73. package/src/status.ts +5 -0
  74. package/dist/main-BuEptwlL.cjs +0 -1707
  75. package/dist/main-ZFTrSVgt.mjs +0 -1685
  76. package/dist/main-ZFTrSVgt.mjs.map +0 -1
@@ -4,7 +4,7 @@
4
4
 
5
5
  set -euo pipefail
6
6
 
7
- DEFAULT_AGENTS=(codex opencode claude antigravity)
7
+ DEFAULT_AGENTS=(codex claude)
8
8
  UPDATE_INTERVAL_SECONDS="${BOXDOWN_CODING_AGENT_UPDATE_INTERVAL_SECONDS:-3600}"
9
9
  UPDATE_LOCK_WAIT_SECONDS="${BOXDOWN_CODING_AGENT_UPDATE_LOCK_WAIT_SECONDS:-120}"
10
10
  STATE_DIR="${BOXDOWN_CODING_AGENT_UPDATE_STATE_DIR:-${HOME}/.cache/boxdown/coding-agent-clis}"
@@ -13,9 +13,23 @@ log() {
13
13
  echo "coding-agent-cli-update: $*" >&2
14
14
  }
15
15
 
16
+ progress() {
17
+ if [ "${BOXDOWN_PROGRESS:-0}" = "1" ]; then
18
+ printf 'BOXDOWN_PROGRESS: %s\n' "$*"
19
+ fi
20
+ }
21
+
22
+ prepend_agent_bin_paths() {
23
+ local codex_home="${CODEX_HOME:-${HOME}/.codex}"
24
+
25
+ PATH="${HOME}/.local/bin:${HOME}/.opencode/bin:${codex_home}/packages/standalone/current/bin:${PATH}"
26
+ export PATH
27
+ }
28
+
16
29
  usage() {
17
30
  cat >&2 <<'EOF'
18
31
  Usage: coding-agent-cli-update.sh <install|update-now|maybe-update> [codex|opencode|claude|antigravity...]
32
+ coding-agent-cli-update.sh ensure <codex|opencode|claude|antigravity...>
19
33
  EOF
20
34
  }
21
35
 
@@ -50,6 +64,27 @@ agent_env_prefix() {
50
64
  esac
51
65
  }
52
66
 
67
+ agent_binary() {
68
+ case "$1" in
69
+ codex) printf '%s\n' codex ;;
70
+ opencode) printf '%s\n' opencode ;;
71
+ claude) printf '%s\n' claude ;;
72
+ antigravity) printf '%s\n' agy ;;
73
+ *)
74
+ log "unknown coding-agent CLI: $1"
75
+ return 1
76
+ ;;
77
+ esac
78
+ }
79
+
80
+ agent_binary_available() {
81
+ local agent="$1"
82
+ local binary
83
+
84
+ binary="$(agent_binary "${agent}")" || return 1
85
+ command -v "${binary}" >/dev/null 2>&1
86
+ }
87
+
53
88
  agent_stamp_file() {
54
89
  local agent="$1"
55
90
  local prefix
@@ -177,6 +212,158 @@ run_installer_url() {
177
212
  curl -fsSL "${url}" | "$@"
178
213
  }
179
214
 
215
+ run_as_root() {
216
+ if [ "$(id -u)" -eq 0 ]; then
217
+ "$@"
218
+ elif command -v sudo >/dev/null 2>&1; then
219
+ sudo "$@"
220
+ else
221
+ "$@"
222
+ fi
223
+ }
224
+
225
+ prepare_codex_home() {
226
+ local codex_home="${CODEX_HOME:-${HOME}/.codex}"
227
+
228
+ if [ -e "${codex_home}" ] && [ ! -d "${codex_home}" ]; then
229
+ log "codex: ${codex_home} exists but is not a directory."
230
+ return 1
231
+ fi
232
+
233
+ if [ ! -d "${codex_home}" ]; then
234
+ mkdir -p "${codex_home}"
235
+ fi
236
+
237
+ if [ ! -w "${codex_home}" ]; then
238
+ run_as_root chown "$(id -u):$(id -g)" "${codex_home}" 2>/dev/null || true
239
+ fi
240
+
241
+ if [ ! -w "${codex_home}" ]; then
242
+ run_as_root chmod u+rwx "${codex_home}" 2>/dev/null || true
243
+ fi
244
+
245
+ if [ ! -w "${codex_home}" ]; then
246
+ log "codex: ${codex_home} is not writable; Codex CLI install/update may fail."
247
+ return 1
248
+ fi
249
+ }
250
+
251
+ prune_codex_standalone_releases() {
252
+ local keep_previous
253
+ local codex_home
254
+ local standalone_dir
255
+ local releases_dir
256
+ local current_link
257
+ local current_target
258
+ local current_name=""
259
+ local previous_kept=0
260
+ local previous_limit
261
+ local release_name
262
+
263
+ keep_previous="$(numeric_or_default "${BOXDOWN_CODEX_STANDALONE_RELEASES_KEEP_PREVIOUS:-0}" 0)"
264
+ codex_home="${CODEX_HOME:-${HOME}/.codex}"
265
+ standalone_dir="${codex_home}/packages/standalone"
266
+ releases_dir="${standalone_dir}/releases"
267
+
268
+ [ -d "${releases_dir}" ] || return 0
269
+
270
+ current_link="${standalone_dir}/current"
271
+ if [ -L "${current_link}" ]; then
272
+ current_target="$(readlink "${current_link}" 2>/dev/null || true)"
273
+ current_name="$(basename "${current_target}")"
274
+ fi
275
+
276
+ previous_limit="${keep_previous}"
277
+ if [ -z "${current_name}" ]; then
278
+ previous_limit=$((keep_previous + 1))
279
+ fi
280
+
281
+ while IFS= read -r release_name; do
282
+ if [ -n "${current_name}" ] && [ "${release_name}" = "${current_name}" ]; then
283
+ continue
284
+ fi
285
+
286
+ if [ "${previous_kept}" -lt "${previous_limit}" ]; then
287
+ previous_kept=$((previous_kept + 1))
288
+ continue
289
+ fi
290
+
291
+ rm -rf -- "${releases_dir}/${release_name}"
292
+ log "codex: pruned old standalone release: ${release_name}"
293
+ done < <(list_child_directory_names "${releases_dir}")
294
+ }
295
+
296
+ prune_claude_versions() {
297
+ local versions_dir="${HOME}/.local/share/claude/versions"
298
+ local current_link="${HOME}/.local/bin/claude"
299
+ local current_target
300
+ local current_name=""
301
+ local version_name
302
+
303
+ [ -d "${versions_dir}" ] || return 0
304
+ [ -L "${current_link}" ] || return 0
305
+
306
+ current_target="$(readlink "${current_link}" 2>/dev/null || true)"
307
+ current_name="$(basename "${current_target}")"
308
+ [ -n "${current_name}" ] || return 0
309
+
310
+ while IFS= read -r version_name; do
311
+ if [ "${version_name}" = "${current_name}" ]; then
312
+ continue
313
+ fi
314
+
315
+ rm -rf -- "${versions_dir}/${version_name}"
316
+ log "claude: pruned old version: ${version_name}"
317
+ done < <(list_child_directory_names "${versions_dir}")
318
+ }
319
+
320
+ cleanup_opencode_install_temps() {
321
+ local tmp_parent="${BOXDOWN_OPENCODE_INSTALL_TMP_PARENT:-/tmp}"
322
+ local tmp_dir
323
+
324
+ [ -d "${tmp_parent}" ] || return 0
325
+
326
+ for tmp_dir in "${tmp_parent}"/opencode_install_*; do
327
+ [ -e "${tmp_dir}" ] || continue
328
+ [ -d "${tmp_dir}" ] || continue
329
+ rm -rf -- "${tmp_dir}"
330
+ log "opencode: removed installer temp dir: ${tmp_dir}"
331
+ done
332
+ }
333
+
334
+ cleanup_antigravity_staging() {
335
+ local cache_dir="${BOXDOWN_ANTIGRAVITY_CACHE_DIR:-${HOME}/.cache/antigravity}"
336
+ local staging_dir="${cache_dir}/staging"
337
+
338
+ [ -d "${staging_dir}" ] || return 0
339
+
340
+ rm -rf -- "${staging_dir}"
341
+ log "antigravity: removed staging cache: ${staging_dir}"
342
+ }
343
+
344
+ cleanup_agent_after_update() {
345
+ case "$1" in
346
+ codex) prune_codex_standalone_releases ;;
347
+ opencode) cleanup_opencode_install_temps ;;
348
+ claude) prune_claude_versions ;;
349
+ antigravity) cleanup_antigravity_staging ;;
350
+ *) return 0 ;;
351
+ esac
352
+ }
353
+
354
+ list_child_directory_names() {
355
+ local parent_dir="$1"
356
+
357
+ if printf '1\n2\n' | sort -V >/dev/null 2>&1; then
358
+ find "${parent_dir}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort -Vr
359
+ return
360
+ fi
361
+
362
+ (cd "${parent_dir}" && ls -1t) 2>/dev/null | while IFS= read -r child_name; do
363
+ [ -d "${parent_dir}/${child_name}" ] && printf '%s\n' "${child_name}"
364
+ done
365
+ }
366
+
180
367
  install_codex() {
181
368
  local url
182
369
 
@@ -185,6 +372,10 @@ install_codex() {
185
372
  }
186
373
 
187
374
  update_codex() {
375
+ local result=0
376
+
377
+ prepare_codex_home || return 1
378
+
188
379
  if command -v codex >/dev/null 2>&1; then
189
380
  if codex update; then
190
381
  return 0
@@ -193,7 +384,13 @@ update_codex() {
193
384
  log "codex: codex update failed; falling back to installer."
194
385
  fi
195
386
 
196
- install_codex
387
+ if install_codex; then
388
+ result=0
389
+ else
390
+ result=$?
391
+ fi
392
+
393
+ return "${result}"
197
394
  }
198
395
 
199
396
  install_opencode() {
@@ -201,11 +398,13 @@ install_opencode() {
201
398
 
202
399
  url="${BOXDOWN_OPENCODE_INSTALL_URL:-https://opencode.ai/install}"
203
400
  run_installer_url "${url}" bash -s -- --no-modify-path
401
+ link_opencode_binary
204
402
  }
205
403
 
206
404
  update_opencode() {
207
405
  if command -v opencode >/dev/null 2>&1; then
208
406
  if opencode upgrade --method curl; then
407
+ link_opencode_binary
209
408
  return 0
210
409
  fi
211
410
 
@@ -215,6 +414,18 @@ update_opencode() {
215
414
  install_opencode
216
415
  }
217
416
 
417
+ link_opencode_binary() {
418
+ local source_path="${HOME}/.opencode/bin/opencode"
419
+ local target_dir="${HOME}/.local/bin"
420
+
421
+ if [ ! -x "${source_path}" ]; then
422
+ return 0
423
+ fi
424
+
425
+ mkdir -p "${target_dir}"
426
+ ln -sfn "${source_path}" "${target_dir}/opencode"
427
+ }
428
+
218
429
  install_claude() {
219
430
  local url
220
431
 
@@ -238,7 +449,7 @@ install_antigravity() {
238
449
  local url
239
450
 
240
451
  url="${BOXDOWN_ANTIGRAVITY_INSTALL_URL:-https://antigravity.google/cli/install.sh}"
241
- run_installer_url "${url}" bash -s -- --skip-path
452
+ run_installer_url "${url}" bash
242
453
  }
243
454
 
244
455
  update_antigravity() {
@@ -270,11 +481,13 @@ update_now_agent() {
270
481
  local lock_dir
271
482
  local result=0
272
483
 
484
+ progress "${agent}: installing or updating CLI"
273
485
  stamp_file="$(agent_stamp_file "${agent}")" || return 1
274
486
  lock_dir="$(agent_lock_dir "${agent}")" || return 1
275
487
  ensure_state_dir "${stamp_file}" "${lock_dir}"
276
488
  acquire_lock "${agent}" "${lock_dir}" || return 1
277
489
  if run_agent_update "${agent}"; then
490
+ cleanup_agent_after_update "${agent}" || true
278
491
  touch_stamp "${stamp_file}"
279
492
  else
280
493
  result=$?
@@ -296,6 +509,7 @@ maybe_update_agent() {
296
509
  return 0
297
510
  fi
298
511
 
512
+ progress "${agent}: checking for CLI updates"
299
513
  acquire_lock "${agent}" "${lock_dir}" || return 0
300
514
  if stamp_fresh "${agent}" "${stamp_file}"; then
301
515
  release_lock
@@ -303,6 +517,7 @@ maybe_update_agent() {
303
517
  fi
304
518
 
305
519
  if run_agent_update "${agent}"; then
520
+ cleanup_agent_after_update "${agent}" || true
306
521
  touch_stamp "${stamp_file}"
307
522
  else
308
523
  result=$?
@@ -311,6 +526,45 @@ maybe_update_agent() {
311
526
  return "${result}"
312
527
  }
313
528
 
529
+ ensure_agent() {
530
+ local agent="$1"
531
+ local result=0
532
+
533
+ progress "${agent}: ensuring CLI is available"
534
+ if agent_binary_available "${agent}"; then
535
+ if maybe_update_agent "${agent}"; then
536
+ return 0
537
+ fi
538
+
539
+ result=$?
540
+ if agent_binary_available "${agent}"; then
541
+ log "${agent}: update failed, but the CLI is installed; continuing."
542
+ return 0
543
+ fi
544
+
545
+ return "${result}"
546
+ fi
547
+
548
+ log "${agent}: CLI is missing; installing before launch."
549
+ if update_now_agent "${agent}"; then
550
+ if agent_binary_available "${agent}"; then
551
+ return 0
552
+ fi
553
+
554
+ log "${agent}: CLI is still unavailable after install/update."
555
+ return 1
556
+ fi
557
+
558
+ result=$?
559
+ if agent_binary_available "${agent}"; then
560
+ log "${agent}: install/update reported failure, but the CLI is installed; continuing."
561
+ return 0
562
+ fi
563
+
564
+ log "${agent}: CLI is still unavailable after install/update."
565
+ return "${result}"
566
+ }
567
+
314
568
  run_action_for_agent() {
315
569
  local action="$1"
316
570
  local agent="$2"
@@ -322,6 +576,9 @@ run_action_for_agent() {
322
576
  maybe-update)
323
577
  maybe_update_agent "${agent}"
324
578
  ;;
579
+ ensure)
580
+ ensure_agent "${agent}"
581
+ ;;
325
582
  *)
326
583
  usage
327
584
  return 1
@@ -340,7 +597,14 @@ main() {
340
597
  fi
341
598
  shift
342
599
 
600
+ prepend_agent_bin_paths
601
+
343
602
  if [ "$#" -eq 0 ]; then
603
+ if [ "${action}" = "ensure" ]; then
604
+ usage
605
+ return 1
606
+ fi
607
+
344
608
  agents=("${DEFAULT_AGENTS[@]}")
345
609
  else
346
610
  agents=("$@")
@@ -5,9 +5,16 @@ main() {
5
5
  [[ -f package.json ]] || return 0
6
6
  local pm
7
7
  pm=$(detect_package_manager)
8
+ progress "Installing workspace dependencies with ${pm}"
8
9
  install_dependencies "$pm" || true
9
10
  }
10
11
 
12
+ progress() {
13
+ if [[ "${BOXDOWN_PROGRESS:-0}" == "1" ]]; then
14
+ printf 'BOXDOWN_PROGRESS: %s\n' "$*"
15
+ fi
16
+ }
17
+
11
18
  detect_package_manager() {
12
19
  local pm
13
20
  if pm=$(detect_package_manager_from_lockfiles); then
@@ -75,6 +82,8 @@ detect_package_manager_from_package_json() {
75
82
 
76
83
  install_dependencies() {
77
84
  local pm="$1"
85
+ ensure_package_manager "$pm"
86
+
78
87
  case "$pm" in
79
88
  pnpm) pnpm install ;;
80
89
  npm) npm install ;;
@@ -84,4 +93,63 @@ install_dependencies() {
84
93
  esac
85
94
  }
86
95
 
96
+ ensure_package_manager() {
97
+ local pm="$1"
98
+
99
+ case "$pm" in
100
+ pnpm)
101
+ ensure_pnpm
102
+ ;;
103
+ yarn)
104
+ ensure_yarn
105
+ ;;
106
+ esac
107
+ }
108
+
109
+ ensure_pnpm() {
110
+ if command -v pnpm >/dev/null 2>&1; then
111
+ return 0
112
+ fi
113
+
114
+ if command -v npm >/dev/null 2>&1; then
115
+ progress "Installing pnpm package manager"
116
+ run_as_root npm install --global pnpm@11
117
+ fi
118
+ }
119
+
120
+ ensure_yarn() {
121
+ if command -v yarn >/dev/null 2>&1; then
122
+ return 0
123
+ fi
124
+
125
+ if command -v corepack >/dev/null 2>&1; then
126
+ progress "Enabling Corepack for Yarn"
127
+ enable_corepack
128
+ fi
129
+ }
130
+
131
+ enable_corepack() {
132
+ if ! command -v corepack >/dev/null 2>&1; then
133
+ return 0
134
+ fi
135
+
136
+ if corepack enable >/dev/null 2>&1; then
137
+ return 0
138
+ fi
139
+
140
+ if command -v sudo >/dev/null 2>&1; then
141
+ sudo corepack enable >/dev/null 2>&1 || true
142
+ fi
143
+ }
144
+
145
+ run_as_root() {
146
+ if [ "$(id -u)" -eq 0 ]; then
147
+ "$@"
148
+ elif command -v sudo >/dev/null 2>&1; then
149
+ sudo "$@"
150
+ else
151
+ "$@"
152
+ fi
153
+ }
154
+
87
155
  main "$@"
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env bash
2
+ # Prepare a writable container-global Git config from the host snapshot.
3
+
4
+ set -euo pipefail
5
+
6
+ SOURCE_PATH="${BOXDOWN_GITCONFIG_SOURCE_PATH:-/opt/boxdown/state/host-gitconfig/.gitconfig}"
7
+ TARGET_PATH="${BOXDOWN_GITCONFIG_TARGET_PATH:-/home/node/.gitconfig}"
8
+
9
+ main() {
10
+ progress "Preparing writable Git config"
11
+ if ! install_writable_gitconfig; then
12
+ return 0
13
+ fi
14
+
15
+ progress "Configuring container Git authentication"
16
+ sanitize_github_rewrites
17
+ sanitize_host_credential_helpers credential.helper
18
+ sanitize_host_credential_helpers credential.https://github.com.helper
19
+ configure_container_github_auth
20
+ }
21
+
22
+ progress() {
23
+ if [[ "${BOXDOWN_PROGRESS:-0}" == "1" ]]; then
24
+ printf 'BOXDOWN_PROGRESS: %s\n' "$*"
25
+ fi
26
+ }
27
+
28
+ install_writable_gitconfig() {
29
+ local target_dir
30
+
31
+ target_dir="$(dirname "${TARGET_PATH}")"
32
+ mkdir -p "${target_dir}"
33
+
34
+ if command -v mountpoint >/dev/null 2>&1 && mountpoint -q "${TARGET_PATH}"; then
35
+ echo "git-config-bootstrap: ${TARGET_PATH} is still a mount; recreate the devcontainer so Boxdown can install a writable Git config copy." >&2
36
+ return 1
37
+ fi
38
+
39
+ if [[ -f "${SOURCE_PATH}" ]]; then
40
+ cp "${SOURCE_PATH}" "${TARGET_PATH}"
41
+ else
42
+ : > "${TARGET_PATH}"
43
+ fi
44
+
45
+ chmod 0600 "${TARGET_PATH}" || true
46
+ ensure_node_owns_gitconfig
47
+
48
+ if [[ ! -w "${TARGET_PATH}" ]]; then
49
+ echo "git-config-bootstrap: ${TARGET_PATH} is not writable; recreate the devcontainer so Boxdown can install a writable Git config copy." >&2
50
+ return 1
51
+ fi
52
+ }
53
+
54
+ ensure_node_owns_gitconfig() {
55
+ if ! id node >/dev/null 2>&1; then
56
+ return 0
57
+ fi
58
+
59
+ if [[ "$(id -un)" == "root" ]]; then
60
+ chown node:node "${TARGET_PATH}" || true
61
+ return 0
62
+ fi
63
+
64
+ if command -v sudo >/dev/null 2>&1; then
65
+ sudo chown node:node "${TARGET_PATH}" >/dev/null 2>&1 || true
66
+ fi
67
+ }
68
+
69
+ git_global() {
70
+ GIT_CONFIG_GLOBAL="${TARGET_PATH}" git config --global "$@"
71
+ }
72
+
73
+ sanitize_github_rewrites() {
74
+ git_global --unset-all url.git@github.com:.insteadOf >/dev/null 2>&1 || true
75
+ git_global --unset-all url.ssh://git@github.com/.insteadOf >/dev/null 2>&1 || true
76
+ }
77
+
78
+ sanitize_host_credential_helpers() {
79
+ local key="$1"
80
+ local values=()
81
+ local value
82
+
83
+ while IFS= read -r value; do
84
+ values+=("${value}")
85
+ done < <(git_global --get-all "${key}" 2>/dev/null || true)
86
+
87
+ if [[ "${#values[@]}" -eq 0 ]]; then
88
+ return 0
89
+ fi
90
+
91
+ git_global --unset-all "${key}" >/dev/null 2>&1 || true
92
+
93
+ for value in "${values[@]}"; do
94
+ if is_unsupported_credential_helper "${value}"; then
95
+ continue
96
+ fi
97
+
98
+ git_global --add "${key}" "${value}"
99
+ done
100
+ }
101
+
102
+ is_unsupported_credential_helper() {
103
+ local value="$1"
104
+ local executable
105
+
106
+ case "${value}" in
107
+ /Users/* | /Applications/* | /opt/homebrew/* | !/Users/* | !/Applications/* | !/opt/homebrew/*)
108
+ return 0
109
+ ;;
110
+ esac
111
+
112
+ if [[ "${value}" == /* ]]; then
113
+ executable="${value%% *}"
114
+ if [[ ! -e "${executable}" ]]; then
115
+ return 0
116
+ fi
117
+ fi
118
+
119
+ return 1
120
+ }
121
+
122
+ configure_container_github_auth() {
123
+ git_global --unset-all credential.https://github.com.helper >/dev/null 2>&1 || true
124
+ git_global --add credential.https://github.com.helper ''
125
+ git_global --add credential.https://github.com.helper '!gh auth git-credential'
126
+ }
127
+
128
+ main "$@"
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env bash
2
+ # Configure best-effort SSH commit signing without modifying the workspace Git config.
3
+
4
+ set -euo pipefail
5
+
6
+ TARGET_PATH="${BOXDOWN_GITCONFIG_TARGET_PATH:-/home/node/.gitconfig}"
7
+ KEY_PATH="${BOXDOWN_GIT_SIGNING_KEY_PATH:-/opt/boxdown/state/git-signing/signing-key.pub}"
8
+ ENABLED="${BOXDOWN_GIT_SIGNING_ENABLED:-0}"
9
+ HOST_REASON="${BOXDOWN_GIT_SIGNING_REASON:-host-preflight-unavailable}"
10
+
11
+ git_global() {
12
+ GIT_CONFIG_GLOBAL="${TARGET_PATH}" git config --global "$@"
13
+ }
14
+
15
+ git_local_value() {
16
+ git config --local --get "$1" 2>/dev/null || true
17
+ }
18
+
19
+ git_global_value() {
20
+ git_global --get "$1" 2>/dev/null || true
21
+ }
22
+
23
+ preserve_user_signing_preference() {
24
+ local local_commit global_commit local_format global_format local_program global_program
25
+ local_commit="$(git_local_value commit.gpgsign)"
26
+ global_commit="$(git_global_value commit.gpgsign)"
27
+ local_format="$(git_local_value gpg.format)"
28
+ global_format="$(git_global_value gpg.format)"
29
+ local_program="$(git_local_value gpg.program)"
30
+ global_program="$(git_global_value gpg.program)"
31
+
32
+ [[ "${local_commit}" == "false" || "${global_commit}" == "false" ]] && return 0
33
+ [[ -n "${local_program}" || -n "${global_program}" ]] && return 0
34
+ [[ -n "${local_format}" && "${local_format}" != "ssh" ]] && return 0
35
+ [[ -n "${global_format}" && "${global_format}" != "ssh" ]] && return 0
36
+ return 1
37
+ }
38
+
39
+ disable_signing() {
40
+ local reason="${1:-unknown}"
41
+ if preserve_user_signing_preference; then
42
+ printf 'boxdown: commit signing configuration preserved (reason: %s).\n' "${reason}" >&2
43
+ return 0
44
+ fi
45
+ git_global --unset-all gpg.format >/dev/null 2>&1 || true
46
+ git_global --unset-all user.signingkey >/dev/null 2>&1 || true
47
+ git_global --unset-all gpg.program >/dev/null 2>&1 || true
48
+ git_global --replace-all commit.gpgsign false
49
+ printf 'boxdown: commit signing unavailable (reason: %s); commits will remain unsigned.\n' "${reason}" >&2
50
+ }
51
+
52
+ enable_signing() {
53
+ if preserve_user_signing_preference; then
54
+ printf '%s\n' 'boxdown: preserving explicit user Git signing configuration.' >&2
55
+ return 0
56
+ fi
57
+
58
+ if [[ ! -r "${KEY_PATH}" ]]; then
59
+ disable_signing 'container-key-unavailable'
60
+ return 0
61
+ fi
62
+
63
+ if ! bash "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ssh-agent-proxy-bootstrap.sh"; then
64
+ disable_signing 'container-agent-proxy-unavailable'
65
+ return 0
66
+ fi
67
+
68
+ local agent_identities
69
+ if ! agent_identities="$(ssh-add -L 2>/dev/null)"; then
70
+ disable_signing 'container-agent-unavailable'
71
+ return 0
72
+ fi
73
+
74
+ local selected_key
75
+ selected_key="$(awk 'NR == 1 { print $1 " " $2 }' "${KEY_PATH}")"
76
+ if ! grep -qF -- "${selected_key}" <<<"${agent_identities}"; then
77
+ disable_signing 'container-key-not-loaded'
78
+ return 0
79
+ fi
80
+
81
+ git_global --unset-all gpg.program >/dev/null 2>&1 || true
82
+ git_global --replace-all gpg.format ssh
83
+ git_global --replace-all user.signingkey "${KEY_PATH}"
84
+ git_global --replace-all commit.gpgsign true
85
+
86
+ local probe_dir
87
+ probe_dir="$(mktemp -d)"
88
+ if ! (
89
+ cd "${probe_dir}"
90
+ git init -q
91
+ git config user.name 'Boxdown signing probe'
92
+ git config user.email 'signing-probe@boxdown.invalid'
93
+ git config gpg.format ssh
94
+ git config user.signingkey "${KEY_PATH}"
95
+ git config commit.gpgsign true
96
+ git commit --allow-empty -m 'boxdown signing probe' >/dev/null
97
+ ); then
98
+ rm -rf "${probe_dir}"
99
+ disable_signing 'container-signing-probe-failed'
100
+ return 0
101
+ fi
102
+ rm -rf "${probe_dir}"
103
+ }
104
+
105
+ if [[ "${ENABLED}" == "1" ]]; then
106
+ enable_signing
107
+ else
108
+ disable_signing "${HOST_REASON}"
109
+ fi