Files
Panama/setup/scripts/link-user
T
Gabriel Brown 89761a7da3 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.
2026-08-22 08:54:43 -04:00

114 lines
3.9 KiB
Bash
Executable File

#!/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."