Constructor Pattern (RULE ZERO). Zero behaviour change, zero flag
drift — all original CLI flags preserved verbatim.
Before: install.sh — 1238 LOC monolith
After: install.sh — 138 LOC dispatcher (sources libs in order)
install/lib-*.sh — 16 cubes, max 183 LOC (lib-menu)
Cubes:
lib-log 21 LOC — logging primitives
lib-backup 63 LOC — rollback trap + BACKUP_PAIRS
lib-profile 115 LOC — MANIFEST.toml profile resolution
lib-args 92 LOC — CLI parsing + --help heredoc
lib-menu 183 LOC — whiptail/dialog/plain-text interactive picker
lib-plan 150 LOC — dry-run --no-execute output
lib-prereqs 91 LOC — hard + soft dependency checks
lib-primitives 131 LOC — primitive copy + MANIFEST drive
lib-rust 114 LOC — cargo workspace build + pre-built support
lib-scaffold 144 LOC — agent/skill/block scaffolding
lib-bridges 31 LOC — project-bridge install
lib-hooks 104 LOC — settings.json jq merge
lib-agents 77 LOC — assembled agent output
lib-skills 23 LOC — skill copy
lib-wizard 20 LOC — sleep-setup wizard invocation
lib-summary 59 LOC — post-install summary
Invariants preserved:
- macOS bash 3.2 compat (no associative arrays, no [[ ]], no ${,,})
- rollback trap wired via setup_backup_trap early in dispatcher
- jq-merge behaviour verbatim in lib-hooks
- scoped Cargo.toml regeneration in lib-rust
Function LOC limits: largest non-heredoc fn 22 LOC (check_soft_prereqs).
Three functions kept >30 LOC because heredoc-dominated (print_help,
print_summary, profile_members); splitting would fragment logical unit.
62 unique function names across cubes, zero duplicates (grep-verified).
bash -n passes on all 17 files. Runtime smoke test deferred to user's
shell (bash-readonly sandbox constraint).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
150 lines
5 KiB
Bash
150 lines
5 KiB
Bash
# shellcheck shell=bash
|
|
# lib-plan.sh — install-plan estimation, soft-dep status, confirm screen.
|
|
#
|
|
# Per-primitive time/disk estimates are hardcoded here (not in MANIFEST) to
|
|
# keep the manifest declarative + UX hints local. Shell primitives are
|
|
# ~1s / 5 KB; rust primitives vary by dep weight.
|
|
#
|
|
# Requires: primitive_field from lib-profile.sh.
|
|
# Requires: say / warn / err from lib-log.sh.
|
|
# Reads globals: ASSUME_YES, CONFIRM_TOTAL, CONFIRM_SECS, CONFIRM_MB (set by install.sh).
|
|
|
|
primitive_time_secs() {
|
|
local name="$1" kind
|
|
kind="$(primitive_field "$name" kind 2>/dev/null || true)"
|
|
case "$kind" in
|
|
shell) echo 1 ;;
|
|
rust)
|
|
case "$name" in
|
|
mock-render|kei-migrate|kei-ledger) echo 20 ;;
|
|
kei-changelog|firewall-diff) echo 15 ;;
|
|
visual-diff|tokens-sync|ssh-check) echo 5 ;;
|
|
*) echo 10 ;;
|
|
esac
|
|
;;
|
|
*) echo 0 ;;
|
|
esac
|
|
}
|
|
|
|
primitive_disk_kb() {
|
|
local name="$1" kind
|
|
kind="$(primitive_field "$name" kind 2>/dev/null || true)"
|
|
case "$kind" in
|
|
shell) echo 5 ;;
|
|
rust)
|
|
case "$name" in
|
|
mock-render|kei-migrate|kei-ledger) echo 30000 ;;
|
|
kei-changelog|firewall-diff) echo 10000 ;;
|
|
visual-diff|tokens-sync|ssh-check) echo 5000 ;;
|
|
*) echo 8000 ;;
|
|
esac
|
|
;;
|
|
*) echo 0 ;;
|
|
esac
|
|
}
|
|
|
|
# estimate_install — reads newline-separated primitive names from stdin,
|
|
# prints "time_secs disk_kb" to stdout.
|
|
estimate_install() {
|
|
local total_secs=0 total_kb=0 name s d
|
|
while IFS= read -r name; do
|
|
[ -z "$name" ] && continue
|
|
s="$(primitive_time_secs "$name")"
|
|
d="$(primitive_disk_kb "$name")"
|
|
total_secs=$(( total_secs + s ))
|
|
total_kb=$(( total_kb + d ))
|
|
done
|
|
echo "$total_secs $total_kb"
|
|
}
|
|
|
|
# Consumers-of-tool — list primitives (from $2..$N) whose deps mention $1.
|
|
_consumers_of() {
|
|
local tool="$1"; shift
|
|
local n deps_raw out=""
|
|
for n in "$@"; do
|
|
deps_raw="$(primitive_field "$n" deps 2>/dev/null || true)"
|
|
echo "$deps_raw" | grep -qiE "(^|[^a-zA-Z])${tool}([^a-zA-Z]|$)" \
|
|
&& out="${out}${n},"
|
|
done
|
|
echo "${out%,}"
|
|
}
|
|
|
|
# check_soft_deps — reads newline-separated primitive names from stdin,
|
|
# prints one OK/MISS per unique soft-dep tool used by any listed primitive.
|
|
check_soft_deps() {
|
|
local names_nl
|
|
names_nl="$(cat)"
|
|
[ -z "$names_nl" ] && return 0
|
|
local -a tools=(jq pandoc playwright npx cargo hcloud vultr-cli yq sqlite3 curl)
|
|
local -a names_arr=()
|
|
local n tool consumers printed_header=0
|
|
while IFS= read -r n; do [ -n "$n" ] && names_arr+=("$n"); done <<< "$names_nl"
|
|
for tool in "${tools[@]}"; do
|
|
consumers="$(_consumers_of "$tool" "${names_arr[@]}")"
|
|
[ -z "$consumers" ] && continue
|
|
[ "$printed_header" = "0" ] && echo "Soft-dep status:" && printed_header=1
|
|
if command -v "$tool" >/dev/null 2>&1; then
|
|
echo " [OK] $tool installed"
|
|
else
|
|
echo " [MISS] $tool missing (needed for: $consumers)"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Per-primitive row (helper for print_plan_body). Stdin: newline names.
|
|
_print_primitive_rows() {
|
|
local name kind extra
|
|
while IFS= read -r name; do
|
|
[ -z "$name" ] && continue
|
|
kind="$(primitive_field "$name" kind 2>/dev/null || echo '?')"
|
|
extra="$(primitive_time_secs "$name")s, $(( $(primitive_disk_kb "$name") / 1024 )) MB"
|
|
printf ' + %-22s (%s, ~%s)\n' "$name" "$kind" "$extra"
|
|
done
|
|
}
|
|
|
|
# print_plan_body — prints "Install Plan" block for given label + names.
|
|
# Args: $1 = label, stdin = newline-separated primitive names.
|
|
# Sets globals: CONFIRM_TOTAL, CONFIRM_SECS, CONFIRM_MB.
|
|
print_plan_body() {
|
|
local profile_label="$1"
|
|
local names total est_secs est_kb est_mb
|
|
names="$(cat)"
|
|
total="$(printf '%s\n' "$names" | grep -c . || true)"
|
|
read -r est_secs est_kb <<< "$(printf '%s\n' "$names" | estimate_install)"
|
|
est_mb=$(( est_kb / 1024 ))
|
|
echo
|
|
echo "================================"
|
|
echo " Install Plan"
|
|
echo "================================"
|
|
echo
|
|
echo "Profile: $profile_label"
|
|
echo "Primitives: ${total:-0} to add"
|
|
[ "${total:-0}" -gt 0 ] && printf '%s\n' "$names" | _print_primitive_rows
|
|
echo
|
|
printf '%s\n' "$names" | check_soft_deps || true
|
|
echo
|
|
printf 'Estimated time: ~%ss\n' "$est_secs"
|
|
printf 'Estimated disk: ~%s MB\n' "$est_mb"
|
|
echo
|
|
CONFIRM_TOTAL="$total"; CONFIRM_SECS="$est_secs"; CONFIRM_MB="$est_mb"
|
|
}
|
|
|
|
# show_confirm_screen — prints plan body, then asks y/N (or whiptail --yesno).
|
|
# Stdin: newline-separated primitive names. Returns 0=confirmed, 1=declined.
|
|
show_confirm_screen() {
|
|
local profile_label="$1"
|
|
print_plan_body "$profile_label"
|
|
[ "$ASSUME_YES" = "1" ] && { echo "(--yes: auto-confirming)"; return 0; }
|
|
[ ! -t 0 ] && { echo "(non-TTY: auto-confirming)"; return 0; }
|
|
if command -v whiptail >/dev/null 2>&1; then
|
|
whiptail --yesno "Install ${CONFIRM_TOTAL:-0} primitive(s) for profile '$profile_label'?\n\nTime: ~${CONFIRM_SECS}s, disk: ~${CONFIRM_MB} MB" 14 70
|
|
return $?
|
|
fi
|
|
local reply
|
|
printf 'Proceed? [Y/n]: '
|
|
read -r reply || return 1
|
|
case "${reply:-Y}" in
|
|
y|Y|yes|YES|'') return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|