Files
dotfiles/bin/dotfiles-doctor.sh
2025-12-14 21:19:00 -05:00

480 lines
14 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ============================================================================
# Dotfiles Doctor - Diagnostic Tool
# ============================================================================
# Checks the health of your dotfiles installation
#
# Usage:
# dotfiles-doctor.sh # Run all checks
# dotfiles-doctor.sh --fix # Attempt to fix issues
# dotfiles-doctor.sh --quiet # Only show errors
# ============================================================================
set -e
# ============================================================================
# Options
# ============================================================================
FIX_MODE=false
QUIET_MODE=false
for arg in "$@"; do
case "$arg" in
--fix)
FIX_MODE=true
;;
--quiet|-q)
QUIET_MODE=true
;;
--help|-h)
echo "Usage: $0 [OPTIONS]"
echo
echo "Options:"
echo " --fix Attempt to automatically fix issues"
echo " --quiet Only show errors and warnings"
echo " --help Show this help message"
echo
exit 0
;;
esac
done
# ============================================================================
# Load Configuration
# ============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOTFILES_CONF="${SCRIPT_DIR}/../dotfiles.conf"
[[ -f "$DOTFILES_CONF" ]] || DOTFILES_CONF="${SCRIPT_DIR}/dotfiles.conf"
[[ -f "$DOTFILES_CONF" ]] || DOTFILES_CONF="$HOME/.dotfiles/dotfiles.conf"
if [[ -f "$DOTFILES_CONF" ]]; then
source "$DOTFILES_CONF"
else
DOTFILES_DIR="$HOME/.dotfiles"
DOTFILES_VERSION="unknown"
ZSH_THEME_NAME="adlee"
fi
# ============================================================================
# Colors
# ============================================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
# ============================================================================
# Counters
# ============================================================================
PASS_COUNT=0
WARN_COUNT=0
FAIL_COUNT=0
# ============================================================================
# Helper Functions
# ============================================================================
print_header() {
if [[ "$QUIET_MODE" != true ]]; then
echo -e "\n${BLUE}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}${NC} Dotfiles Doctor ${CYAN}v${DOTFILES_VERSION}${NC} ${BLUE}${NC}"
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}\n"
fi
}
print_section() {
if [[ "$QUIET_MODE" != true ]]; then
echo -e "\n${BLUE}━━━ $1 ━━━${NC}"
fi
}
pass() {
((PASS_COUNT++))
if [[ "$QUIET_MODE" != true ]]; then
echo -e "${GREEN}${NC} $1"
fi
}
warn() {
((WARN_COUNT++))
echo -e "${YELLOW}${NC} $1"
}
fail() {
((FAIL_COUNT++))
echo -e "${RED}${NC} $1"
}
info() {
if [[ "$QUIET_MODE" != true ]]; then
echo -e "${CYAN}${NC} $1"
fi
}
# ============================================================================
# Check Functions
# ============================================================================
check_dotfiles_dir() {
print_section "Dotfiles Directory"
if [[ -d "$DOTFILES_DIR" ]]; then
pass "Dotfiles directory exists: $DOTFILES_DIR"
# Check if it's a git repo
if [[ -d "$DOTFILES_DIR/.git" ]]; then
pass "Is a git repository"
# Check for uncommitted changes
cd "$DOTFILES_DIR"
if git diff --quiet 2>/dev/null; then
pass "No uncommitted changes"
else
warn "Uncommitted changes in dotfiles"
fi
# Check if up to date with remote
git fetch origin --quiet 2>/dev/null || true
local local_hash=$(git rev-parse HEAD 2>/dev/null)
local remote_hash=$(git rev-parse origin/${DOTFILES_BRANCH:-main} 2>/dev/null || echo "")
if [[ -n "$remote_hash" && "$local_hash" == "$remote_hash" ]]; then
pass "Up to date with remote"
elif [[ -n "$remote_hash" ]]; then
warn "Behind remote (run: cd ~/.dotfiles && git pull)"
fi
cd - > /dev/null
else
warn "Not a git repository"
fi
# Check config file
if [[ -f "$DOTFILES_DIR/dotfiles.conf" ]]; then
pass "Config file exists: dotfiles.conf"
else
fail "Config file missing: dotfiles.conf"
fi
else
fail "Dotfiles directory not found: $DOTFILES_DIR"
fi
}
check_symlinks() {
print_section "Symlinks"
local symlinks=(
"$HOME/.zshrc:$DOTFILES_DIR/zsh/.zshrc"
"$HOME/.gitconfig:$DOTFILES_DIR/git/.gitconfig"
"$HOME/.vimrc:$DOTFILES_DIR/vim/.vimrc"
"$HOME/.tmux.conf:$DOTFILES_DIR/tmux/.tmux.conf"
"$HOME/.oh-my-zsh/themes/${ZSH_THEME_NAME}.zsh-theme:$DOTFILES_DIR/zsh/themes/${ZSH_THEME_NAME}.zsh-theme"
)
local valid_count=0
local total_count=0
for entry in "${symlinks[@]}"; do
local link="${entry%%:*}"
local target="${entry##*:}"
local name=$(basename "$link")
((total_count++))
if [[ -L "$link" ]]; then
local actual_target=$(readlink -f "$link" 2>/dev/null)
local expected_target=$(readlink -f "$target" 2>/dev/null)
if [[ "$actual_target" == "$expected_target" ]]; then
pass "Symlink valid: $name"
((valid_count++))
else
warn "Symlink points elsewhere: $name"
info " Expected: $target"
info " Actual: $actual_target"
fi
elif [[ -f "$link" ]]; then
warn "Regular file (not symlink): $name"
if [[ "$FIX_MODE" == true ]]; then
if [[ -f "$target" ]]; then
mv "$link" "$link.backup"
ln -sf "$target" "$link"
pass "Fixed: $name (backup saved)"
((valid_count++))
fi
fi
elif [[ -f "$target" ]]; then
fail "Symlink missing: $name"
if [[ "$FIX_MODE" == true ]]; then
ln -sf "$target" "$link"
pass "Fixed: Created symlink for $name"
((valid_count++))
fi
else
info "Source not present: $name (optional)"
fi
done
# Check espanso symlink
if [[ -L "$HOME/.config/espanso" ]]; then
pass "Symlink valid: espanso config"
elif [[ -d "$HOME/.config/espanso" ]]; then
warn "Espanso config is directory (not symlink)"
elif [[ -d "$DOTFILES_DIR/espanso" ]]; then
fail "Espanso symlink missing"
fi
info "Symlinks: $valid_count/$total_count valid"
}
check_shell() {
print_section "Shell"
# Check current shell
if [[ "$SHELL" == *"zsh"* ]]; then
pass "Default shell is zsh"
else
warn "Default shell is not zsh: $SHELL"
info " Change with: chsh -s \$(which zsh)"
fi
# Check oh-my-zsh
if [[ -d "$HOME/.oh-my-zsh" ]]; then
pass "oh-my-zsh installed"
else
fail "oh-my-zsh not installed"
fi
# Check theme
if [[ -f "$HOME/.oh-my-zsh/themes/${ZSH_THEME_NAME}.zsh-theme" ]]; then
pass "Theme installed: ${ZSH_THEME_NAME}"
else
fail "Theme missing: ${ZSH_THEME_NAME}"
fi
# Check ZSH_THEME in .zshrc
if grep -q "ZSH_THEME=\"${ZSH_THEME_NAME}\"" "$HOME/.zshrc" 2>/dev/null; then
pass "Theme configured in .zshrc"
else
warn "Theme may not be configured in .zshrc"
fi
}
check_zsh_plugins() {
print_section "Zsh Plugins"
local custom_dir="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins"
# zsh-autosuggestions
if [[ -d "$custom_dir/zsh-autosuggestions" ]]; then
pass "Plugin installed: zsh-autosuggestions"
else
fail "Plugin missing: zsh-autosuggestions"
if [[ "$FIX_MODE" == true ]]; then
git clone --depth 1 https://github.com/zsh-users/zsh-autosuggestions "$custom_dir/zsh-autosuggestions"
pass "Fixed: Installed zsh-autosuggestions"
else
info " Install: git clone https://github.com/zsh-users/zsh-autosuggestions $custom_dir/zsh-autosuggestions"
fi
fi
# zsh-syntax-highlighting
if [[ -d "$custom_dir/zsh-syntax-highlighting" ]]; then
pass "Plugin installed: zsh-syntax-highlighting"
else
fail "Plugin missing: zsh-syntax-highlighting"
if [[ "$FIX_MODE" == true ]]; then
git clone --depth 1 https://github.com/zsh-users/zsh-syntax-highlighting "$custom_dir/zsh-syntax-highlighting"
pass "Fixed: Installed zsh-syntax-highlighting"
else
info " Install: git clone https://github.com/zsh-users/zsh-syntax-highlighting $custom_dir/zsh-syntax-highlighting"
fi
fi
}
check_git() {
print_section "Git Configuration"
# Check git installed
if command -v git &>/dev/null; then
pass "git installed: $(git --version | cut -d' ' -f3)"
else
fail "git not installed"
return
fi
# Check user.name
local git_name=$(git config --global user.name 2>/dev/null)
if [[ -n "$git_name" ]]; then
pass "Git user.name: $git_name"
else
fail "Git user.name not configured"
info " Set with: git config --global user.name \"Your Name\""
fi
# Check user.email
local git_email=$(git config --global user.email 2>/dev/null)
if [[ -n "$git_email" ]]; then
pass "Git user.email: $git_email"
else
fail "Git user.email not configured"
info " Set with: git config --global user.email \"you@example.com\""
fi
# Check credential helper
local cred_helper=$(git config --global credential.helper 2>/dev/null)
if [[ -n "$cred_helper" ]]; then
pass "Git credential helper: $cred_helper"
else
warn "Git credential helper not configured"
fi
}
check_espanso() {
print_section "Espanso"
if command -v espanso &>/dev/null; then
pass "espanso installed: $(espanso --version 2>/dev/null | head -1)"
# Check if running
if espanso status 2>/dev/null | grep -q "running"; then
pass "espanso service running"
else
warn "espanso service not running"
info " Start with: espanso service start"
fi
# Check config
if [[ -f "$HOME/.config/espanso/match/base.yml" ]]; then
pass "espanso config present"
else
warn "espanso base.yml not found"
fi
else
info "espanso not installed (optional)"
fi
}
check_optional_tools() {
print_section "Optional Tools"
# fzf
if command -v fzf &>/dev/null; then
pass "fzf installed"
else
info "fzf not installed (optional)"
fi
# bat/batcat
if command -v bat &>/dev/null || command -v batcat &>/dev/null; then
pass "bat installed"
else
info "bat not installed (optional)"
fi
# eza
if command -v eza &>/dev/null; then
pass "eza installed"
else
info "eza not installed (optional)"
fi
# fd
if command -v fd &>/dev/null; then
pass "fd installed"
else
info "fd not installed (optional)"
fi
}
check_bin_scripts() {
print_section "Bin Scripts"
local bin_dir="$HOME/.local/bin"
if [[ -d "$bin_dir" ]]; then
local script_count=0
local valid_count=0
for script in "$DOTFILES_DIR/bin"/*; do
if [[ -f "$script" ]]; then
((script_count++))
local name=$(basename "$script")
local link="$bin_dir/$name"
if [[ -L "$link" ]]; then
((valid_count++))
elif [[ -f "$link" ]]; then
warn "Script is regular file: $name"
else
fail "Script not linked: $name"
fi
fi
done
if [[ $script_count -gt 0 ]]; then
pass "Bin scripts: $valid_count/$script_count linked"
fi
# Check PATH
if [[ ":$PATH:" == *":$bin_dir:"* ]]; then
pass "$bin_dir is in PATH"
else
warn "$bin_dir not in PATH"
info " Add to .zshrc: export PATH=\"\$HOME/.local/bin:\$PATH\""
fi
else
warn "~/.local/bin directory doesn't exist"
fi
}
print_summary() {
echo
echo -e "${BLUE}━━━ Summary ━━━${NC}"
echo
echo -e " ${GREEN}Passed:${NC} $PASS_COUNT"
echo -e " ${YELLOW}Warnings:${NC} $WARN_COUNT"
echo -e " ${RED}Failed:${NC} $FAIL_COUNT"
echo
if [[ $FAIL_COUNT -eq 0 && $WARN_COUNT -eq 0 ]]; then
echo -e "${GREEN}✓ All checks passed! Your dotfiles are healthy.${NC}"
elif [[ $FAIL_COUNT -eq 0 ]]; then
echo -e "${YELLOW}⚠ Some warnings, but no critical issues.${NC}"
else
echo -e "${RED}✗ Some issues found.${NC}"
if [[ "$FIX_MODE" != true ]]; then
echo -e " Run with ${CYAN}--fix${NC} to attempt automatic fixes."
fi
fi
echo
}
# ============================================================================
# Main
# ============================================================================
main() {
print_header
check_dotfiles_dir
check_symlinks
check_shell
check_zsh_plugins
check_git
check_espanso
check_optional_tools
check_bin_scripts
print_summary
# Exit with error code if there were failures
[[ $FAIL_COUNT -eq 0 ]]
}
main "$@"