diff --git a/install-deps.sh b/install-deps.sh new file mode 100755 index 0000000..4a7b1fe --- /dev/null +++ b/install-deps.sh @@ -0,0 +1,205 @@ +#!/usr/bin/env bash +# install-deps.sh: Install all system and tooling dependencies. +# Run this after cloning on a new machine (before install-stow.sh). + +set -euo pipefail + +DOTFILES_DIR="$(cd "$(dirname "$0")" && pwd)" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +ok() { echo " ✓ $*"; } +info() { echo " → $*"; } +warn() { echo " ⚠ $*"; } +header() { echo ""; echo "==> $*"; } + +have() { command -v "$1" &>/dev/null; } + +# --------------------------------------------------------------------------- +# Detect Linux distribution +# --------------------------------------------------------------------------- + +detect_distro() { + if [[ -f /etc/os-release ]]; then + # shellcheck source=/dev/null + . /etc/os-release + echo "$ID" + else + echo "unknown" + fi +} + +DISTRO=$(detect_distro) + +# --------------------------------------------------------------------------- +# System packages +# --------------------------------------------------------------------------- + +header "System packages" + +COMMON_PACKAGES=( + stow + swayidle + mako + libnotify + fuzzel + wireplumber + brightnessctl + alsa-utils + wf-recorder + slurp + zenity + jq + firefox + zoxide +) + +case "$DISTRO" in + endeavouros|arch) + DISTRO_PACKAGES=( + pipewire-pulse + ffmpeg + ) + PACKAGES=("${COMMON_PACKAGES[@]}" "${DISTRO_PACKAGES[@]}") + + # Warn about pending upgrades (partial upgrades are unsupported on Arch) + sudo pacman -Sy --noconfirm &>/dev/null + updates=$(pacman -Qu 2>/dev/null | wc -l) + if [[ "$updates" -gt 0 ]]; then + warn "$updates system package(s) are out of date." + warn "If pacman fails with dependency conflicts, run 'sudo pacman -Syu'" + warn "locally (not over SSH) then re-run this script." + echo "" + fi + + # Only install packages that aren't already present + MISSING=() + for pkg in "${PACKAGES[@]}"; do + if ! pacman -Q "$pkg" &>/dev/null; then + MISSING+=("$pkg") + else + ok "$pkg" + fi + done + if [[ ${#MISSING[@]} -gt 0 ]]; then + info "Installing: ${MISSING[*]}" + sudo pacman -S --noconfirm "${MISSING[@]}" + fi + ;; + + fedora|fedora-asahi-remix) + DISTRO_PACKAGES=( + pipewire-utils + ffmpeg + ) + PACKAGES=("${COMMON_PACKAGES[@]}" "${DISTRO_PACKAGES[@]}") + + MISSING=() + for pkg in "${PACKAGES[@]}"; do + if rpm -q "$pkg" &>/dev/null; then + ok "$pkg" + else + MISSING+=("$pkg") + fi + done + if [[ ${#MISSING[@]} -gt 0 ]]; then + info "Installing: ${MISSING[*]}" + sudo dnf install -y "${MISSING[@]}" + fi + ;; + + *) + warn "Unsupported distribution: $DISTRO — skipping system packages." + ;; +esac + +# --------------------------------------------------------------------------- +# Tools not in distro repos — warn if missing +# --------------------------------------------------------------------------- + +header "Optional tools (manual install)" + +warn_missing() { + local cmd="$1" msg="$2" + if have "$cmd"; then + ok "$cmd" + else + warn "'$cmd' not found — $msg" + fi +} + +warn_missing autotiling "install via pip: pip install --user autotiling" +warn_missing eww "install from https://github.com/elkowar/eww/releases" +warn_missing wezterm "install from https://wezfurlong.org/wezterm/install/linux.html" + +# --------------------------------------------------------------------------- +# Starship +# --------------------------------------------------------------------------- + +header "Starship" + +if have starship; then + ok "starship already installed" +else + info "Installing Starship..." + curl -sS https://starship.rs/install.sh | sh +fi + +# --------------------------------------------------------------------------- +# npm global packages +# --------------------------------------------------------------------------- + +header "npm global packages" + +if npm list -g --depth=0 @mariozechner/pi-coding-agent &>/dev/null; then + ok "@mariozechner/pi-coding-agent already installed" +else + info "Installing @mariozechner/pi-coding-agent..." + npm install -g @mariozechner/pi-coding-agent +fi + +# --------------------------------------------------------------------------- +# pi extension dependencies +# --------------------------------------------------------------------------- + +header "pi extension node_modules" + +for ext_dir in "$DOTFILES_DIR"/pi/.pi/agent/extensions/*/; do + [[ -f "$ext_dir/package.json" ]] || continue + ext_name="$(basename "$ext_dir")" + + if [[ -d "$ext_dir/node_modules" ]]; then + ok "$ext_name (node_modules already present)" + else + info "Running npm install for $ext_name..." + npm install --prefix "$ext_dir" --ignore-scripts + fi +done + +# --------------------------------------------------------------------------- +# Fonts +# --------------------------------------------------------------------------- + +header "Fonts" + +FONT_DIR="$HOME/.local/share/fonts/JetBrainsMono" +if [[ -d "$FONT_DIR" ]]; then + ok "JetBrains Mono Nerd Font already installed" +else + info "Installing JetBrains Mono Nerd Font..." + TMP="$(mktemp -d)" + curl -fLo "$TMP/JetBrainsMono.zip" \ + "https://github.com/ryanoasis/nerd-fonts/releases/latest/download/JetBrainsMono.zip" + mkdir -p "$FONT_DIR" + unzip -q "$TMP/JetBrainsMono.zip" -d "$FONT_DIR" + rm -rf "$TMP" + fc-cache -f "$FONT_DIR" + ok "Font installed" +fi + +# --------------------------------------------------------------------------- + +echo "" +echo "All dependencies done. Run ./install-stow.sh to link configs." diff --git a/install-stow.sh b/install-stow.sh new file mode 100755 index 0000000..21f9a76 --- /dev/null +++ b/install-stow.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +# install-stow.sh: Stow dotfile packages into $HOME with per-package prompts. +# Packages that are already fully stowed are skipped automatically. +# Run after install-deps.sh. + +set -euo pipefail + +DOTFILES_DIR="$(cd "$(dirname "$0")" && pwd)" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +ok() { echo " ✓ $*"; } +info() { echo " → $*"; } +warn() { echo " ⚠ $*"; } +header() { echo ""; echo "==> $*"; } + +# Ask yes/no. Returns 0 for yes, 1 for no. +ask_yn() { + local prompt="$1" + while true; do + read -r -p "$prompt [y/N] " answer + case "${answer,,}" in + y|yes) return 0 ;; + n|no|"") return 1 ;; + *) echo " Please answer y or n." ;; + esac + done +} + +# --------------------------------------------------------------------------- +# Check if a package is already fully stowed +# (every file in the package has a correct symlink in $HOME) +# --------------------------------------------------------------------------- + +is_stowed() { + local pkg="$1" + local pkg_dir="$DOTFILES_DIR/$pkg" + local all_ok=true + + while IFS= read -r -d '' src; do + local rel="${src#$pkg_dir/}" + local target="$HOME/$rel" + # Must exist as a symlink pointing at this exact source + if [[ ! -L "$target" ]] || [[ "$(readlink -f "$target")" != "$(readlink -f "$src")" ]]; then + all_ok=false + break + fi + done < <(find "$pkg_dir" -type f -print0 2>/dev/null) + + $all_ok +} + +# List any real files (non-symlinks) that would be overwritten by stowing +conflicts_for() { + local pkg="$1" + local pkg_dir="$DOTFILES_DIR/$pkg" + local conflicts=() + + while IFS= read -r -d '' src; do + local rel="${src#$pkg_dir/}" + local target="$HOME/$rel" + if [[ -e "$target" ]] && [[ ! -L "$target" ]]; then + conflicts+=("$target") + fi + done < <(find "$pkg_dir" -type f -print0 2>/dev/null) + + printf '%s\n' "${conflicts[@]}" +} + +# --------------------------------------------------------------------------- +# Collect packages (top-level dirs, skip hidden) +# --------------------------------------------------------------------------- + +header "Stowing dotfile packages" + +PACKAGES=() +for d in "$DOTFILES_DIR"/*/; do + pkg="$(basename "$d")" + [[ "$pkg" == .* ]] && continue + PACKAGES+=("$pkg") +done + +STOWED=() +SKIPPED=() + +for pkg in "${PACKAGES[@]}"; do + echo "" + echo "Package: $pkg" + + if is_stowed "$pkg"; then + ok "already stowed — skipping" + STOWED+=("$pkg (already)") + continue + fi + + # Show conflicts, if any + mapfile -t conflicts < <(conflicts_for "$pkg") + if [[ ${#conflicts[@]} -gt 0 ]]; then + warn "Stowing would overwrite these real files:" + for f in "${conflicts[@]}"; do + echo " $f" + done + echo " Back them up or delete them first, then re-run." + fi + + if ask_yn " Stow '$pkg'?"; then + stow --dir="$DOTFILES_DIR" --target="$HOME" --restow "$pkg" + ok "stowed" + STOWED+=("$pkg") + else + info "skipped" + SKIPPED+=("$pkg") + fi +done + +# --------------------------------------------------------------------------- +# Configure shell (after stowing zshrc) +# --------------------------------------------------------------------------- + +header "Shell configuration" + +if [[ ! -f "$HOME/.zshrc" ]]; then + touch "$HOME/.zshrc" +fi + +if ! grep -q 'eval "$(starship init zsh)"' "$HOME/.zshrc"; then + echo 'eval "$(starship init zsh)"' >> "$HOME/.zshrc" + ok "Added starship init to ~/.zshrc" +else + ok "Starship already configured in ~/.zshrc" +fi + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- + +header "Summary" + +if [[ ${#STOWED[@]} -gt 0 ]]; then + echo " Stowed: ${STOWED[*]}" +fi +if [[ ${#SKIPPED[@]} -gt 0 ]]; then + echo " Skipped: ${SKIPPED[*]}" +fi + +echo "" +echo "Done." diff --git a/install.sh b/install.sh deleted file mode 100755 index 23fa2b0..0000000 --- a/install.sh +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/env bash -# install.sh: Install dependencies and stow all packages. -# Run this after cloning on a new machine. - -set -euo pipefail - -DOTFILES_DIR="$(cd "$(dirname "$0")" && pwd)" - -# --------------------------------------------------------------------------- -# Detect Linux distribution -# --------------------------------------------------------------------------- - -detect_distro() { - if [[ -f /etc/os-release ]]; then - . /etc/os-release - echo "$ID" - else - echo "unknown" - fi -} - -DISTRO=$(detect_distro) - -# --------------------------------------------------------------------------- -# Dependencies -# --------------------------------------------------------------------------- - -COMMON_PACKAGES=( - stow - swayidle - mako - libnotify - fuzzel - wireplumber - brightnessctl - alsa-utils - wf-recorder - slurp - zenity - jq - firefox - zoxide -) -# On Arch-based systems, partial upgrades are unsupported — installing a -# package that pulls in a newer library breaks packages built against the old -# one. The right fix is 'sudo pacman -Syu', but doing that over SSH risks -# a dropped connection mid-kernel-upgrade. So we just warn and let the user -# decide. -check_arch_updates() { - sudo pacman -Sy --noconfirm # refresh databases only, no upgrade - local updates - updates=$(pacman -Qu 2>/dev/null | wc -l) - if [[ "$updates" -gt 0 ]]; then - echo "" - echo "WARNING: $updates system package(s) are out of date." - echo " If pacman fails with dependency conflicts, run 'sudo pacman -Syu'" - echo " locally (not over SSH, to avoid kernel upgrade risks), then re-run" - echo " this script." - echo "" - fi -} - -case "$DISTRO" in - endeavouros|arch) - # Keep distro-specific packages clearly listed - DISTRO_PACKAGES=( - pipewire-pulse # Arch-specific - ffmpeg # Available in official repos - ) - PACKAGES=("${COMMON_PACKAGES[@]}" "${DISTRO_PACKAGES[@]}") - echo "Installing packages for Arch/EndeavourOS..." - sudo pacman -S --noconfirm "${PACKAGES[@]}" - ;; - fedora|fedora-asahi-remix) - DISTRO_PACKAGES=( - pipewire-utils # Fedora's package name - ffmpeg # Requires RPM Fusion - ) - PACKAGES=("${COMMON_PACKAGES[@]}" "${DISTRO_PACKAGES[@]}") - echo "Installing packages for Fedora..." - sudo dnf install -y "${PACKAGES[@]}" - ;; - - *) - echo "ERROR: Unsupported distribution: $DISTRO" - exit 1 - ;; -esac - -# --------------------------------------------------------------------------- -# Packages not in Fedora repos - install manually if missing -# --------------------------------------------------------------------------- - -warn_missing() { - local cmd="$1" msg="$2" - if ! command -v "$cmd" &>/dev/null; then - echo "WARNING: '$cmd' not found - $msg" - fi -} - -warn_missing autotiling "install via pip: pip install --user autotiling" -warn_missing eww "install from https://github.com/elkowar/eww/releases" -warn_missing wezterm "install from https://wezfurlong.org/wezterm/install/linux.html" - -# --------------------------------------------------------------------------- -# Starship -# --------------------------------------------------------------------------- - -if ! command -v starship &>/dev/null; then - echo "" - echo "Installing Starship..." - curl -sS https://starship.rs/install.sh | sh -else - echo "" - echo "Starship already installed, skipping." -fi - -# --------------------------------------------------------------------------- -# NPM packages -# --------------------------------------------------------------------------- - -echo "" -echo "Installing npm packages..." -npm install -g @mariozechner/pi-coding-agent - -# --------------------------------------------------------------------------- -# Fonts -# --------------------------------------------------------------------------- - -FONT_DIR="$HOME/.local/share/fonts/JetBrainsMono" -if [[ ! -d "$FONT_DIR" ]]; then - echo "" - echo "Installing JetBrains Mono Nerd Font..." - TMP="$(mktemp -d)" - curl -fLo "$TMP/JetBrainsMono.zip" \ - "https://github.com/ryanoasis/nerd-fonts/releases/latest/download/JetBrainsMono.zip" - mkdir -p "$FONT_DIR" - unzip -q "$TMP/JetBrainsMono.zip" -d "$FONT_DIR" - rm -rf "$TMP" - fc-cache -f "$FONT_DIR" - echo "Font installed." -else - echo "JetBrains Mono Nerd Font already installed, skipping." -fi - -# --------------------------------------------------------------------------- -# Stow all packages -# --------------------------------------------------------------------------- - -echo "" -echo "Stowing dotfiles..." - -# Check for conflicts before stowing -CONFLICTS=() -for PACKAGE in "$DOTFILES_DIR"/*/; do - PACKAGE="$(basename "$PACKAGE")" - - # Skip hidden directories and .git - [[ "$PACKAGE" == .* ]] && continue - - # Check if package has files that would conflict - while IFS= read -r -d '' dotfile; do - # Get the target path this would create - TARGET_PATH="${HOME}/${dotfile#$DOTFILES_DIR/$PACKAGE/}" - - # If target exists and is NOT a symlink to the correct location, it's a conflict - if [[ -e "$TARGET_PATH" ]] && [[ ! -L "$TARGET_PATH" ]]; then - CONFLICTS+=("$PACKAGE (file: $TARGET_PATH)") - fi - done < <(find "$DOTFILES_DIR/$PACKAGE" -type f -print0 2>/dev/null || true) -done - -if [[ ${#CONFLICTS[@]} -gt 0 ]]; then - echo "WARNING: Found conflicts with existing configs:" - for conflict in "${CONFLICTS[@]}"; do - echo " ✗ $conflict" - done - echo "" -fi - -# Stow all packages -for PACKAGE in "$DOTFILES_DIR"/*/; do - PACKAGE="$(basename "$PACKAGE")" - - # Skip hidden directories - [[ "$PACKAGE" == .* ]] && continue - - echo " $PACKAGE" - stow --dir="$DOTFILES_DIR" --target="$HOME" --restow "$PACKAGE" -done - -# --------------------------------------------------------------------------- -# Configure shell (after stowing) -# --------------------------------------------------------------------------- - -echo "" -echo "Configuring zsh..." - -# Ensure .zshrc exists (stow should have created it via symlink) -if [[ ! -f "$HOME/.zshrc" ]]; then - touch "$HOME/.zshrc" -fi - -# Ensure starship is initialized in .zshrc -if ! grep -q 'eval "$(starship init zsh)"' "$HOME/.zshrc"; then - echo 'eval "$(starship init zsh)"' >> "$HOME/.zshrc" - echo " Added starship initialization to ~/.zshrc" -else - echo " Starship already configured in ~/.zshrc" -fi - -echo "Done."