- Зачем:
- stow tree-folding создавал симлинк ~/.claude → dotfiles/claude/.claude/,
из-за чего Claude Code модифицировал settings.json прямо в репозитории,
а рантайм-данные (sessions, credentials) попадали в каталог dotfiles.
- Что:
- claude убран из stow-пакетов, установка через ручную секцию в install.sh.
- settings.json копируется в ~/.claude/ (оригинал защищён chmod 444).
- добавлена автомиграция: симлинк → директория с переносом рантайм-данных.
- грязный settings.json в репо автоматически сбрасывается через git checkout.
- добавлен .stow-local-ignore для защиты от случайного stow claude.
- check.sh дополнен проверками: ~/.claude — директория, settings.json — копия.
- Проверка:
- bash install.sh (на машине с симлинком — мигрирует, иначе — идемпотентно).
- bash agents/.agents/skills/dotfiles-check/check.sh.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
178 lines
7.3 KiB
Bash
Executable File
178 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.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
|