#!/usr/bin/env bash # Проверка целостности dotfiles-установки через stow. # Использование: bash check.sh [путь-к-dotfiles] set -euo pipefail DOTFILES_DIR="${1:-$(cd "$(dirname "$0")/../../../.." && pwd)}" GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[1;33m' NC='\033[0m' ok=0; warn=0; fail=0 pass() { echo -e " ${GREEN}✅${NC} $*"; ok=$((ok + 1)); } skip() { echo -e " ${YELLOW}⚠️${NC} $*"; warn=$((warn + 1)); } err() { echo -e " ${RED}❌${NC} $*"; fail=$((fail + 1)); } echo "Проверка dotfiles ($DOTFILES_DIR)" echo "═══════════════════════════════════════" # --- 1. Stow-пакеты: проверка симлинков --- # Stow использует "tree folding": если вся директория принадлежит одному пакету, # создаётся один симлинк на директорию, а не на каждый файл внутри. # Поэтому проверяем точки входа верхнего уровня каждого пакета. for pkg_dir in "$DOTFILES_DIR"/*/; do pkg=$(basename "$pkg_dir") [[ "$pkg" == .* ]] && continue # claude управляется вручную (копирование settings.json), не через stow [[ "$pkg" == "claude" ]] && continue # Точки входа — элементы первого уровня внутри пакета entries=() for entry in "$pkg_dir".* "$pkg_dir"*; do [ -e "$entry" ] || continue base=$(basename "$entry") [[ "$base" == "." || "$base" == ".." ]] && continue # Берём только dot-файлы/директории (то, что stow раскладывает в ~) [[ "$base" == .* ]] && entries+=("$base") done [ ${#entries[@]} -eq 0 ] && continue echo "" echo "$pkg (${#entries[@]} точек входа):" for entry in "${entries[@]}"; do home_path="$HOME/$entry" expected="$DOTFILES_DIR/$pkg/$entry" # Если это директория в пакете — stow мог создать симлинк на неё целиком # или "раскрыть" её и создать симлинки внутри. Проверяем оба варианта. if [ -L "$home_path" ]; then # Симлинк на верхнем уровне (tree folding) actual=$(readlink -f "$home_path" 2>/dev/null || true) expected_resolved=$(readlink -f "$expected" 2>/dev/null || true) if [ "$actual" = "$expected_resolved" ]; then pass "~/$entry" else err "~/$entry → $(readlink "$home_path") (ожидался $pkg/$entry)" fi elif [ -d "$home_path" ] && [ -d "$expected" ]; then # Директория раскрыта — проверяем файлы внутри (один уровень) inner_ok=0; inner_fail=0 while IFS= read -r -d '' f; do fname=$(basename "$f") inner_home="$home_path/$fname" if [ -L "$inner_home" ]; then inner_actual=$(readlink -f "$inner_home" 2>/dev/null || true) inner_expected=$(readlink -f "$f" 2>/dev/null || true) if [ "$inner_actual" = "$inner_expected" ]; then inner_ok=$((inner_ok + 1)) else inner_fail=$((inner_fail + 1)) fi elif [ -e "$inner_home" ]; then inner_ok=$((inner_ok + 1)) # файл есть, но не симлинк — ок для stow unfolded else inner_fail=$((inner_fail + 1)) fi done < <(find "$expected" -maxdepth 1 -not -path "$expected" -print0) if [ "$inner_fail" -eq 0 ]; then pass "~/$entry/ ($inner_ok элементов)" else err "~/$entry/ — $inner_fail из $((inner_ok + inner_fail)) элементов отсутствуют" fi elif [ -e "$home_path" ]; then skip "~/$entry — обычный файл (не симлинк)" else err "~/$entry — не найден" fi done done # --- 2. Дополнительные проверки из install.sh --- install_script="$DOTFILES_DIR/install.sh" if [ -f "$install_script" ]; then echo "" echo "Доп. проверки (install.sh):" # Claude Code: ~/.claude должна быть директорией, не симлинком if [ -L "$HOME/.claude" ]; then err "~/.claude — симлинк (должна быть директория). Запустите install.sh" elif [ -d "$HOME/.claude" ]; then pass "~/.claude — директория (не симлинк)" else err "~/.claude — не найден" fi # Claude Code: settings.json должен быть копией, не симлинком if [ -L "$HOME/.claude/settings.json" ]; then err "~/.claude/settings.json — симлинк (должна быть копия)" elif [ -f "$HOME/.claude/settings.json" ]; then pass "~/.claude/settings.json — файл (копия)" else err "~/.claude/settings.json — не найден" fi # chmod 444 на settings.json в репозитории if [ -f "$DOTFILES_DIR/claude/.claude/settings.json" ]; then perms=$(stat -c %a "$DOTFILES_DIR/claude/.claude/settings.json" 2>/dev/null || true) if [ "$perms" = "444" ]; then pass "claude/settings.json — chmod 444" else skip "claude/settings.json — chmod $perms (ожидался 444)" fi fi # settings.local.json if [ -f "$HOME/.claude/settings.local.json" ]; then pass "~/.claude/settings.local.json существует" else skip "~/.claude/settings.local.json — не найден (создайте через install.sh)" fi # skills симлинк if [ -L "$HOME/.claude/skills" ]; then pass "~/.claude/skills — симлинк" else skip "~/.claude/skills — не найден" fi # bashrc if grep -q 'bash_custom' "$HOME/.bashrc" 2>/dev/null; then pass "~/.bashrc содержит подключение bash_custom" else skip "~/.bashrc не содержит bash_custom" fi fi # --- 3. Битые симлинки --- broken=$(find "$HOME" -maxdepth 3 -xtype l 2>/dev/null | grep -v '.cache\|.local/share\|node_modules\|.npm' | head -10) if [ -n "$broken" ]; then echo "" echo "Битые симлинки в ~:" while IFS= read -r link; do err "$link → $(readlink "$link") (цель не существует)" done <<< "$broken" fi # --- 4. Секреты --- if [ -f "$HOME/.env.secrets" ]; then echo "" echo "Секреты:" pass "~/.env.secrets существует" fi # --- Итого --- total=$((ok + warn + fail)) echo "" echo "═══════════════════════════════════════" echo -e "Итого: ${GREEN}$ok${NC}/${total} ✅, ${YELLOW}$warn${NC} ⚠️, ${RED}$fail${NC} ❌" [ "$fail" -eq 0 ] && exit 0 || exit 1