fix: emit BOX_MANUAL line-by-line; bare command for single candidate

claude REQUEST_CHANGES on 1c9a245: the all-candidates-fail path still
interpolated multi-line BOX_MANUAL into one warn, orphaning the or: line
and leaving try: non-pasteable on the default host.

- box_manual_emit log|warn prints one recovery line at a time
- single-candidate hosts (GitHub) get a bare pasteable command (no try:)
- multi-candidate keeps try:/or: prefixes
- never ${BOX_MANUAL} inside a log/warn string
- cli.sh asserts the emission invariant
This commit is contained in:
grok-reviewer-andresmgsl 2026-07-29 22:38:32 +00:00
parent 1c9a245595
commit a3ec47113a
2 changed files with 59 additions and 23 deletions

View file

@ -751,15 +751,31 @@ if [ "$HOST" = "yes" ]; then
# with English prose or shell metacharacters — "curl A | bash; if that
# 404s: curl B | bash" is not valid shell (`bash -n` exits 2) and is the
# same class of operator-facing failure #111 exists to remove (#125 /
# codex REQUEST_CHANGES on !114). Display uses separate prefixed lines
# (try: / or:); after a live probe succeeds the install loop rewrites
# BOX_MANUAL to the single URL that worked.
# codex REQUEST_CHANGES on !114). Multi-candidate display uses separate
# prefixed lines (try: / or:); a single candidate (GitHub default) is a
# bare pasteable command — a try: prefix turns paste into a silent no-op
# (`try:` is not a command; the pipe's bash still exits 0). After a live
# probe succeeds the install loop rewrites BOX_MANUAL to the single URL
# that worked. Consumers MUST emit via box_manual_emit — never interpolate
# ${BOX_MANUAL} into a single log/warn string (multi-line orphans the or:
# line; claude REQUEST_CHANGES on 1c9a245).
box_manual_cmd() { # box_manual_cmd <url> — one pasteable install line
printf 'curl -fsSL %s | BOX_YES=1 BOX_REF=%s bash\n' "$1" "$BOX_REF"
}
box_manual_text() {
local _n=0 _url _cmd
local _n=0 _url _cmd _urls=()
while IFS= read -r _url; do
[ -n "$_url" ] && _urls+=("$_url")
done < <(box_install_urls)
# Single candidate: bare command (no try:). Multi: try:/or: lines.
if [ "${#_urls[@]}" -le 1 ]; then
if [ "${#_urls[@]}" -eq 1 ]; then
_cmd="$(box_manual_cmd "${_urls[0]}")"
printf '%s' "$_cmd"
fi
return 0
fi
for _url in "${_urls[@]}"; do
_cmd="$(box_manual_cmd "$_url")"
_cmd="${_cmd%$'\n'}"
_n=$((_n + 1))
@ -768,22 +784,28 @@ if [ "$HOST" = "yes" ]; then
else
printf 'or: %s\n' "$_cmd"
fi
done < <(box_install_urls)
done
}
# Emit BOX_MANUAL one line at a time through log or warn. Never splice the
# multi-line value into a prose sentence.
box_manual_emit() { # box_manual_emit log|warn
local _fn="$1" _line
while IFS= read -r _line; do
[ -n "$_line" ] && "$_fn" " ${_line}"
done <<EOF
$BOX_MANUAL
EOF
}
BOX_INSTALL_URL="$(box_install_urls | head -n1)"
# Newline-separated try:/or: lines — each command after the prefix is
# independently pasteable (never one shell-looking string with prose).
# Newline-separated recovery lines — each command (after optional try:/or:
# prefix) is independently pasteable.
BOX_MANUAL="$(box_manual_text)"
if [ "${RIG_SKIP_BOX_INSTALL:-}" = "1" ]; then
log "RIG_SKIP_BOX_INSTALL=1 — skipping box install; to prepare Incus by hand later:"
while IFS= read -r _line; do [ -n "$_line" ] && log " ${_line}"; done <<EOF
$BOX_MANUAL
EOF
box_manual_emit log
elif ! command -v curl >/dev/null 2>&1; then
warn "curl not found — skipping box install; once curl is present, prepare Incus with:"
while IFS= read -r _line; do [ -n "$_line" ] && warn " ${_line}"; done <<EOF
$BOX_MANUAL
EOF
box_manual_emit warn
else
log "installing box (${BOX_REPO}@${BOX_REF}) and running its host setup — box owns Incus, not rig"
# BOX_YES=1 in the environment: non-interactive AND keeps setup-host, so box
@ -825,13 +847,16 @@ EOF
if box doctor >/dev/null 2>&1; then
log "box installed and host set up — 'box doctor' passed; mint guest boxes with 'box new'"
else
warn "box is on PATH but 'box doctor' does not pass — the CLI landed, the host stack is unproven. Run 'box doctor' for the verdict, then 'box setup-host' (or finish by hand: ${BOX_MANUAL})"
warn "box is on PATH but 'box doctor' does not pass — the CLI landed, the host stack is unproven. Run 'box doctor' for the verdict, then 'box setup-host' (or finish by hand:)"
box_manual_emit warn
fi
else
warn "box's installer reported success but no 'box' is on PATH — the install did not take effect. Finish the host by hand: ${BOX_MANUAL}"
warn "box's installer reported success but no 'box' is on PATH — the install did not take effect. Finish the host by hand:"
box_manual_emit warn
fi
else
warn "box install did not complete (no network, or box's installer failed); bootstrap's core work is done. Finish the host by hand: ${BOX_MANUAL}"
warn "box install did not complete (no network, or box's installer failed); bootstrap's core work is done. Finish the host by hand:"
box_manual_emit warn
fi
rm -f "$BOX_SCRIPT"
fi

