The committed ~/.local/bin/claude symlink targets a specific Claude Code version path (whatever was current when this snapshot was taken). On any host that has Claude Code installed at a different version, that symlink arrives dangling. Old behavior: just delete the dangling link and rely on the Claude installer to recreate it. That doesn't help when the host has Claude installed but at a version other than the one in the snapshot, and we also support skipping the installer via DRUNKENDOTFILES_SKIP_CLAUDE=1 (used when the binary is already on disk but at a different version). New behavior: when the deployed symlink is dangling, scan ~/.local/share/claude/versions/ and repoint the symlink to the highest installed version (semver sort). Only fall back to deleting the link if no version is installed at all.
225 lines
7.7 KiB
Bash
Executable File
225 lines
7.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# drunkendotfiles installer
|
|
#
|
|
# Usage:
|
|
# curl -fsSL https://gitea.dlw.la/dissimulo/drunkendotfiles/raw/branch/drunkendotfiles/install.sh | bash
|
|
#
|
|
# Or clone first and run locally:
|
|
# git clone https://gitea.dlw.la/dissimulo/drunkendotfiles.git ~/.yadr && ~/.yadr/install.sh
|
|
#
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_URL="${DRUNKENDOTFILES_REPO:-https://gitea.dlw.la/dissimulo/drunkendotfiles.git}"
|
|
YADR_DIR="$HOME/.yadr"
|
|
BACKUP_DIR="$HOME/.drunkendotfiles.bak.$(date +%Y%m%d-%H%M%S)"
|
|
|
|
# Files at the repo root (relative to $YADR_DIR) that get placed at $HOME/<file>.
|
|
# Includes real files, relative symlinks into .yadr, and .backup copies.
|
|
PERSONAL_FILES=(
|
|
.bash_logout .bashrc .fehbg .gopherrc .jigdo-lite .nanorc .profile
|
|
.rainbow_config.json .selected_editor .tmux.conf .weatherspect .xmodmap
|
|
.xscreensaver .xsnowrc
|
|
.zlogin.backup .zlogout.backup .zpreztorc.backup .zprofile.backup
|
|
.zshenv.backup .zshrc.backup
|
|
# Relative symlinks into .yadr/... — YADR's rake install creates equivalents,
|
|
# but we deploy these too so things work even if rake install is skipped.
|
|
.aprc .ctags .editrc .escaped_colors.rb .gemrc .gitconfig .inputrc .pryrc
|
|
.rdebugrc .unescaped_colors.rb .vim .vimrc .zlogin .zlogout .zpreztorc
|
|
.zprofile .zshenv .zshrc
|
|
)
|
|
|
|
# Directories whose contents are merged into $HOME/<dir>/, file by file. Files
|
|
# the repo provides replace conflicting on-disk versions (with the displaced
|
|
# version preserved in $BACKUP_DIR); files outside the repo's tree are left
|
|
# untouched. This deliberately avoids wholesale-replacing $HOME/.local etc.,
|
|
# which would sweep away unrelated user data (fonts, app state, ...).
|
|
PERSONAL_DIRS=(
|
|
.fonts .irssi .nano .themes .local .mplayer
|
|
)
|
|
|
|
have() { command -v "$1" >/dev/null 2>&1; }
|
|
|
|
log() { printf '==> %s\n' "$*"; }
|
|
info() { printf ' %s\n' "$*"; }
|
|
warn() { printf '!! %s\n' "$*" >&2; }
|
|
|
|
require() {
|
|
have "$1" || { warn "missing required command: $1"; exit 1; }
|
|
}
|
|
|
|
require git
|
|
|
|
log "drunkendotfiles installer"
|
|
log "repo: $REPO_URL"
|
|
log "target: $YADR_DIR"
|
|
log "backup: $BACKUP_DIR (created on first collision)"
|
|
echo
|
|
|
|
# 1. Fetch the repo into $HOME/.yadr
|
|
if [ -d "$YADR_DIR/.git" ]; then
|
|
log "Repo already cloned — pulling latest"
|
|
git -C "$YADR_DIR" pull --ff-only
|
|
elif [ -d "$YADR_DIR" ]; then
|
|
warn "$YADR_DIR exists but isn't a git repo. Refusing to overwrite."
|
|
exit 1
|
|
else
|
|
log "Cloning drunkendotfiles"
|
|
git clone --recurse-submodules "$REPO_URL" "$YADR_DIR"
|
|
fi
|
|
|
|
cd "$YADR_DIR"
|
|
|
|
# 1a. Ensure submodules are initialized (Prezto lives in zsh/prezto as a
|
|
# submodule — without it, ~/.zshrc fails with "no such file or directory:
|
|
# ~/.zprezto/runcoms/zshrc"). Safe to run on fresh and existing clones alike.
|
|
log "Ensuring submodules are initialized"
|
|
git submodule update --init --recursive
|
|
|
|
# 2. Run YADR's native install (Vim plugins, prezto, etc.)
|
|
if have rake; then
|
|
log "Running YADR rake install"
|
|
[ "${1:-}" = "ask" ] && export ASK="true"
|
|
rake install || warn "rake install returned non-zero; continuing"
|
|
else
|
|
warn "rake not found — skipping YADR rake install. Install ruby+rake and re-run this script to finish YADR setup."
|
|
fi
|
|
|
|
# 3. Deploy personal dotfiles
|
|
|
|
# Move $HOME/<rel> aside into $BACKUP_DIR, preserving its sub-path.
|
|
backup_one_path() {
|
|
local rel="$1"
|
|
local dst="$HOME/$rel"
|
|
if [ -e "$dst" ] || [ -L "$dst" ]; then
|
|
mkdir -p "$BACKUP_DIR/$(dirname "$rel")"
|
|
mv "$dst" "$BACKUP_DIR/$rel"
|
|
fi
|
|
}
|
|
|
|
# Returns 0 iff $1 and $2 are equivalent (same symlink target, or identical
|
|
# regular-file content). Used to make re-runs idempotent: unchanged files get
|
|
# skipped instead of needlessly backed up.
|
|
paths_equivalent() {
|
|
local a="$1" b="$2"
|
|
if [ -L "$a" ] && [ -L "$b" ]; then
|
|
[ "$(readlink "$a")" = "$(readlink "$b")" ]
|
|
elif [ -L "$a" ] || [ -L "$b" ]; then
|
|
return 1
|
|
elif [ -f "$a" ] && [ -f "$b" ]; then
|
|
cmp -s "$a" "$b"
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Deploy one file or symlink at $YADR_DIR/<rel> to $HOME/<rel>. If a different
|
|
# file/symlink is already at the destination, it's moved into $BACKUP_DIR first.
|
|
deploy_file() {
|
|
local rel="$1"
|
|
local src="$YADR_DIR/$rel"
|
|
local dst="$HOME/$rel"
|
|
if [ ! -e "$src" ] && [ ! -L "$src" ]; then
|
|
return
|
|
fi
|
|
if paths_equivalent "$src" "$dst"; then
|
|
return
|
|
fi
|
|
backup_one_path "$rel"
|
|
mkdir -p "$(dirname "$dst")"
|
|
cp -a "$src" "$dst"
|
|
info "$rel"
|
|
}
|
|
|
|
# Deploy a directory by walking its tree and deploying each file/symlink in
|
|
# turn. Directories get mkdir'd; existing entries inside $HOME/<rel> that the
|
|
# repo doesn't know about are left alone. This is the merge semantics that
|
|
# protects e.g. $HOME/.local/share/fonts from being clobbered when the repo
|
|
# only tracks $HOME/.local/bin.
|
|
deploy_dir() {
|
|
local rel="$1"
|
|
local src="$YADR_DIR/$rel"
|
|
if [ ! -d "$src" ] || [ -L "$src" ]; then
|
|
return
|
|
fi
|
|
mkdir -p "$HOME/$rel"
|
|
local sub sub_rel src_path
|
|
while IFS= read -r -d '' sub; do
|
|
sub="${sub#./}"
|
|
[ "$sub" = "." ] && continue
|
|
sub_rel="$rel/$sub"
|
|
src_path="$YADR_DIR/$sub_rel"
|
|
if [ -d "$src_path" ] && [ ! -L "$src_path" ]; then
|
|
mkdir -p "$HOME/$sub_rel"
|
|
continue
|
|
fi
|
|
deploy_file "$sub_rel"
|
|
done < <(cd "$src" && find . -mindepth 1 -print0)
|
|
}
|
|
|
|
log "Deploying personal dotfiles"
|
|
for f in "${PERSONAL_FILES[@]}"; do deploy_file "$f"; done
|
|
for d in "${PERSONAL_DIRS[@]}"; do deploy_dir "$d"; done
|
|
|
|
# .local/bin/claude is a relative symlink to ../share/claude/versions/<version>.
|
|
# The version baked into the repo snapshot is whatever was current when this
|
|
# was last committed; on a fresh host that exact version probably isn't
|
|
# installed. Resolve as follows:
|
|
# 1. If the deployed symlink already resolves, leave it alone.
|
|
# 2. Else, if any other Claude version is present under
|
|
# ~/.local/share/claude/versions/, repoint the symlink to the highest
|
|
# one (semver sort) so users get the latest installed binary.
|
|
# 3. Else, drop the dangling link and let Claude Code's own installer
|
|
# (next step) recreate it.
|
|
claude_link="$HOME/.local/bin/claude"
|
|
claude_versions_dir="$HOME/.local/share/claude/versions"
|
|
if [ -L "$claude_link" ] && [ ! -e "$claude_link" ]; then
|
|
latest_claude=""
|
|
if [ -d "$claude_versions_dir" ]; then
|
|
latest_claude="$(ls -1 "$claude_versions_dir" 2>/dev/null | sort -V | tail -1)"
|
|
fi
|
|
if [ -n "$latest_claude" ] && [ -e "$claude_versions_dir/$latest_claude" ]; then
|
|
ln -sfn "../share/claude/versions/$latest_claude" "$claude_link"
|
|
info "Repointed ~/.local/bin/claude -> $latest_claude (latest installed)"
|
|
else
|
|
rm "$claude_link"
|
|
fi
|
|
fi
|
|
|
|
# 4. Install Claude Code (Anthropic's CLI). Skip if already installed or
|
|
# explicitly opted out via DRUNKENDOTFILES_SKIP_CLAUDE=1.
|
|
if [ "${DRUNKENDOTFILES_SKIP_CLAUDE:-0}" = "1" ]; then
|
|
info "Skipping Claude Code install (DRUNKENDOTFILES_SKIP_CLAUDE=1)"
|
|
elif have claude || [ -x "$HOME/.local/bin/claude" ]; then
|
|
info "Claude Code already installed ($(claude --version 2>/dev/null || echo 'version unknown'))"
|
|
else
|
|
log "Installing Claude Code"
|
|
if have curl; then
|
|
curl -fsSL https://claude.ai/install.sh | bash \
|
|
|| warn "Claude Code install failed; continuing. Install manually with: curl -fsSL https://claude.ai/install.sh | bash"
|
|
else
|
|
warn "curl not found; skipping Claude Code install"
|
|
fi
|
|
fi
|
|
|
|
# Ensure +x on user scripts
|
|
if [ -d "$HOME/.local/bin" ]; then
|
|
find "$HOME/.local/bin" -maxdepth 1 -type f -exec chmod +x {} \; 2>/dev/null || true
|
|
fi
|
|
|
|
# 5. Refresh font cache so newly-deployed .fonts/ and .local/share/fonts/
|
|
# entries are visible to apps without a logout/login.
|
|
if have fc-cache; then
|
|
log "Refreshing font cache"
|
|
fc-cache -f >/dev/null 2>&1 || warn "fc-cache returned non-zero; continuing"
|
|
fi
|
|
|
|
echo
|
|
log "Done."
|
|
if [ -d "$BACKUP_DIR" ]; then
|
|
info "Files that would have been overwritten are backed up at:"
|
|
info " $BACKUP_DIR"
|
|
fi
|
|
info "Log out and back in (or start a new shell) to pick up the new configs."
|