Keep the personal half of the desktop in one place, and ask before installing it

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.
This commit is contained in:
Gabriel Brown
2026-08-22 08:54:43 -04:00
parent 8b96d907a1
commit 89761a7da3
156 changed files with 16439 additions and 6 deletions
+29 -1
View File
@@ -191,6 +191,33 @@ if [[ -d "$extras_dir" ]]; then
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
@@ -210,7 +237,8 @@ gum style --border rounded --padding "0 1" "$(
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' "${extras:-none}"
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
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
# Personal content: the things that should be identical on every machine one
# person owns.
#
# Panama is meant to be installable by anybody, and it is also somebody's
# actual dotfiles. Those two goals only conflict if the personal half is mixed
# into the shared half, so it lives in one directory with one manifest, and
# this stage links it -- but only on a machine that said yes.
#
# A stranger who clones Panama gets user/ in their checkout and nothing linked
# from it. Their own content replaces it, or they delete it; either way their
# agent instructions are their own and their skills are their own. That is what
# makes tracking somebody's personal files in a public repository defensible.
#
# The answer comes from the interview as PANAMA_USER_CONTENT. Running this
# stage by hand outside an install honours the recorded answer instead, so
# `panama upgrade` on an already-configured machine does not need re-asking.
set -euo pipefail
log() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
USER_DIR="$PANAMA_PATH/user"
MANIFEST="$USER_DIR/manifest"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
DECISION="$STATE_DIR/user-content"
PANAMA_OLD="$PANAMA_PATH/config/old"
[[ -r "$MANIFEST" ]] || { log "No personal content manifest; nothing to link."; exit 0; }
# The interview's answer wins when there is one, and is remembered so a later
# run without it behaves the same way. A machine that has never been asked and
# is not being asked now links nothing, which is the safe direction: the cost
# of guessing yes is somebody else's agent instructions on your machine.
mkdir -p "$STATE_DIR"
if [[ -n "${PANAMA_USER_CONTENT:-}" ]]; then
printf '%s\n' "$PANAMA_USER_CONTENT" >"$DECISION"
fi
decision="$( [[ -r "$DECISION" ]] && cat "$DECISION" || printf 'no' )"
if [[ "$decision" != "yes" ]]; then
log "Personal content is not enabled on this machine; nothing linked."
log "Enable it by re-running ./install and answering yes, or: echo yes > $DECISION"
exit 0
fi
mkdir -p "$PANAMA_OLD"
# Moves whatever is already at a destination out of the way, once. A real file
# somebody has is never deleted: it goes to config/old/ under a name that says
# where it came from, which is the same promise link-dotfiles makes.
displace() {
local destination="$1" backup
if [[ -L "$destination" ]]; then
rm -f "$destination"
return 0
fi
[[ -e "$destination" ]] || return 0
backup="$PANAMA_OLD/user-$(printf '%s' "${destination#"$HOME"/}" | tr '/' '-')"
if [[ -e "$backup" ]]; then
backup="$backup.$(date +%s)"
fi
mv "$destination" "$backup"
log "Moved existing $destination to $backup"
}
linked=0
copied=0
while read -r kind source destination; do
[[ -n "${kind:-}" ]] || continue
[[ "$kind" == \#* ]] && continue
src="$USER_DIR/$source"
dst="${destination/#\~/$HOME}"
if [[ ! -e "$src" ]]; then
log "Skipping $source: it is not in user/"
continue
fi
parent="$(dirname "$dst")"
mkdir -p "$parent"
# ssh refuses to read a config out of a directory anyone else can write,
# and the default umask here would have created one.
[[ "$parent" == "$HOME/.ssh" ]] && chmod 700 "$parent"
case "$kind" in
link)
displace "$dst"
ln -s "$src" "$dst"
log "Linked $source → $dst"
linked=$(( linked + 1 ))
;;
copy)
if [[ -e "$dst" ]]; then
log "Keeping existing $dst"
else
cp -r "$src" "$dst"
log "Copied $source → $dst"
copied=$(( copied + 1 ))
fi
;;
*)
log "Skipping unknown manifest kind: $kind"
;;
esac
done < <(grep -vE '^\s*(#|$)' "$MANIFEST")
log "Personal content: $linked linked, $copied copied."