View file

@ -303,23 +303,25 @@ check "bootstrap: box_install_urls Forgejo branch second" 0 \
check "bootstrap: box_install_urls Forgejo tag-first even for BOX_REF=main" 0 \
"https://forgejo.example/heavy-duty/box/raw/tag/main/install.sh" \
biu_line https://forgejo.example main 1
# BOX_MANUAL recovery text: separate try:/or: lines, each command pasteable
# (codex REQUEST_CHANGES / #125 — prose-joined strings fail `bash -n`).
# Extract box_manual_cmd + box_manual_text with the same nested-fn idiom.
# BOX_MANUAL recovery text: multi-candidate → separate try:/or: lines;
# single-candidate (GitHub) → bare pasteable command (no try: prefix —
# `try: curl…` is a silent no-op under bash -c; claude RC on 1c9a245).
# Extract helpers with the same nested-fn idiom.
BIM="$BIU_DIR/box-manual.sh"
{
awk '/^ box_install_urls\(\) \{/,/^ \}/' "$ROOT/commands/bootstrap.sh"
awk '/^ box_manual_cmd\(\) \{/,/^ \}/' "$ROOT/commands/bootstrap.sh"
awk '/^ box_manual_text\(\) \{/,/^ \}/' "$ROOT/commands/bootstrap.sh"
awk '/^ box_manual_emit\(\) \{/,/^ \}/' "$ROOT/commands/bootstrap.sh"
} | sed 's/^ //' > "$BIM"
check "bootstrap: box_manual helpers extracted" 0 "try:" cat "$BIM"
check "bootstrap: box_manual helpers extracted" 0 "box_manual_emit" cat "$BIM"
# shellcheck disable=SC2016
bmanual() { # bmanual HOST REF — render BOX_MANUAL text
env BOX_HOST="$1" BOX_REPO=heavy-duty/box BOX_REF="$2" \
bash -c 'set -euo pipefail; . "$1"; box_manual_text' _ "$BIM"
}
# shellcheck disable=SC2016
bmanual_cmds_ok() { # every pasteable command after try:/or: passes bash -n
bmanual_cmds_ok() { # every pasteable command after optional try:/or: passes bash -n
env BOX_HOST="$1" BOX_REPO=heavy-duty/box BOX_REF="$2" \
bash -c 'set -euo pipefail
. "$1"
@ -332,8 +334,9 @@ bmanual_cmds_ok() { # every pasteable command after try:/or: passes bash -n
bash -n <<<"$cmd"
done < <(box_manual_text)' _ "$BIM"
}
check "bootstrap: BOX_MANUAL GitHub is a single try: raw.githubusercontent.com line" 0 \
"try: curl -fsSL https://raw.githubusercontent.com/heavy-duty/box/0.9.0/install.sh | BOX_YES=1 BOX_REF=0.9.0 bash" \
# GitHub: bare command, no try: (single candidate — pasteable as-is).
check "bootstrap: BOX_MANUAL GitHub is a bare raw.githubusercontent.com command" 0 \
"curl -fsSL https://raw.githubusercontent.com/heavy-duty/box/0.9.0/install.sh | BOX_YES=1 BOX_REF=0.9.0 bash" \
bmanual https://github.com 0.9.0
check "bootstrap: BOX_MANUAL Forgejo lists raw/tag first" 0 \
"try: curl -fsSL https://forgejo.example/heavy-duty/box/raw/tag/main/install.sh | BOX_YES=1 BOX_REF=main bash" \
@ -348,6 +351,14 @@ check "bootstrap: BOX_MANUAL Forgejo commands pass bash -n" 0 "" \
# Regression: the old prose join must not return.
check "bootstrap: BOX_MANUAL does not use prose 'if that 404s'" 1 "" \
grep -qF 'if that 404s' "$ROOT/commands/bootstrap.sh"
# claude REQUEST_CHANGES on 1c9a245: multi-line BOX_MANUAL must never be
# interpolated into a single log/warn string (orphans the or: line; try:
# prefix inside a sentence is not pasteable). Only box_manual_emit may
# consume the value, one line at a time.
check "bootstrap: BOX_MANUAL never interpolated into log/warn string" 1 "" \
grep -nE '(log|warn) .*\$\{BOX_MANUAL\}' "$ROOT/commands/bootstrap.sh"
check "bootstrap: box_manual_emit is the sole multi-line consumer" 0 "" \
grep -qF 'box_manual_emit' "$ROOT/commands/bootstrap.sh"
rm -rf "$BIU_DIR"
# Opt-out for rehearsals / offline / hand-managed hosts.
check "bootstrap: box install honors RIG_SKIP_BOX_INSTALL opt-out" 0 "" \