[Update] bulk update
This commit is contained in:
+138
-22
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Deploy the unified dots for this machine using GNU Stow.
|
||||
#
|
||||
# ./install.sh [host] host defaults to $(hostname -s)
|
||||
# ./install.sh [host] host: the arg, else ~/.config/dots-host, else $(hostname -s);
|
||||
# the resolved host is recorded in ~/.config/dots-host
|
||||
# ./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
|
||||
@@ -28,13 +29,26 @@ set -uo pipefail
|
||||
# — run once on each tty-only box: touch ~/.config/dots-headless
|
||||
HEADLESS_MARKER="${XDG_CONFIG_HOME:-$HOME/.config}/dots-headless"
|
||||
|
||||
# --- which host package this machine deploys ---
|
||||
# Machine-local like the marker above, and deliberately NOT derived from `hostname -s`: several
|
||||
# boxes may deploy the same overlay without sharing a hostname, and a hostname that happens not
|
||||
# to match a package name would otherwise half-deploy (base layers restow, the overlay is
|
||||
# skipped). Resolution order:
|
||||
# the explicit arg -> this file -> `hostname -s` (a guess, not an invariant)
|
||||
# The resolved host is recorded here, so later bare runs — and `bin/dotsync`, which re-links
|
||||
# without being able to pass an argument — reuse it. Hand-editable: one word, e.g. `wm`.
|
||||
HOST_MARKER="${XDG_CONFIG_HOME:-$HOME/.config}/dots-host"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Deploy this machine's dotfiles with GNU Stow.
|
||||
|
||||
Usage: ./install.sh [options] [host]
|
||||
|
||||
host profile to deploy (default: `hostname -s`)
|
||||
host host package to deploy. Resolved as: this arg, else the name in
|
||||
~/.config/dots-host, else `hostname -s`. The resolved host is
|
||||
remembered in ~/.config/dots-host, so later runs (and dotsync,
|
||||
which cannot pass one) reuse it — the hostname need not match.
|
||||
-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 .`
|
||||
@@ -53,18 +67,47 @@ 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 ;;
|
||||
-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)}"
|
||||
# resolve the host package (see HOST_MARKER above). RECORDED is read BEFORE the marker is
|
||||
# rewritten below, so a profile switch still knows which overlay to release first.
|
||||
RECORDED=""
|
||||
[ -r "$HOST_MARKER" ] && RECORDED="$(head -1 "$HOST_MARKER" | tr -d '[:space:]')"
|
||||
HOST="${ARGS[0]:-${RECORDED:-$(hostname -s)}}"
|
||||
# an empty HOST must never reach stow: `stow -S ""` treats the repo ROOT as the package
|
||||
# and links common/, gui/, install.sh… straight into $HOME (and `-D ""` unlinks likewise)
|
||||
[ -n "$HOST" ] || {
|
||||
echo "error: no host resolved — pass one (./install.sh <host>) or write it to ~/.config/dots-host" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
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; }
|
||||
command -v stow >/dev/null 2>&1 || {
|
||||
echo "error: GNU stow is not installed (pacman -S stow)" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --adopt copies LIVE files over the repo working tree; uncommitted edits there would be
|
||||
# silently overwritten (and the post-adopt `git restore .` advice would discard whatever
|
||||
# survived). Refuse until the tree is clean, so nothing is lost without a git safety net.
|
||||
if [ -n "$ADOPT" ] && [ -z "$DRY" ] && [ -n "$(git -C "$DOTS" status --porcelain 2>/dev/null)" ]; then
|
||||
echo "error: -a/--adopt rewrites repo files with this machine's live copies, and the repo has" >&2
|
||||
echo " uncommitted changes that would be lost — commit or stash them first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gui=true
|
||||
[ -e "$HEADLESS_MARKER" ] && gui=false
|
||||
@@ -73,7 +116,8 @@ 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"
|
||||
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
|
||||
@@ -96,35 +140,76 @@ if [ "$UNSTOW" -eq 1 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# remember the resolved host, so bare re-runs — and dotsync, which re-links without being able to
|
||||
# pass an argument — deploy the same overlay on this machine whatever its hostname is. Only ever
|
||||
# records a name that IS a package (never a typo, never a hostname with no overlay behind it), and
|
||||
# records the *choice*, not the outcome: a run whose stow later fails is still remembered.
|
||||
if [ -z "$DRY" ] && [ "$HOST" != "$RECORDED" ] && [ -d "$DOTS/$HOST" ]; then
|
||||
printf '%s\n' "$HOST" >"$HOST_MARKER" && echo " recorded host '$HOST' in ${HOST_MARKER/#$HOME/~}"
|
||||
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:
|
||||
# common. Three stale-link classes a prior link.sh/heal/migration 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.
|
||||
# - a DANGLING link whose target is gone (e.g. a relative link into the deleted ~/.dotfiles) —
|
||||
# stow rejects it just the same, so drop it too.
|
||||
# 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"; }
|
||||
[ -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"
|
||||
if [ -L "$t" ] && [ ! -e "$t" ]; then
|
||||
rm -f "$t"
|
||||
echo " cleared dangling symlink ~/.config/claude/$f (stow will relink)"
|
||||
continue
|
||||
fi
|
||||
case "$(readlink "$t" 2>/dev/null)" in
|
||||
/*) rm -f "$t"; echo " cleared absolute symlink ~/.config/claude/$f (stow will relink)" ;;
|
||||
/*)
|
||||
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
|
||||
# --- keep a live Hyprland session quiet while symlinks are removed/recreated ---
|
||||
# Step 1 releases the host overlay before restowing, and Hyprland's config watcher reloads
|
||||
# the moment a sourced file vanishes (monitors.conf/host.conf, hyprland.conf:58/60 — the two
|
||||
# that come from the overlay): that parse aborts and the session sits on a partial config
|
||||
# until a manual Super+Shift+R. So pause the watcher and reload once at the end, when every
|
||||
# link is back. The final reload also re-reads misc:disable_autoreload from the config,
|
||||
# undoing the runtime keyword — no state is left behind.
|
||||
if [ -z "$DRY" ] && [ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ] && command -v hyprctl >/dev/null 2>&1; then
|
||||
hyprctl keyword misc:disable_autoreload true >/dev/null 2>&1
|
||||
trap 'hyprctl reload >/dev/null 2>&1' EXIT
|
||||
fi
|
||||
|
||||
# 1. release the host overlay first so base restows never conflict on overridden files.
|
||||
# The PREVIOUSLY recorded host is released too, so switching a machine to another overlay
|
||||
# works: its leftover links are "stowed to a different package" as far as the step-2 restows
|
||||
# are concerned, which would abort those whole packages. Releasing an already-released
|
||||
# package is a harmless no-op, so the repeat when nothing changed costs nothing.
|
||||
if [ -z "$DRY" ]; then
|
||||
for p in "$HOST" "$RECORDED"; do
|
||||
[ -n "$p" ] && [ -d "$DOTS/$p" ] && stow -d "$DOTS" -t "$HOME" -D "$p" 2>/dev/null
|
||||
done
|
||||
true # a release that found nothing to do must not fail the run
|
||||
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
|
||||
local label="$1"
|
||||
shift
|
||||
if stow $DRY $ADOPT --no-folding -d "$DOTS" -t "$HOME" "$@"; then
|
||||
echo " stowed $label"
|
||||
else
|
||||
@@ -160,18 +245,49 @@ 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)"
|
||||
if [ "$HOST" != "${ARGS[0]:-}" ] && [ "$HOST" != "$RECORDED" ]; then
|
||||
echo " ('$HOST' is only this machine's hostname — if it should deploy a named host package," >&2
|
||||
echo " run ./install.sh <name> once; that choice is remembered and hostname stops mattering)" >&2
|
||||
fi
|
||||
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"
|
||||
# 3.5 seed waybar's theme.css where no generator has ever written one. layouts/custom.css
|
||||
# does `@import "../theme.css"` and a missing import target kills waybar outright (bar dead
|
||||
# at login, Ctrl+Alt+W looks kill-only). On themed hosts HyDE/wallbash owns the real file —
|
||||
# [ ! -e ] never touches those; it fires only when the file is absent or the pre-2026-08-14
|
||||
# stow link dangles (-e follows symlinks), covering fresh machines and never-themed ones.
|
||||
if [ "$gui" = true ]; then
|
||||
wbtheme="$HOME/.config/waybar/theme.css"
|
||||
if [ ! -e "$wbtheme" ]; then
|
||||
if [ -n "$DRY" ]; then
|
||||
echo " would seed waybar theme.css from gui/.config/waybar/theme-fallback.css"
|
||||
else
|
||||
mkdir -p "${wbtheme%/*}"
|
||||
cp --remove-destination "$DOTS/gui/.config/waybar/theme-fallback.css" "$wbtheme" \
|
||||
&& echo " seeded waybar theme.css from fallback (wallbash overwrites it when HyDE themes run)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4. surface Claude config into ~/.claude. Deliberately NOT gated on stow success: link.sh's
|
||||
# drift heal is what clears the usual stow failure (a ~/.config/claude file severed by one
|
||||
# of Claude's atomic rewrites), so running it only after a clean stow made that failure
|
||||
# permanent. Its exit matters too — swallowing it reported "linked" over a refused heal.
|
||||
if [ -z "$DRY" ] && [ -f "$HOME/.config/claude/link.sh" ]; then
|
||||
if bash "$HOME/.config/claude/link.sh"; then
|
||||
echo " linked ~/.claude"
|
||||
else
|
||||
FAIL=1
|
||||
echo " link.sh failed (see above) — ~/.claude not fully linked" >&2
|
||||
fi
|
||||
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
|
||||
echo " first run on a machine with existing real files? re-run with -a to adopt them" >&2
|
||||
echo " (-a rewrites the repo working tree with the live copies — it refuses to run while" >&2
|
||||
echo " the repo has uncommitted edits, so commit/stash first)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user