Files

139 lines
5.0 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 ))
;;
linkdir)
# A destination that has to hold more than this one source. The
# only one today is ~/.claude/skills, which now carries Panama's
# own shipped skills as well as these -- so the directory itself
# cannot be a symlink, and each child is linked into it instead.
#
# Whatever link-skills put there is kept; only same-named entries
# are displaced, which is how a personal skill deliberately
# shadows a shipped one. This stage runs last for that reason.
if [[ -L "$dst" ]]; then
rm -f "$dst"
elif [[ -e "$dst" && ! -d "$dst" ]]; then
displace "$dst"
fi
mkdir -p "$dst"
for child in "$src"/*; do
[[ -e "$child" ]] || continue
child_name="$(basename "$child")"
displace "$dst/$child_name"
ln -s "$child" "$dst/$child_name"
log "Linked $source/$child_name$dst/$child_name"
linked=$(( linked + 1 ))
done
;;
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."