#!/usr/bin/env bash set -Eeuo pipefail REPO_SLUG="${DOTFILES_REPO:-KAJdev/dotfiles}" BRANCH="${DOTFILES_BRANCH:-main}" SOURCE_OVERRIDE="${DOTFILES_SOURCE:-}" BACKUP_BASE="${DOTFILES_BACKUP_DIR:-$HOME/.dotfiles-backups}" INSTALL_PI=1 INSTALL_NVIM=1 DRY_RUN=0 TEMP_DIR="" BACKUP_CREATED=0 TIMESTAMP="$(date +%Y%m%d-%H%M%S)" BACKUP_DIR="$BACKUP_BASE/$TIMESTAMP" if [[ -e "$BACKUP_DIR" ]]; then BACKUP_DIR="${BACKUP_DIR}-$$"; fi if [[ -t 1 ]]; then BOLD=$'\033[1m' DIM=$'\033[2m' GREEN=$'\033[32m' YELLOW=$'\033[33m' RESET=$'\033[0m' else BOLD="" DIM="" GREEN="" YELLOW="" RESET="" fi log() { printf '%s\n' "${BOLD}==>${RESET} $*" >&2; } ok() { printf '%s\n' "${GREEN} ✓${RESET} $*"; } warn() { printf '%s\n' "${YELLOW} !${RESET} $*" >&2; } die() { printf 'error: %s\n' "$*" >&2; exit 1; } usage() { cat <<'EOF' Usage: install.sh [options] Options: --pi-only Install only Pi configuration --nvim-only Install only Neovim configuration --no-pi Skip Pi configuration --no-nvim Skip Neovim configuration --source PATH Install from an existing checkout --dry-run Show changes without writing files -h, --help Show this help Environment: DOTFILES_SOURCE, DOTFILES_REPO, DOTFILES_BRANCH, DOTFILES_BACKUP_DIR, PI_AGENT_DIR, NVIM_CONFIG_DIR EOF } while (($# > 0)); do case "$1" in --pi-only) INSTALL_PI=1; INSTALL_NVIM=0 ;; --nvim-only) INSTALL_PI=0; INSTALL_NVIM=1 ;; --no-pi) INSTALL_PI=0 ;; --no-nvim) INSTALL_NVIM=0 ;; --source) (($# >= 2)) || die "--source requires a path" SOURCE_OVERRIDE="$2" shift ;; --dry-run) DRY_RUN=1 ;; -h|--help) usage; exit 0 ;; *) die "unknown option: $1" ;; esac shift done ((INSTALL_PI || INSTALL_NVIM)) || die "nothing selected to install" cleanup() { if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then rm -rf "$TEMP_DIR" fi } trap cleanup EXIT absolute_dir() { (cd "$1" 2>/dev/null && pwd -P) } find_local_source() { local candidate="" if [[ -n "$SOURCE_OVERRIDE" ]]; then candidate="$SOURCE_OVERRIDE" elif [[ -n "${BASH_SOURCE[0]:-}" && -f "${BASH_SOURCE[0]}" ]]; then candidate="$(dirname "${BASH_SOURCE[0]}")" fi if [[ -n "$candidate" && -f "$candidate/pi/settings.json" && -f "$candidate/nvim/init.lua" ]]; then absolute_dir "$candidate" return 0 fi return 1 } download_source() { TEMP_DIR="$(mktemp -d 2>/dev/null || mktemp -d -t dotfiles)" if command -v git >/dev/null 2>&1; then log "Downloading $REPO_SLUG@$BRANCH with git" git clone --quiet --depth 1 --branch "$BRANCH" "https://github.com/$REPO_SLUG.git" "$TEMP_DIR/repo" SOURCE_DIR="$TEMP_DIR/repo" return fi command -v curl >/dev/null 2>&1 || die "git or curl is required" command -v tar >/dev/null 2>&1 || die "tar is required when git is unavailable" log "Downloading $REPO_SLUG@$BRANCH archive" curl -fsSL "https://github.com/$REPO_SLUG/archive/refs/heads/$BRANCH.tar.gz" -o "$TEMP_DIR/repo.tar.gz" mkdir -p "$TEMP_DIR/archive" tar -xzf "$TEMP_DIR/repo.tar.gz" -C "$TEMP_DIR/archive" local candidate for candidate in "$TEMP_DIR"/archive/*; do if [[ -d "$candidate" && -f "$candidate/pi/settings.json" ]]; then SOURCE_DIR="$candidate" return fi done die "downloaded archive did not contain the expected files" } resolve_nvim_dir() { if [[ -n "${NVIM_CONFIG_DIR:-}" ]]; then printf '%s\n' "$NVIM_CONFIG_DIR" return fi case "$(uname -s 2>/dev/null || printf unknown)" in MINGW*|MSYS*|CYGWIN*) if [[ -n "${LOCALAPPDATA:-}" ]]; then if command -v cygpath >/dev/null 2>&1; then printf '%s/nvim\n' "$(cygpath -u "$LOCALAPPDATA")" else printf '%s/nvim\n' "$LOCALAPPDATA" fi return fi ;; esac printf '%s/nvim\n' "${XDG_CONFIG_HOME:-$HOME/.config}" } ensure_backup_dir() { if ((BACKUP_CREATED)); then return; fi if ((DRY_RUN)); then warn "would create backup directory $BACKUP_DIR" else mkdir -p "$BACKUP_DIR" fi BACKUP_CREATED=1 } backup_item() { local path="$1" label="$2" target="$BACKUP_DIR/$label" ensure_backup_dir if ((DRY_RUN)); then warn "would back up $path -> $target" return fi mkdir -p "$(dirname "$target")" mv "$path" "$target" ok "Backed up $path" } install_file() { local source="$1" destination="$2" label="$3" [[ -f "$source" ]] || die "missing source file: $source" if [[ -f "$destination" ]] && cmp -s "$source" "$destination"; then ok "Unchanged $destination" return fi if [[ -e "$destination" || -L "$destination" ]]; then backup_item "$destination" "$label" fi if ((DRY_RUN)); then log "Would install $destination" return fi mkdir -p "$(dirname "$destination")" cp -p "$source" "$destination" ok "Installed $destination" } directories_equal() { [[ -d "$1" && -d "$2" ]] || return 1 command -v diff >/dev/null 2>&1 || return 1 diff -qr "$1" "$2" >/dev/null 2>&1 } install_directory() { local source="$1" destination="$2" label="$3" [[ -d "$source" ]] || die "missing source directory: $source" if directories_equal "$source" "$destination"; then ok "Unchanged $destination" return fi if [[ -e "$destination" || -L "$destination" ]]; then backup_item "$destination" "$label" fi if ((DRY_RUN)); then log "Would install $destination" return fi mkdir -p "$(dirname "$destination")" cp -R "$source" "$destination" ok "Installed $destination" } SOURCE_DIR="" if LOCAL_SOURCE="$(find_local_source)"; then SOURCE_DIR="$LOCAL_SOURCE" else download_source fi [[ -f "$SOURCE_DIR/pi/settings.json" ]] || die "invalid source directory: $SOURCE_DIR" log "Installing KAJdev dotfiles" printf '%s\n' "${DIM}source: $SOURCE_DIR${RESET}" if ((INSTALL_PI)); then PI_DIR="${PI_AGENT_DIR:-$HOME/.pi/agent}" log "Pi configuration" install_file "$SOURCE_DIR/pi/settings.json" "$PI_DIR/settings.json" "pi/settings.json" install_file "$SOURCE_DIR/pi/models.json" "$PI_DIR/models.json" "pi/models.json" install_directory "$SOURCE_DIR/pi/extensions" "$PI_DIR/extensions" "pi/extensions" install_directory "$SOURCE_DIR/pi/themes" "$PI_DIR/themes" "pi/themes" fi if ((INSTALL_NVIM)); then NVIM_DIR="$(resolve_nvim_dir)" log "Neovim configuration" install_directory "$SOURCE_DIR/nvim" "$NVIM_DIR" "nvim" fi if ((DRY_RUN)); then log "Dry run complete; nothing was changed" else log "Installation complete" if ((BACKUP_CREATED)); then printf 'Backups: %s\n' "$BACKUP_DIR" fi if ((INSTALL_PI)); then printf '%s\n' "Run /reload in an existing pi session." fi if ((INSTALL_NVIM)) && ! command -v nvim >/dev/null 2>&1; then warn "Neovim is not installed; configuration is ready for when it is installed." fi fi