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.
- package/README.md +135 -12
- package/assets/devcontainer/README.md +65 -21
- package/assets/devcontainer/devcontainer.json +27 -15
- package/assets/devcontainer/hooks/initialize.sh +76 -22
- package/assets/devcontainer/hooks/post-create.sh +70 -12
- package/assets/devcontainer/hooks/post-start.sh +20 -13
- package/assets/devcontainer/ssh-config-install.sh +12 -3
- package/assets/devcontainer/start.sh +721 -44
- package/assets/devcontainer/utils/coding-agent-cli-update.sh +267 -3
- package/assets/devcontainer/utils/deps-install.sh +68 -0
- package/assets/devcontainer/utils/git-config-bootstrap.sh +128 -0
- package/assets/devcontainer/utils/git-signing-bootstrap.sh +109 -0
- package/assets/devcontainer/utils/python-bootstrap.sh +69 -0
- package/assets/devcontainer/utils/secret-env-bootstrap.sh +22 -0
- package/assets/devcontainer/utils/ssh-agent-proxy-bootstrap.sh +27 -0
- package/assets/devcontainer/utils/ssh-agent-proxy.mjs +39 -0
- package/assets/devcontainer/utils/ssh-bootstrap.sh +9 -0
- package/dist/bin/cli.cjs +1 -1
- package/dist/bin/cli.mjs +1 -1
- package/dist/main-BDgyf2t5.cjs +5758 -0
- package/dist/main-J4_2Up3o.mjs +5718 -0
- package/dist/main-J4_2Up3o.mjs.map +1 -0
- package/dist/main.cjs +5 -2
- package/dist/main.d.cts +501 -4
- package/dist/main.d.cts.map +1 -1
- package/dist/main.d.mts +501 -4
- package/dist/main.d.mts.map +1 -1
- package/dist/main.mjs +2 -2
- package/docs/README.md +1 -0
- package/docs/architecture.md +32 -0
- package/docs/development.md +13 -6
- package/docs/features/README.md +2 -0
- package/docs/features/commit-signing.md +94 -0
- package/docs/features/generated-config-and-state.md +73 -5
- package/docs/features/github-auth-refresh.md +15 -2
- package/docs/features/lifecycle.md +103 -11
- package/docs/features/setup.md +66 -0
- package/docs/features/ssh-config-and-proxy.md +228 -7
- package/docs/features/start-and-shell.md +45 -5
- package/docs/superpowers/plans/2026-07-11-default-commit-signing.md +110 -0
- package/docs/superpowers/plans/2026-07-11-progress-ci-regression.md +125 -0
- package/docs/superpowers/plans/2026-07-17-node-ssh-agent-proxy-signing.md +132 -0
- package/docs/superpowers/plans/2026-07-17-runtime-secret-environment.md +174 -0
- package/docs/superpowers/specs/2026-07-11-default-commit-signing-design.md +416 -0
- package/docs/superpowers/specs/2026-07-11-progress-ci-regression-design.md +43 -0
- package/docs/superpowers/specs/2026-07-17-node-ssh-agent-proxy-design.md +63 -0
- package/docs/superpowers/specs/2026-07-17-runtime-secret-environment-design.md +194 -0
- package/docs/testing.md +35 -2
- package/docs/todo.md +2 -2
- package/package.json +1 -1
- package/src/claude-app-config.ts +304 -0
- package/src/cli-style.ts +43 -0
- package/src/codex-app-config.ts +656 -0
- package/src/config.ts +80 -10
- package/src/constants.ts +12 -0
- package/src/devcontainer.ts +511 -64
- package/src/doctor.ts +292 -30
- package/src/git-signing.ts +267 -0
- package/src/interactive-prompts.ts +692 -0
- package/src/list.ts +16 -2
- package/src/logging.ts +164 -0
- package/src/main.ts +1214 -63
- package/src/metadata.ts +52 -3
- package/src/package-info.ts +18 -0
- package/src/paths.ts +71 -11
- package/src/process.ts +80 -2
- package/src/progress.ts +593 -0
- package/src/purge.ts +216 -0
- package/src/shell.ts +25 -0
- package/src/ssh-config.ts +134 -16
- package/src/ssh-install-targets.ts +111 -0
- package/src/ssh-key.ts +53 -13
- package/src/status.ts +5 -0
- package/dist/main-BuEptwlL.cjs +0 -1707
- package/dist/main-ZFTrSVgt.mjs +0 -1685
- 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
|
|
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
|
|
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
|