139 lines
5.0 KiB
Bash
Executable File
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."
|