#!/usr/bin/env bash # ============================================================================ # Dotfiles Health Check (Arch/CachyOS) # ============================================================================ # Comprehensive health check with Arch-specific diagnostics # # Usage: # dotfiles-doctor.sh # Run all checks # dotfiles-doctor.sh --fix # Attempt automatic fixes # dotfiles-doctor.sh --quick # Quick essential checks only # ============================================================================ # Note: Not using set -e because arithmetic operations can return non-zero readonly DOTFILES_HOME="${DOTFILES_HOME:-$HOME/.dotfiles}" readonly DOTFILES_VERSION="3.1.0" # Parse arguments DO_FIX=false QUICK_MODE=false for arg in "$@"; do case "$arg" in --fix) DO_FIX=true ;; --quick) QUICK_MODE=true ;; --help|-h) echo "Usage: dotfiles-doctor.sh [OPTIONS]" echo "" echo "Options:" echo " --fix Attempt automatic fixes for issues" echo " --quick Run quick essential checks only" echo " --help Show this help" exit 0 ;; esac done # Source shared colors source "$DOTFILES_HOME/zsh/lib/colors.zsh" 2>/dev/null || { DF_RED=$'\033[0;31m' DF_GREEN=$'\033[0;32m' DF_YELLOW=$'\033[1;33m' DF_BLUE=$'\033[0;34m' DF_CYAN=$'\033[0;36m' DF_NC=$'\033[0m' DF_GREY=$'\033[38;5;242m' DF_LIGHT_BLUE=$'\033[38;5;39m' DF_BOLD=$'\033[1m' DF_DIM=$'\033[2m' } # Track results TOTAL_CHECKS=0 PASSED_CHECKS=0 FAILED_CHECKS=0 WARNING_CHECKS=0 FIXED_CHECKS=0 # ============================================================================ # MOTD-style header # ============================================================================ print_header() { local user="${USER:-root}" local hostname="${HOSTNAME:-$(hostname -s 2>/dev/null)}" local datetime=$(date '+%a %b %d %H:%M') local width=66 local hline="" && for ((i=0; i/dev/null; then local version=$(grep "VERSION_ID" /etc/os-release 2>/dev/null | cut -d= -f2 | tr -d '"') check_pass "Running CachyOS ${version}" elif grep -qi "arch" /etc/os-release 2>/dev/null; then check_pass "Running Arch Linux" else check_fail "Not running on Arch/CachyOS" fi else check_fail "Not running on Linux" fi # Kernel check local kernel=$(uname -r) if [[ "$kernel" == *"cachyos"* ]]; then check_pass "CachyOS kernel: $kernel" elif [[ "$kernel" == *"zen"* ]]; then check_pass "Zen kernel: $kernel" elif [[ "$kernel" == *"lts"* ]]; then check_pass "LTS kernel: $kernel" else check_pass "Kernel: $kernel" fi } check_shell() { print_section "Shell Configuration" if [[ -f "$HOME/.zshrc" ]]; then check_pass "Zsh configuration exists" else check_fail "Zsh configuration missing" if [[ "$DO_FIX" == true ]]; then ln -sf "$DOTFILES_HOME/zsh/.zshrc" "$HOME/.zshrc" 2>/dev/null && check_fixed ".zshrc symlink created" fi fi if [[ "$SHELL" == *"zsh"* ]]; then check_pass "Zsh is default shell" else check_warn "Zsh is not default shell (current: $SHELL)" if [[ "$DO_FIX" == true ]]; then echo " Run: chsh -s \$(which zsh)" fi fi # Check if zsh is recent version if command -v zsh &>/dev/null; then local zsh_version=$(zsh --version | awk '{print $2}') check_pass "Zsh version: $zsh_version" fi } check_symlinks() { print_section "Symlinks" local symlink_count=0 local broken_count=0 for symlink in ~/.zshrc ~/.gitconfig ~/.vimrc ~/.tmux.conf; do if [[ -L "$symlink" ]]; then symlink_count=$((symlink_count + 1)) if [[ -e "$symlink" ]]; then check_pass "$(basename $symlink) → $(readlink $symlink)" else broken_count=$((broken_count + 1)) check_fail "$(basename $symlink) is broken" fi elif [[ -f "$symlink" ]]; then check_warn "$(basename $symlink) is regular file (not symlink)" fi done if [[ $symlink_count -eq 0 ]]; then check_warn "No symlinks found (may not be installed yet)" fi } check_vim() { print_section "Editor Configuration" if command -v vim &>/dev/null; then local vim_version=$(vim --version | head -1 | awk '{print $5}') check_pass "Vim installed: $vim_version" else check_fail "Vim not installed" fi if command -v nvim &>/dev/null; then local nvim_version=$(nvim --version | head -1 | awk '{print $2}') check_pass "Neovim installed: $nvim_version" else check_warn "Neovim not installed (optional)" fi } check_git() { print_section "Git Configuration" if command -v git &>/dev/null; then check_pass "Git installed" if git config --global user.name &>/dev/null; then local git_user=$(git config --global user.name) check_pass "Git user: $git_user" else check_fail "Git user not configured" fi if git config --global user.email &>/dev/null; then check_pass "Git email configured" else check_fail "Git email not configured" fi else check_fail "Git not installed" fi } # ============================================================================ # Arch-Specific Checks # ============================================================================ check_pacman() { print_section "Package Manager" if command -v pacman &>/dev/null; then check_pass "Pacman available" else check_fail "Pacman not found" return fi # Check for AUR helper if command -v paru &>/dev/null; then check_pass "AUR helper: paru" elif command -v yay &>/dev/null; then check_pass "AUR helper: yay" else check_warn "No AUR helper installed (recommend: paru)" fi } check_pacman_health() { [[ "$QUICK_MODE" == true ]] && return print_section "Pacman Health" # Check for orphaned packages local orphans=$(pacman -Qtdq 2>/dev/null | wc -l) if [[ $orphans -eq 0 ]]; then check_pass "No orphaned packages" else check_warn "$orphans orphaned package(s)" if [[ "$DO_FIX" == true ]]; then echo " Clean: pacman -Qtdq | sudo pacman -Rns -" fi fi # Check package cache size if [[ -d /var/cache/pacman/pkg ]]; then local cache_size=$(du -sh /var/cache/pacman/pkg 2>/dev/null | cut -f1) local pkg_count=$(ls /var/cache/pacman/pkg 2>/dev/null | wc -l) if [[ $pkg_count -gt 500 ]]; then check_warn "Package cache: $cache_size ($pkg_count files)" if [[ "$DO_FIX" == true ]]; then echo " Clean: sudo paccache -rk2" fi else check_pass "Package cache: $cache_size" fi fi # Check for available updates if command -v checkupdates &>/dev/null; then local updates=$(checkupdates 2>/dev/null | wc -l) if [[ $updates -eq 0 ]]; then check_pass "System up to date" else check_warn "$updates update(s) available" fi fi } check_systemd() { [[ "$QUICK_MODE" == true ]] && return print_section "Systemd Services" # Check for failed services local failed_count=$(systemctl --failed --no-pager --no-legend 2>/dev/null | wc -l) if [[ $failed_count -eq 0 ]]; then check_pass "No failed system services" else check_fail "$failed_count failed service(s)" systemctl --failed --no-pager --no-legend 2>/dev/null | head -3 | while read -r line; do local svc=$(echo "$line" | awk '{print $1}') echo -e " ${DF_DIM}• $svc${DF_NC}" done fi # Check user services local user_failed=$(systemctl --user --failed --no-pager --no-legend 2>/dev/null | wc -l) if [[ $user_failed -eq 0 ]]; then check_pass "No failed user services" else check_warn "$user_failed failed user service(s)" fi } check_btrfs() { [[ "$QUICK_MODE" == true ]] && return # Only check if root is btrfs local fstype=$(df -T / 2>/dev/null | awk 'NR==2 {print $2}') [[ "$fstype" != "btrfs" ]] && return print_section "Btrfs Filesystem" check_pass "Root filesystem: btrfs" # Check for device errors local stats=$(sudo btrfs device stats / 2>/dev/null) local errors=$(echo "$stats" | grep -v " 0$" | grep -v "^$") if [[ -z "$errors" ]]; then check_pass "No btrfs device errors" else check_fail "Btrfs errors detected!" echo "$errors" | head -3 | while read -r line; do echo -e " ${DF_DIM}$line${DF_NC}" done fi # Check last scrub local scrub_info=$(sudo btrfs scrub status / 2>/dev/null) if echo "$scrub_info" | grep -q "running"; then check_pass "Scrub currently running" elif echo "$scrub_info" | grep -q "finished"; then local scrub_date=$(echo "$scrub_info" | grep "Scrub started" | awk '{print $3, $4}') check_pass "Last scrub: $scrub_date" else check_warn "No scrub history (recommend monthly)" fi # Check snapper if command -v snapper &>/dev/null && [[ -d "/.snapshots" ]]; then local snap_count=$(sudo snapper -c root list 2>/dev/null | tail -n +3 | wc -l) check_pass "Snapper: $snap_count snapshot(s)" fi } # ============================================================================ # Standard Checks # ============================================================================ check_optional_tools() { print_section "Optional Tools" if command -v fzf &>/dev/null; then check_pass "fzf (fuzzy finder)" else check_warn "fzf not installed (command palette needs this)" fi if command -v bat &>/dev/null; then check_pass "bat (syntax highlighting)" else check_warn "bat not installed" fi if command -v eza &>/dev/null; then check_pass "eza (modern ls)" else check_warn "eza not installed" fi if command -v tmux &>/dev/null; then check_pass "tmux (terminal multiplexer)" else check_warn "tmux not installed" fi if command -v age &>/dev/null || command -v gpg &>/dev/null; then check_pass "Encryption available (age/gpg)" else check_warn "No encryption tool (vault needs age/gpg)" fi } check_permissions() { print_section "File Permissions" if [[ -f "$DOTFILES_HOME/install.sh" ]]; then if [[ -x "$DOTFILES_HOME/install.sh" ]]; then check_pass "install.sh is executable" else check_fail "install.sh is not executable" if [[ "$DO_FIX" == true ]]; then chmod +x "$DOTFILES_HOME/install.sh" check_fixed "install.sh permissions" fi fi fi if [[ -d "$DOTFILES_HOME/bin" ]]; then local non_exec=$(find "$DOTFILES_HOME/bin" -type f ! -perm /u+x 2>/dev/null | wc -l) if [[ $non_exec -eq 0 ]]; then check_pass "All bin/ scripts executable" else check_fail "$non_exec bin/ scripts not executable" if [[ "$DO_FIX" == true ]]; then find "$DOTFILES_HOME/bin" -type f ! -perm /u+x -exec chmod +x {} \; check_fixed "bin/ permissions" fi fi fi } check_zsh_plugins() { print_section "Zsh Plugins" if [[ -d "$HOME/.oh-my-zsh" ]]; then check_pass "Oh My Zsh installed" if [[ -d "$HOME/.oh-my-zsh/custom/plugins/zsh-autosuggestions" ]]; then check_pass "zsh-autosuggestions" else check_warn "zsh-autosuggestions not installed" fi if [[ -d "$HOME/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting" ]]; then check_pass "zsh-syntax-highlighting" else check_warn "zsh-syntax-highlighting not installed" fi if [[ -f "$HOME/.oh-my-zsh/themes/adlee.zsh-theme" ]]; then check_pass "adlee theme" else check_warn "adlee theme not installed" fi else check_warn "Oh My Zsh not installed" fi } check_dotfiles_dir() { print_section "Dotfiles Directory" if [[ -d "$DOTFILES_HOME" ]]; then check_pass "Dotfiles: $DOTFILES_HOME" else check_fail "Dotfiles not found: $DOTFILES_HOME" return fi if [[ -f "$DOTFILES_HOME/dotfiles.conf" ]]; then check_pass "Config file exists" else check_warn "Config file missing" fi if [[ -d "$DOTFILES_HOME/.git" ]]; then check_pass "Git repo initialized" # Check for uncommitted changes local changes=$(cd "$DOTFILES_HOME" && git status --porcelain 2>/dev/null | wc -l) if [[ $changes -gt 0 ]]; then check_warn "$changes uncommitted change(s)" fi else check_warn "Not a git repository" fi } # ============================================================================ # Print Summary # ============================================================================ print_summary() { echo "" printf "${DF_CYAN}─%.0s${DF_NC}" {1..70}; echo "" if [[ $FAILED_CHECKS -eq 0 ]]; then echo -e "${DF_GREEN}✓${DF_NC} All checks passed ($PASSED_CHECKS/$TOTAL_CHECKS)" else echo -e "${DF_RED}✗${DF_NC} Issues found" echo -e " ${DF_GREEN}Passed:${DF_NC} $PASSED_CHECKS" echo -e " ${DF_RED}Failed:${DF_NC} $FAILED_CHECKS" if [[ $WARNING_CHECKS -gt 0 ]]; then echo -e " ${DF_YELLOW}Warnings:${DF_NC} $WARNING_CHECKS" fi if [[ $FIXED_CHECKS -gt 0 ]]; then echo -e " ${DF_CYAN}Fixed:${DF_NC} $FIXED_CHECKS" fi fi echo "" if [[ $FAILED_CHECKS -gt 0 && "$DO_FIX" != true ]]; then echo -e "${DF_YELLOW}💡${DF_NC} Run with --fix to attempt automatic fixes" echo "" return 1 fi if [[ $FIXED_CHECKS -gt 0 ]]; then echo -e "${DF_CYAN}ℹ${DF_NC} Fixed $FIXED_CHECKS issue(s). Run again to verify." echo "" fi } # ============================================================================ # Main # ============================================================================ main() { print_header # Essential checks (always run) check_os check_pacman check_shell check_dotfiles_dir check_symlinks # Additional checks (skip in quick mode) if [[ "$QUICK_MODE" != true ]]; then check_vim check_git check_zsh_plugins check_optional_tools check_permissions check_pacman_health check_systemd check_btrfs fi print_summary } main "$@"