- Зачем:
- Claude Code перезаписывает settings.json при каждом «always allow», что создавало
конфликт с managed-копией из dotfiles при повторном запуске install.sh.
- Что:
- settings.json удалён из репозитория, Claude Code владеет им полностью.
- создан settings.local.json (базовые permissions + statusLine), деплоится через симлинк.
- install.sh: копирование + шаблон заменены на ln -sf с бэкапом и guard для edge cases.
- check.sh: проверка обновлена под симлинк вместо «файл + chmod 444».
- тесты: старый тест копирования заменён на 4 новых (симлинк, бэкап, директория, stow-миграция).
- Проверка:
- bash tests/test_install.sh (15 тестов).
- bash agents/.agents/skills/dotfiles-check/check.sh.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
169 lines
7.3 KiB
Bash
Executable File
169 lines
7.3 KiB
Bash
Executable File
#!/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.local.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.local.json должен быть симлинком на репозиторий
|
|
if [ -L "$HOME/.claude/settings.local.json" ]; then
|
|
local_target=$(readlink -f "$HOME/.claude/settings.local.json" 2>/dev/null || true)
|
|
local_expected=$(readlink -f "$DOTFILES_DIR/claude/.claude/settings.local.json" 2>/dev/null || true)
|
|
if [ "$local_target" = "$local_expected" ]; then
|
|
pass "~/.claude/settings.local.json → репозиторий"
|
|
else
|
|
err "~/.claude/settings.local.json → $local_target (ожидался $local_expected)"
|
|
fi
|
|
elif [ -f "$HOME/.claude/settings.local.json" ]; then
|
|
err "~/.claude/settings.local.json — обычный файл (должен быть симлинк). Запустите install.sh"
|
|
elif [ -e "$HOME/.claude/settings.local.json" ]; then
|
|
err "~/.claude/settings.local.json — не файл и не симлинк. Удалите вручную и запустите install.sh"
|
|
else
|
|
err "~/.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
|