KeiSeiKit-1.0/install/lib-plan.sh
Parfii-bot 03d1dc7362 refactor(v0.16): split install.sh monolith (1238 LOC) into 17 cubes
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>
2026-04-22 15:09:35 +08:00

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
}