Agent instructions, skills, SSH host aliases and expansion triggers are worth having identical on every machine one person owns, and belong in none of the shared configuration. They live in user/ now, with a manifest saying where each piece goes and a link-user stage that puts it there. That stage does nothing unless the machine said yes. Somebody who clones Panama to try the desktop keeps their own ~/.claude/CLAUDE.md exactly where it was; the question names the destinations and defaults to no. Anything displaced goes to config/old rather than being deleted. ~/.claude/CLAUDE.md and ~/.codex/AGENTS.md were byte-identical copies of one file, which is the drift this exists to prevent. Also adds the vitals toggles for the battery and Claude usage readouts, which had preferences and no way to reach them.
248 lines
11 KiB
Bash
Executable File
248 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Everything Panama needs to be told, asked before anything is installed.
|
|
#
|
|
# sunhat's failure mode was a question -- or a failure -- twenty minutes into a
|
|
# run, with a person needed at the keyboard to get past it. Walking away from an
|
|
# install meant coming back to a prompt that had been waiting an hour.
|
|
#
|
|
# So Panama asks first and then runs untouched. Everything interactive lives
|
|
# here, at the front, where the answers are cheap to change and nothing has been
|
|
# installed yet.
|
|
#
|
|
# Answers are NOT remembered between runs. There is no state file to go stale, and
|
|
# nothing personal is committed, which is what keeps this repository something
|
|
# somebody else could clone. Re-answering a handful of questions costs less than
|
|
# maintaining an answers file that drifts out of date.
|
|
#
|
|
# This asks only what a stage in this repository actually consumes. A prompt
|
|
# whose answer nothing reads is a control that lies.
|
|
#
|
|
# The hardware questions name what was found rather than asking a person to
|
|
# recite their own machine, and they are not asked at all on a machine they
|
|
# would do nothing to. Detection alone would be worse: it would remove the
|
|
# ability to decline a proprietary driver on a machine that has the card.
|
|
|
|
set -uo pipefail
|
|
|
|
# install passes the path. Refusing to guess one keeps the answers where the
|
|
# caller can delete them, rather than somewhere this script invented.
|
|
answers="${PANAMA_ANSWERS:-}"
|
|
[[ -n "$answers" ]] || { printf 'interview: PANAMA_ANSWERS is not set; run this through ./install\n' >&2; exit 1; }
|
|
: >"$answers"
|
|
|
|
# %q so a value containing a space, a quote or a dollar sign survives being
|
|
# sourced by install exactly as it was typed.
|
|
record() { printf '%s=%q\n' "$1" "$2" >>"$answers"; }
|
|
|
|
heading() { gum style --bold --foreground 4 "$1"; }
|
|
ask() { gum input --header "$1" --placeholder "${2:-}"; }
|
|
yes_no() { gum confirm --default=false "$1"; }
|
|
|
|
# ── Machine ──────────────────────────────────────────────────────────────────
|
|
|
|
heading "This machine"
|
|
current_hostname="$(hostname)"
|
|
printf 'Current hostname: %s\n' "$current_hostname"
|
|
new_hostname=""
|
|
if yes_no "Change the hostname?"; then
|
|
new_hostname="$(ask "Hostname" "$current_hostname")"
|
|
fi
|
|
record PANAMA_HOSTNAME "$new_hostname"
|
|
|
|
# ── Identity ─────────────────────────────────────────────────────────────────
|
|
#
|
|
# Left blank, each of these keeps whatever git already has. That matters on a
|
|
# re-run: the prompts start empty every time by design, and an empty answer must
|
|
# not wipe a name that was already correct.
|
|
|
|
heading "Git identity"
|
|
printf 'Leave any of these blank to keep the current setting.\n'
|
|
git_name="$(ask "Git user.name")"
|
|
git_email="$(ask "Git user.email")"
|
|
git_editor="$(ask "Git editor" "nvim")"
|
|
record PANAMA_GIT_NAME "$git_name"
|
|
record PANAMA_GIT_EMAIL "$git_email"
|
|
record PANAMA_GIT_EDITOR "$git_editor"
|
|
|
|
# ── Accounts and keys ────────────────────────────────────────────────────────
|
|
#
|
|
# These two are asked only when they would do something. Checking whether a
|
|
# credential already exists is not the same as remembering a previous answer --
|
|
# it is refusing to ask a question whose answer is already on the machine.
|
|
|
|
heading "Accounts"
|
|
gh_login=no
|
|
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
|
|
printf 'GitHub CLI is already signed in.\n'
|
|
elif yes_no "Sign in to GitHub after packages are installed?"; then
|
|
gh_login=yes
|
|
fi
|
|
record PANAMA_GH_LOGIN "$gh_login"
|
|
|
|
ssh_key=no
|
|
if compgen -G "$HOME/.ssh/id_*.pub" >/dev/null 2>&1; then
|
|
printf 'An SSH key already exists.\n'
|
|
elif yes_no "Generate an SSH key?"; then
|
|
ssh_key=yes
|
|
fi
|
|
record PANAMA_SSH_KEY "$ssh_key"
|
|
|
|
# ── Hardware ─────────────────────────────────────────────────────────────────
|
|
#
|
|
# Each question names what was detected, so declining is a decision about this
|
|
# machine rather than an answer to a hypothetical. A machine with no NVIDIA card
|
|
# is never asked about drivers, and one with nothing to remove is never asked
|
|
# about removing it.
|
|
|
|
heading "Hardware"
|
|
|
|
nvidia=no
|
|
mok_hash=""
|
|
nvidia_card="$(lspci 2>/dev/null | grep -iE 'vga compatible|3d controller' | grep -i nvidia | sed 's/.*: //' | head -1)"
|
|
|
|
if [[ -n "$nvidia_card" ]]; then
|
|
if yes_no "Found $nvidia_card — install the NVIDIA driver?"; then
|
|
nvidia=yes
|
|
|
|
# Only asked where it does something. On a machine with Secure Boot off,
|
|
# akmods' signature is never checked and enrolling a key is ceremony.
|
|
if mokutil --sb-state 2>/dev/null | grep -qi 'secureboot enabled'; then
|
|
printf 'Secure Boot is on, so the driver must be signed with a key you enroll.\n'
|
|
printf 'The next boot will ask for this password on a blue screen.\n'
|
|
if yes_no "Enroll a machine owner key?"; then
|
|
# Hashed here and only the hash recorded. The password never
|
|
# reaches the answers file, the environment, or a command line
|
|
# -- mokutil takes a hash file precisely so it does not have to.
|
|
while :; do
|
|
first="$(gum input --password --header "MOK password")"
|
|
if [[ -z "$first" ]]; then
|
|
printf 'No password given; skipping enrollment.\n'
|
|
break
|
|
fi
|
|
second="$(gum input --password --header "MOK password again")"
|
|
if [[ "$first" == "$second" ]]; then
|
|
# Fed on stdin, never as an argument: an argument sits
|
|
# in /proc/<pid>/cmdline for any local process to read
|
|
# while mokutil runs. mokutil prints its two prompts on
|
|
# stdout too, so the hash is the last line.
|
|
mok_hash="$(printf '%s\n%s\n' "$first" "$first" \
|
|
| mokutil --generate-hash | tail -n 1)"
|
|
break
|
|
fi
|
|
printf 'Those did not match.\n'
|
|
done
|
|
unset first second
|
|
fi
|
|
fi
|
|
fi
|
|
else
|
|
printf 'No NVIDIA card found.\n'
|
|
fi
|
|
record PANAMA_NVIDIA "$nvidia"
|
|
record PANAMA_MOK_HASH "$mok_hash"
|
|
|
|
# The stage that removes them owns the list, so there is one copy of it.
|
|
debloat=no
|
|
mapfile -t removable < <("$(dirname "${BASH_SOURCE[0]}")/install-hardware" --debloat-list)
|
|
installed=()
|
|
for package in "${removable[@]}"; do
|
|
rpm -q "$package" >/dev/null 2>&1 && installed+=("$package")
|
|
done
|
|
if (( ${#installed[@]} > 0 )); then
|
|
if yes_no "Remove Fedora's preinstalled extras (${installed[*]})?"; then
|
|
debloat=yes
|
|
fi
|
|
fi
|
|
record PANAMA_DEBLOAT "$debloat"
|
|
|
|
firmware=no
|
|
if command -v fwupdmgr >/dev/null 2>&1; then
|
|
if yes_no "Update firmware with fwupdmgr?"; then
|
|
firmware=yes
|
|
fi
|
|
fi
|
|
record PANAMA_FIRMWARE "$firmware"
|
|
|
|
# ── Applications ─────────────────────────────────────────────────────────────
|
|
#
|
|
# Everything else in this repository is what every Panama machine gets. This is
|
|
# the one question about what this machine is for: a work laptop should not
|
|
# acquire emulators and a desktop should not skip Steam.
|
|
#
|
|
# The categories are read from the directory rather than listed here, so adding
|
|
# one is adding a file. Nothing is preselected -- a default here would install
|
|
# applications nobody chose, on a machine whose owner answered a question they
|
|
# thought was about something else.
|
|
|
|
heading "Applications"
|
|
|
|
extras_dir="$(dirname "${BASH_SOURCE[0]}")/../packages/extras"
|
|
extras=""
|
|
if [[ -d "$extras_dir" ]]; then
|
|
mapfile -t categories < <(for file in "$extras_dir"/*; do
|
|
[[ -f "$file" ]] && basename "$file"
|
|
done)
|
|
if (( ${#categories[@]} > 0 )); then
|
|
printf 'Optional application categories. Space to select, enter to accept.\n'
|
|
extras="$(gum choose --no-limit --header "Extras" "${categories[@]}" | tr '\n' ' ')"
|
|
extras="${extras% }"
|
|
fi
|
|
fi
|
|
record PANAMA_EXTRAS "$extras"
|
|
|
|
# ── Personal content ─────────────────────────────────────────────────────────
|
|
#
|
|
# user/ holds whoever-owns-this-checkout's personal files: agent instructions,
|
|
# SSH host aliases, expansion triggers. Linking them is how one person keeps
|
|
# several machines identical, and it is exactly the wrong thing to do to
|
|
# somebody who just cloned this repository to try the desktop out.
|
|
#
|
|
# So it is asked rather than assumed, the question names what it would link,
|
|
# and no is the default. Someone who forks Panama replaces user/ with their own
|
|
# and starts answering yes.
|
|
|
|
user_content=no
|
|
if [[ -r "$(dirname "${BASH_SOURCE[0]}")/../../user/manifest" ]]; then
|
|
mapfile -t user_targets < <(
|
|
grep -vE '^\s*(#|$)' "$(dirname "${BASH_SOURCE[0]}")/../../user/manifest" \
|
|
| awk '{ print $3 }' | sort -u
|
|
)
|
|
if (( ${#user_targets[@]} > 0 )); then
|
|
printf 'This checkout carries personal content for: %s\n' "${user_targets[*]}"
|
|
printf 'Say no unless this checkout is yours.\n'
|
|
if yes_no "Link this checkout's personal content into your home?"; then
|
|
user_content=yes
|
|
fi
|
|
fi
|
|
fi
|
|
record PANAMA_USER_CONTENT "$user_content"
|
|
|
|
# ── Confirm ──────────────────────────────────────────────────────────────────
|
|
#
|
|
# The last chance to catch a typo before twenty minutes of package work that
|
|
# nobody is watching.
|
|
|
|
shown() { [[ -n "$1" ]] && printf '%s' "$1" || printf 'unchanged'; }
|
|
|
|
heading "Ready"
|
|
gum style --border rounded --padding "0 1" "$(
|
|
printf 'Hostname %s\n' "${new_hostname:-"$current_hostname (unchanged)"}"
|
|
printf 'Git name %s\n' "$(shown "$git_name")"
|
|
printf 'Git email %s\n' "$(shown "$git_email")"
|
|
printf 'Git editor %s\n' "$(shown "$git_editor")"
|
|
printf 'GitHub %s\n' "$([[ "$gh_login" == yes ]] && echo "sign in" || echo "no change")"
|
|
printf 'SSH key %s\n' "$([[ "$ssh_key" == yes ]] && echo "generate" || echo "no change")"
|
|
printf 'NVIDIA %s\n' "$([[ "$nvidia" == yes ]] && echo "install driver" || echo "no")"
|
|
printf 'Secure Boot %s\n' "$([[ -n "$mok_hash" ]] && echo "enroll a key" || echo "no change")"
|
|
printf 'Fedora apps %s\n' "$([[ "$debloat" == yes ]] && echo "remove ${installed[*]}" || echo "keep")"
|
|
printf 'Firmware %s\n' "$([[ "$firmware" == yes ]] && echo "update" || echo "no")"
|
|
printf 'Extras %s\n' "${extras:-none}"
|
|
printf 'Personal %s' "$([[ "$user_content" == yes ]] && echo "link user/ into home" || echo "not linked")"
|
|
)"
|
|
|
|
if ! gum confirm --default=true "Install with these answers?"; then
|
|
printf 'interview: cancelled; nothing was installed.\n' >&2
|
|
exit 1
|
|
fi
|