Files
dotfiles/agents/.agents/skills/dotfiles-check/check.sh
T
Dmitry DementievandClaude Opus 4.6 e00611015a fix(claude): settings.json копируется вместо симлинка через stow
- Зачем:
  - 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>
2026-03-18 15:01:34 +03:00

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