one section per concern, each announced and confirmed before it acts

Three sections where there were two, and none of them touches the machine until
you say so:

  2. System update      upgrades what is already installed
  3. Core utils         what the distribution provides
  4. Command-line tools lazydocker, lazygit, starship, fastfetch

The split matters because these are different kinds of change and deserve
separate answers. System update is the only step in the whole script that moves
versions of software already on the machine; core utils only ever adds what is
absent; and the four tools are upstream binaries the distribution does not ship
at all. Previously the update and the core packages were one step and the tools
were tacked onto the end of it, so agreeing to "essentials" meant agreeing to all
three at once.

Every section now prints what it will install and what it is leaving alone, then
asks. Enter means yes — unlike the machine-role question, which has no default,
because these are "do the thing you already asked for" and making twenty of them
require a deliberate keystroke would train people to hold the y key down.
ASSUME_YES=1 answers all of them for an unattended run, and EOF fails with that
named rather than spinning.

Refusing is recorded rather than glossed: LAST_SKIPPED feeds the summary, so a
declined section reads "Core utils: SKIPPED by request — cowsay neofetch" instead
of quietly reporting nothing installed.

Nothing to install means no prompt at all — there is nothing to agree to.

announce_plan takes the array NAMES rather than their contents, because once a
list has been through word splitting an empty one cannot be told from a missing
one.

