#!/usr/bin/env bash # One-shot, host-aware dotfile linker for Linux & macOS. # # home_root/* -> ~/ (e.g. ~/.zshrc) via `stow home_root` # home_config/* -> ~/.config/ (e.g. ~/.config/nvim) via `stow home_config` # # Ensures GNU stow is present first (Homebrew on macOS; apt/dnf/pacman on Linux), # so a fresh box needs nothing pre-installed. Re-running is safe — stow is # idempotent and refuses to clobber unrelated real files. # # Windows / PowerShell: run ./link-dotfiles.ps1 instead (stow has no native # Windows port; the .ps1 mirrors this with junctions + symlinks). # # `--check`: report drift and change nothing. Two kinds are caught. (1) A tracked # file whose live copy is no longer a link into this repo: stow would refuse to # stow over it, so the repo copy has silently gone stale. `sed -i` and any other # write-temp-then-rename editor does this, and so can an app that rewrites its # own config atomically. (2) A link into this repo whose target has been removed. # Exits 1 on drift. set -euo pipefail repo="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$repo" if [ "${1:-}" = "--check" ]; then command -v stow >/dev/null 2>&1 || { echo "error: GNU stow not installed; run ./link-dotfiles once first." >&2; exit 2; } drift=0 for spec in "home_root:$HOME" "home_config:$HOME/.config"; do pkg="${spec%%:*}"; target="${spec#*:}" out="$(stow -n -v --target="$target" "$pkg" 2>&1 | grep -v 'simulation mode' || true)" if [ -n "$out" ]; then drift=1 echo "✗ $pkg -> $target is not fully linked:" printf '%s\n' "$out" | sed 's/^/ /' fi done dangling="$(find "$HOME" "$HOME/.config" "$HOME/.claude" "$HOME/.claude/bin" "$HOME/.claude/skills" "$HOME/.local/bin" \ -maxdepth 1 -xtype l -lname "*dotfiles/*" 2>/dev/null || true)" if [ -n "$dangling" ]; then drift=1 echo "✗ links into dotfiles whose target is gone:" printf '%s\n' "$dangling" | sed 's/^/ /' fi # A link can stay intact while its content moves: an app that holds a stale copy # in memory (a long-running Claude Code session with settings.json) writes it # straight back through the link. That shows up only as an uncommitted change. dirty="$(git -C "$repo" status --porcelain -- home_root home_config 2>/dev/null || true)" if [ -n "$dirty" ]; then drift=1 echo "✗ live config differs from the last commit (not backed up yet):" printf '%s\n' "$dirty" | sed 's/^/ /' fi # Auto-memory backup (claude-memory-sync), checked only on machines that have it. if [ -d "$HOME/claude-memory.git" ]; then last="$HOME/.claude/state/claude-memory-sync.last" if [ ! -f "$last" ]; then drift=1; echo "✗ claude-memory-sync has never completed a run on this machine" else read -r ts result detail < "$last" age=$(( ($(date +%s) - ts) / 60 )) if [ "$result" != ok ] || [ "$age" -gt 180 ]; then drift=1; echo "✗ claude-memory-sync: last run ${age} min ago — $result${detail:+ ($detail)}" fi fi fi [ "$drift" -eq 0 ] && echo "✓ no drift: every tracked file is linked and committed, no dotfiles link dangles, auto-memory backup is current." exit "$drift" fi # `--apps`: install all host CLI tools first, then link. Without it, this only # links (fast + idempotent). App install is delegated to the host-aware # lk-installers/install-apps. if [ "${1:-}" = "--apps" ]; then echo "Installing apps first (--apps)…" "$repo/lk-installers/install-apps" fi ensure_stow() { command -v stow >/dev/null 2>&1 && return echo "GNU stow not found — installing it…" case "$(uname -s)" in Darwin) if ! command -v brew >/dev/null 2>&1; then echo "error: Homebrew is required on macOS. Run ./lk-installers/install-apps (or ./link-dotfiles --apps) first." >&2 exit 1 fi brew install stow ;; Linux) if command -v apt >/dev/null 2>&1; then sudo apt update && sudo apt install -y stow elif command -v dnf >/dev/null 2>&1; then sudo dnf install -y stow elif command -v pacman >/dev/null 2>&1; then sudo pacman -S --noconfirm stow else echo "error: no supported package manager (apt/dnf/pacman). Install GNU stow manually." >&2 exit 1 fi ;; *) echo "error: unsupported OS '$(uname -s)'. On Windows run ./link-dotfiles.ps1 instead." >&2 exit 1 ;; esac } ensure_stow echo "Linking home_root -> ~" stow -v home_root echo "Linking home_config -> ~/.config" stow -v --target="$HOME/.config" home_config echo "Done."