Two real defects fixed on the way across.
The sudoers write was in the wrong order. The original echoed the rule straight
into /etc/sudoers.d, validated it afterwards, and chmod'd it later still. A
malformed file there breaks sudo COMPLETELY — and you cannot sudo to repair it,
so on a remote machine that is a rescue console — and so does one with loose
permissions, because sudo refuses to read its own configuration. Both of those
windows were live in the original ordering. grant_passwordless_sudo now writes a
temp file, runs visudo -c against it, and only then places it with install(1),
which applies the content and the 0440 mode in one step. Nothing reaches
/etc/sudoers.d that has not already been validated.
The .tmux.conf copy overwrote whatever was in the home on every run. lib/files.sh
adds the two shapes that stop this whole class of thing:
install_config installs when absent, does nothing when identical, and keeps
what the user wrote when it differs — printing the cp to take
ours, so the choice stays theirs
append_once wraps a block in named markers so a second run recognises its
own work; also lets a human see which lines came from this
script and remove them as a unit
append_once is what the five unguarded `cat >>` into .zshrc need when those
sections are ported — a second pass currently duplicates the starship init, the
nvim PATH, bun, deno and the aliases.
Passwordless sudo is asked separately from creating the account, because it is a
security posture rather than part of making a user, and the cost is stated: a key
that can log into this account is root without a further step. Officer's actual
requirement is stated too — os-user-shell.ts runs `sudo -n`, and a prompt it
cannot answer surfaces as a permissions error rather than a question — and
refusing records that consequence in the summary instead of a bare "skipped".
Verified: all three install_config outcomes, append_once writing exactly once
across two runs, visudo rejecting junk before anything is installed, and the
section reporting correctly against this host's existing account.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
76 lines
2.3 KiB
Bash
76 lines
2.3 KiB
Bash
#!/bin/bash
|
|
# =============================================================================
|
|
# machine-setup — writing files into somebody's home
|
|
# =============================================================================
|
|
#
|
|
# Definitions only, like the other lib/ files.
|
|
#
|
|
# ── The rule ──
|
|
#
|
|
# A setup script may create a config file. It may not silently replace one the
|
|
# user wrote. The original did the second: `cp .tmux.conf $USER_HOME/` on every
|
|
# run, over whatever was there, and five separate `cat >>` into .zshrc with no
|
|
# guard — so a second pass duplicated the starship init, the nvim PATH, bun, deno
|
|
# and the aliases.
|
|
#
|
|
# Both of those are the same mistake in different shapes: writing without looking
|
|
# first. The two helpers here are the two safe shapes.
|
|
|
|
[[ -n "${MACHINE_SETUP_FILES_LOADED:-}" ]] && return 0
|
|
MACHINE_SETUP_FILES_LOADED=1
|
|
|
|
# Put a config file in place, unless the user has their own.
|
|
#
|
|
# Three outcomes, and the caller can tell them apart by the return code:
|
|
#
|
|
# 0 installed — there was nothing there
|
|
# 1 identical — already exactly this, nothing done
|
|
# 2 kept — theirs differs, left alone
|
|
#
|
|
# Converging when there is nothing to lose and keeping what the user wrote when
|
|
# there is. The third case prints how to take ours, so the choice stays with
|
|
# them.
|
|
install_config() {
|
|
local src="$1" dest="$2" owner="$3"
|
|
|
|
if [[ -f "$dest" ]]; then
|
|
if cmp -s "$src" "$dest"; then
|
|
return 1
|
|
fi
|
|
warn "kept your ${dest} — it differs from the one shipped here"
|
|
echo " to take ours instead: cp ${src} ${dest}"
|
|
return 2
|
|
fi
|
|
|
|
install -D -m 0644 -o "$owner" -g "$owner" "$src" "$dest"
|
|
return 0
|
|
}
|
|
|
|
# Append a block to a file exactly once.
|
|
#
|
|
# The block is wrapped in markers naming what it is, so a second run recognises
|
|
# its own work instead of adding it again — and so a human reading the file can
|
|
# see which lines came from here and delete them as a unit.
|
|
#
|
|
# append_once ~/.zshrc bun <<'EOF'
|
|
# export PATH="$HOME/.bun/bin:$PATH"
|
|
# EOF
|
|
#
|
|
# Returns 0 if it wrote, 1 if the block was already there.
|
|
append_once() {
|
|
local file="$1" name="$2"
|
|
local begin="# >>> machine-setup: ${name} >>>"
|
|
local end="# <<< machine-setup: ${name} <<<"
|
|
|
|
if [[ -f "$file" ]] && grep -qF "$begin" "$file"; then
|
|
return 1
|
|
fi
|
|
|
|
{
|
|
echo ""
|
|
echo "$begin"
|
|
cat
|
|
echo "$end"
|
|
} >>"$file"
|
|
}
|