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.
114 lines
3.9 KiB
Bash
Executable File
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."
|