Supersedes the old flat .config/ layout (last published 2026-06-28) with the private repo's structure: one shared base plus per-host overlays. - packages: common/ gui/ lw/ fl/ wm/ plus install.sh and bin/ tooling (dotsync, reconcile-hyde.sh) - new README covering the layout, deploy order and the HyDE dependency - current HyDE waybar rig (layouts/, cava), pi agent extensions, claude/ config, tmux, presenterm, aichat roles - fish: kp (keepassxc-cli + fzf picker, db path from $KP_DB) and bind_M_n_history (alt+1..9 recalls the nth history entry) - drops cruft that should never have been tracked: the duplicate top-level .pi/ copy, btop.log, zellij config.kdl.bak, fish_variables - .pi/agent/auth.json is gitignored; auth.json.example ships instead Host-specific work sessions and the personal backlog stay in the private tree. Endpoint locators in the llamacpp/whisper guides are placeholders ($SERVER, <own-domain>) — the guides themselves stay, since they are the useful part.
190 lines
7.9 KiB
Bash
Executable File
190 lines
7.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Deploy the unified dots for this machine using GNU Stow.
|
|
#
|
|
# ./install.sh [host] host defaults to $(hostname -s)
|
|
# ./install.sh -n [host] dry-run (preview, no changes)
|
|
# ./install.sh -a [host] first run on a machine that still has real config files:
|
|
# --adopt them into the repo, then review `git diff` and
|
|
# `git restore .` to keep the repo (merged) versions.
|
|
# ./install.sh -D [host] revert: unstow this host's symlinks (repo + real files untouched)
|
|
# ./install.sh -h show all options
|
|
#
|
|
# Layering (later wins): common -> gui -> <host>
|
|
# - common : shared everywhere
|
|
# - gui : GUI configs (skipped on headless machines — see the marker below)
|
|
# - <host> : per-machine overrides (top-level package), stowed last with --override
|
|
# All packages live at the repo root so they share one stow dir — required for
|
|
# --override to let a host file take over a common/gui file of the same path.
|
|
# --no-folding: every file is symlinked individually (dirs stay real) so a host can
|
|
# override a single file in a shared dir without shadowing the rest.
|
|
#
|
|
# Idempotent: re-run any time. Adding/removing files in the repo needs a re-run
|
|
# (editing an existing file does not — symlinks point straight at the repo file).
|
|
set -uo pipefail
|
|
|
|
# --- headless (terminal-only, no GUI) machines skip the gui package ---
|
|
# A machine is "headless" if this marker file exists. It's machine-local (not tracked by the repo)
|
|
# and hostname-independent, so the same terminal-only setup replicates across any number of machines
|
|
# — run once on each tty-only box: touch ~/.config/dots-headless
|
|
HEADLESS_MARKER="${XDG_CONFIG_HOME:-$HOME/.config}/dots-headless"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Deploy this machine's dotfiles with GNU Stow.
|
|
|
|
Usage: ./install.sh [options] [host]
|
|
|
|
host profile to deploy (default: `hostname -s`)
|
|
-n, --dry-run preview stow actions; change nothing
|
|
-a, --adopt first run on a machine with existing real config files: adopt
|
|
them into the repo, then review `git diff` and `git restore .`
|
|
-D, --unstow revert: remove this host's stow symlinks (repo + real files kept)
|
|
-h, --help show this help and exit
|
|
|
|
Layering (later wins): common -> gui -> <host>. A machine with ~/.config/dots-headless
|
|
skips gui (and has any stray gui unstowed). Run from inside the repo (~/.dots).
|
|
EOF
|
|
}
|
|
|
|
# --- args ---
|
|
DRY=""
|
|
ADOPT=""
|
|
UNSTOW=0
|
|
ARGS=()
|
|
for a in "$@"; do
|
|
case "$a" in
|
|
-n|--dry-run) DRY="-n" ;;
|
|
-a|--adopt) ADOPT="--adopt" ;;
|
|
-D|--unstow|--delete) UNSTOW=1 ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
-*) echo "unknown option: $a" >&2; usage >&2; exit 2 ;;
|
|
*) ARGS+=("$a") ;;
|
|
esac
|
|
done
|
|
HOST="${ARGS[0]:-$(hostname -s)}"
|
|
|
|
DOTS="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
|
command -v stow >/dev/null 2>&1 || { echo "error: GNU stow is not installed (pacman -S stow)" >&2; exit 1; }
|
|
|
|
gui=true
|
|
[ -e "$HEADLESS_MARKER" ] && gui=false
|
|
|
|
echo "host=$HOST dots=$DOTS gui=$gui${DRY:+ (dry-run)}"
|
|
|
|
# --- revert mode: unstow this host's packages (reverse of deploy), leave repo + real files intact ---
|
|
if [ "$UNSTOW" -eq 1 ]; then
|
|
verb="unstowed"; [ -n "$DRY" ] && verb="would unstow"
|
|
for p in "$HOST" gui common; do
|
|
[ -d "$DOTS/$p" ] || continue
|
|
if stow $DRY -d "$DOTS" -t "$HOME" -D "$p" 2>/dev/null; then
|
|
echo " $verb $p"
|
|
fi
|
|
# prune the now-empty dirs this package created under ~/.config (empty only — never real files)
|
|
if [ -z "$DRY" ] && [ -d "$DOTS/$p/.config" ]; then
|
|
for d in "$DOTS/$p"/.config/*/; do
|
|
t="$HOME/.config/$(basename "$d")"
|
|
[ -d "$t" ] && find "$t" -type d -empty -delete 2>/dev/null || true
|
|
done
|
|
fi
|
|
done
|
|
if [ -n "$DRY" ]; then
|
|
echo "(dry-run) nothing changed."
|
|
else
|
|
echo "reverted — removed this host's stow symlinks. ~/.dots and any real files are untouched."
|
|
echo "note: ~/.claude/* links (from link.sh) are separate; remove by hand if you want those gone too."
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# 0. self-heal symlinks that stow would reject ("not owned by stow"), which otherwise aborts all of
|
|
# common. Two stale-link classes a prior link.sh/heal can leave behind:
|
|
# - hooks/skills folded into an ABSOLUTE dir-symlink (they must be real dirs) — drop it entirely.
|
|
# - a file item re-linked with an ABSOLUTE target instead of stow's relative one — drop it.
|
|
# Only ever removes a *symlink* here (never a real file/dir), so nothing is lost; stow then
|
|
# recreates the correct relative link. A drifted *real* file is left for link.sh's drift guard
|
|
# (or `-a` to adopt) to reconcile, since it may hold un-synced edits.
|
|
if [ -z "$DRY" ]; then
|
|
for d in hooks skills; do
|
|
t="$HOME/.config/claude/$d"
|
|
[ -L "$t" ] && { rm -f "$t"; echo " cleared stale folded symlink ~/.config/claude/$d"; }
|
|
done
|
|
for f in settings.json statusline.py keybindings.json CLAUDE.md; do
|
|
t="$HOME/.config/claude/$f"
|
|
case "$(readlink "$t" 2>/dev/null)" in
|
|
/*) rm -f "$t"; echo " cleared absolute symlink ~/.config/claude/$f (stow will relink)" ;;
|
|
esac
|
|
done
|
|
fi
|
|
|
|
# 1. release the host overlay first so base restows never conflict on overridden files
|
|
if [ -d "$DOTS/$HOST" ] && [ -z "$DRY" ]; then
|
|
stow -d "$DOTS" -t "$HOME" -D "$HOST" 2>/dev/null || true
|
|
fi
|
|
|
|
# stow one package; track failures instead of silently continuing and reporting success
|
|
FAIL=0
|
|
stow_pkg() { # stow_pkg <label> <stow args...>
|
|
local label="$1"; shift
|
|
if stow $DRY $ADOPT --no-folding -d "$DOTS" -t "$HOME" "$@"; then
|
|
echo " stowed $label"
|
|
else
|
|
FAIL=1
|
|
fi
|
|
}
|
|
|
|
# 2. (re)stow base layers
|
|
# In dry-run the host overlay is NOT released first (step 1 is real-run-only), so simulating
|
|
# a base restow would report conflicts on host-overridden files that a real run never hits.
|
|
# --override in the simulation makes its outcome (exit status) match the real sequence.
|
|
DRYOVR=""
|
|
[ -n "$DRY" ] && DRYOVR="--override=.*"
|
|
stow_pkg common $DRYOVR -R common
|
|
if [ "$gui" = true ]; then
|
|
stow_pkg gui $DRYOVR -R gui
|
|
else
|
|
# headless: ensure no stray gui is left deployed (self-heals a prior wrong run)
|
|
if stow $DRY -d "$DOTS" -t "$HOME" -D gui 2>/dev/null; then
|
|
test -z "$DRY" && echo " ensured gui not deployed (headless)"
|
|
fi
|
|
# prune the now-empty dirs that unstowing gui leaves behind (only removes empty ones)
|
|
if [ -z "$DRY" ] && [ -d "$DOTS/gui/.config" ]; then
|
|
for d in "$DOTS"/gui/.config/*/; do
|
|
t="$HOME/.config/$(basename "$d")"
|
|
[ -d "$t" ] && find "$t" -type d -empty -delete 2>/dev/null || true
|
|
done
|
|
fi
|
|
fi
|
|
|
|
# 3. stow the host overlay last, overriding base files
|
|
if [ -d "$DOTS/$HOST" ]; then
|
|
stow_pkg "$HOST" --override='.*' -S "$HOST"
|
|
else
|
|
echo " no '$HOST' overlay — using common only (add a top-level '$HOST/' package for host overrides)"
|
|
fi
|
|
|
|
# 4. surface Claude config into ~/.claude (only if everything stowed cleanly)
|
|
if [ "$FAIL" -eq 0 ] && [ -z "$DRY" ] && [ -f "$HOME/.config/claude/link.sh" ]; then
|
|
bash "$HOME/.config/claude/link.sh"
|
|
echo " linked ~/.claude"
|
|
fi
|
|
|
|
# fail loudly with a non-zero exit so callers / re-runs can tell it did not apply
|
|
if [ "$FAIL" -ne 0 ]; then
|
|
echo "FAILED: some packages could not be stowed (conflicts above)." >&2
|
|
echo " first run on a machine with existing real files? re-run with -a to adopt them." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "done."
|
|
|
|
if [ -n "$ADOPT" ] && [ -z "$DRY" ]; then
|
|
echo
|
|
echo "adopted this machine's existing files INTO the repo working tree — heads up: they can appear"
|
|
echo "under common/ or gui/ even when they are really host-specific. Review, then normally discard:"
|
|
echo " git -C \"$DOTS\" diff # inspect the adopted drift"
|
|
echo " git -C \"$DOTS\" restore . # discard it, keep the merged repo versions (recommended)"
|
|
echo "To KEEP a change, hand-copy it into the right package (common/gui/$HOST) — do not blanket-commit"
|
|
echo "the adopted diff, or host-specific content leaks into shared packages. (restore . drops ALL"
|
|
echo "uncommitted edits — fine on a fresh clone.)"
|
|
fi
|