Verified all three paths: accept, refuse, and nothing-to-do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 17:37:31 +00:00
co-authored by Claude Opus 5
parent a5ef9f7662
commit f896d4882f
4 changed files with 104 additions and 32 deletions
+28
View File
@@ -135,6 +135,34 @@ prompt_value() {
fi
}
# Ask before acting. Every section that changes the machine goes through this, so
# a run is a sequence of things you agreed to rather than a wall of output you
# read afterwards to find out what happened.
#
# Enter means yes — unlike the machine-role question, which has no default. These
# are "do the thing you already asked for", and making twenty of them require a
# deliberate keystroke would train people to hold the y key down.
#
# ASSUME_YES=1 answers all of them, for an unattended run.
confirm() {
local message="${1:-Proceed?}" answer
[[ "${ASSUME_YES:-}" == "1" ]] && return 0
while true; do
# EOF is not a yes. Without this an unattended run without ASSUME_YES would
# spin here forever.
if ! read -rp " ${message} [Y/n]: " answer; then
echo ""
fail "No answer. Set ASSUME_YES=1 to run without prompts."
fi
case "$answer" in
"" | y | Y | yes | Yes) return 0 ;;
n | N | no | No) return 1 ;;
*) warn "Answer y or n." ;;
esac
done
}
# Run a block as the created user (login shell, inherits HOME)
as_user() {
sudo -u "$USERNAME" -i bash -c "$1"
+36 -7
View File
@@ -35,6 +35,7 @@ MACHINE_SETUP_PACKAGES_LOADED=1
# packages it deliberately left alone.
LAST_INSTALLED=()
LAST_KEPT=()
LAST_SKIPPED=()
# -----------------------------------------------------------------------------
# The sections
@@ -155,6 +156,7 @@ pkg_install() {
local pkg
local -a missing=() present=()
LAST_SKIPPED=()
for pkg in "$@"; do
if pkg_is_installed "$pkg"; then present+=("$pkg"); else missing+=("$pkg"); fi
done
@@ -162,23 +164,50 @@ pkg_install() {
LAST_INSTALLED=("${missing[@]}")
LAST_KEPT=("${present[@]}")
info "$label — installs what is missing, keeps what you already have"
((${#present[@]})) && echo " already here: ${present[*]}"
announce_plan "$label" present missing || return 0
pkg_install_now "${missing[@]}"
}
if ((${#missing[@]} == 0)); then
# Print what a section is about to do and ask permission for it.
#
# Takes the NAMES of the two arrays rather than their contents, because a list
# passed by value cannot be told apart from an empty one once it has been through
# word splitting.
#
# Returns non-zero when there is nothing to do, or when the answer was no — in
# both cases the caller should skip its action. LAST_INSTALLED is cleared on a
# refusal so the summary does not claim work that never happened.
announce_plan() {
local label="$1"
local -n _present="$2"
local -n _missing="$3"
echo ""
info "${label} — installs what is missing, keeps what you already have"
((${#_present[@]})) && echo " already here: ${_present[*]}"
if ((${#_missing[@]} == 0)); then
echo " to install: nothing, all present"
return 0
return 1
fi
echo " to install: ${missing[*]}"
pkg_install_now "${missing[@]}"
echo " to install: ${_missing[*]}"
if ! confirm "Proceed?"; then
warn "skipped by request"
LAST_INSTALLED=()
LAST_SKIPPED=("${_missing[@]}")
return 1
fi
return 0
}
# One summary line describing what a section actually did, from LAST_INSTALLED
# and LAST_KEPT. Call straight after pkg_install or tools_install.
summarise_last() {
local label="$1"
if ((${#LAST_INSTALLED[@]} == 0)); then
if ((${#LAST_SKIPPED[@]})); then
SUMMARY+=("$label: SKIPPED by request — ${LAST_SKIPPED[*]}")
elif ((${#LAST_INSTALLED[@]} == 0)); then
SUMMARY+=("$label: already present, nothing installed")
elif ((${#LAST_KEPT[@]} == 0)); then
SUMMARY+=("$label installed: ${LAST_INSTALLED[*]}")
+2 -8
View File
@@ -109,6 +109,7 @@ tools_install() {
local tool
local -a missing=() present=()
LAST_SKIPPED=()
for tool in "$@"; do
if tool_is_installed "$tool"; then present+=("$tool"); else missing+=("$tool"); fi
done
@@ -116,15 +117,8 @@ tools_install() {
LAST_INSTALLED=("${missing[@]}")
LAST_KEPT=("${present[@]}")
info "$label — installs what is missing, keeps what you already have"
((${#present[@]})) && echo " already here: ${present[*]}"
announce_plan "$label" present missing || return 0
if ((${#missing[@]} == 0)); then
echo " to install: nothing, all present"
return 0
fi
echo " to install: ${missing[*]}"
for tool in "${missing[@]}"; do
info " installing ${tool}..."
"tool_install_${tool}"
+38 -17
View File
@@ -75,35 +75,56 @@ fi
USER_HOME="/home/$USERNAME"
# =============================================================================
# 2. System Update & Essentials
# 2. System update
# =============================================================================
#
# Its own section because it is the only thing in the script that moves versions
# of software already on the machine. Everything else only ever adds what is
# absent, so this is the one that deserves to be refused on its own.
step "System Update & Essentials"
step "System update"
if ! skip; then
info "Refreshing the package index..."
pkg_refresh
pkg_refresh >/dev/null
# The one place that deliberately moves versions of things already installed.
# Everything else in this script only ever adds what is absent.
info "Upgrading installed packages..."
echo ""
info "System update — upgrades packages already installed to their latest versions"
echo " this is the only step that changes software already on this machine"
if confirm "Proceed?"; then
pkg_upgrade_all
# shellcheck disable=SC2046 # word splitting is how the list is passed
pkg_install "Core packages" $(pkgs_core)
summarise_last "Core packages"
ok "System updated and core packages in place"
ok "System upgraded"
SUMMARY+=("System packages upgraded")
else
warn "skipped by request"
SUMMARY+=("System update: SKIPPED by request")
fi
step_ok
fi
# =============================================================================
# 2b. Command-line tools
# 3. Core utils
# =============================================================================
#
# Separate from the packages above because they are a different thing: upstream
# binaries on their own release cadence, not anything the distribution ships.
# Lumping them in made a run look like it was installing system packages and
# then start downloading tarballs unannounced.
# What the distribution provides: the six this script would break without, and
# the command-line tools that make a machine worth sitting at.
step "Core utils"
if ! skip; then
# shellcheck disable=SC2046 # word splitting is how the list is passed
pkg_install "Core utils" $(pkgs_core)
summarise_last "Core utils"
step_ok
fi
# =============================================================================
# 4. Command-line tools
# =============================================================================
#
# A different thing from core utils, and kept apart from them: upstream binaries
# fetched from upstream, on their own release cadence, none of which the
# distribution ships. Lumping them in made a run look like it was installing
# system packages and then start pulling tarballs unannounced.
step "Command-line tools"
if ! skip; then