- Зачем: - нужно было убрать падение install.sh на абсолютных WSL-симлинках и существующей runtime-директории opencode. - Что: - убран `--restow` из вызова stow, чтобы install не падал на ссылках `~/.aws` и `~/.azure`. - настройка `~/.config/opencode` переведена на ручную установку с backup существующего `opencode.json` и сохранением runtime-файлов. - в tests/test_install.sh добавлены сценарии для WSL-симлинков и существующей директории opencode. - Проверка: - bash -n install.sh - bash -n tests/test_install.sh - ./tests/test_install.sh
282 lines
10 KiB
Bash
Executable File
282 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
DOTFILES_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
STOW_BIN="${STOW_BIN:-stow}"
|
|
GIT_BIN="${GIT_BIN:-git}"
|
|
SUDO_BIN="${SUDO_BIN:-sudo}"
|
|
WSL_CONF_PATH="${WSL_CONF_PATH:-/etc/wsl.conf}"
|
|
INSTALL_INTERACTIVE_MODE="${INSTALL_INTERACTIVE_MODE:-auto}"
|
|
|
|
# --- Цвета ---
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m'
|
|
|
|
info() { echo -e "${GREEN}[+]${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
|
error() { echo -e "${RED}[✗]${NC} $*"; }
|
|
|
|
stow_package() {
|
|
"$STOW_BIN" --target="$HOME" -v "$@" 2>&1 | sed "s/^/ /"
|
|
}
|
|
|
|
ensure_symlink() {
|
|
local src="$1"
|
|
local dst="$2"
|
|
|
|
if [ -L "$dst" ]; then
|
|
ln -sfnT "$src" "$dst"
|
|
return 0
|
|
fi
|
|
|
|
if [ -d "$dst" ]; then
|
|
if [ -L "$dst/skills" ] && [ "$(find "$dst" -mindepth 1 -maxdepth 1 | wc -l)" -eq 1 ]; then
|
|
rm -rf "$dst"
|
|
ln -s "$src" "$dst"
|
|
return 0
|
|
fi
|
|
|
|
if [ -z "$(find "$dst" -mindepth 1 -maxdepth 1 -print -quit)" ]; then
|
|
rmdir "$dst"
|
|
ln -s "$src" "$dst"
|
|
return 0
|
|
fi
|
|
|
|
warn "$dst уже существует как директория, пропускаю замену"
|
|
return 0
|
|
fi
|
|
|
|
if [ -e "$dst" ]; then
|
|
warn "$dst уже существует и не является симлинком, пропускаю замену"
|
|
return 0
|
|
fi
|
|
|
|
ln -s "$src" "$dst"
|
|
}
|
|
|
|
backup_existing_path() {
|
|
local path="$1"
|
|
local label="$2"
|
|
local backup_path
|
|
|
|
backup_path="${path}.bak.$(date +%Y%m%d%H%M%S)"
|
|
if [ -e "$backup_path" ] || [ -L "$backup_path" ]; then
|
|
backup_path="${backup_path}.$$"
|
|
fi
|
|
|
|
mv "$path" "$backup_path"
|
|
warn "$label уже существовал, сохранил резервную копию: $backup_path"
|
|
}
|
|
|
|
copy_managed_file() {
|
|
local src="$1"
|
|
local dst="$2"
|
|
local label="$3"
|
|
local src_real=""
|
|
local dst_real=""
|
|
|
|
mkdir -p "$(dirname "$dst")"
|
|
src_real="$(readlink -f "$src" 2>/dev/null || printf '%s' "$src")"
|
|
|
|
if [ -L "$dst" ]; then
|
|
dst_real="$(readlink -f "$dst" 2>/dev/null || true)"
|
|
if [ "$dst_real" = "$src_real" ]; then
|
|
rm "$dst"
|
|
else
|
|
backup_existing_path "$dst" "$label"
|
|
fi
|
|
elif [ -f "$dst" ]; then
|
|
if cmp -s "$src" "$dst"; then
|
|
return 0
|
|
fi
|
|
backup_existing_path "$dst" "$label"
|
|
elif [ -d "$dst" ]; then
|
|
warn "$dst уже существует как директория, пропускаю замену"
|
|
return 0
|
|
elif [ -e "$dst" ]; then
|
|
warn "$dst уже существует и не является обычным файлом, пропускаю замену"
|
|
return 0
|
|
fi
|
|
|
|
cp -f "$src" "$dst"
|
|
}
|
|
|
|
is_interactive() {
|
|
case "$INSTALL_INTERACTIVE_MODE" in
|
|
always) return 0 ;;
|
|
never) return 1 ;;
|
|
auto) [ -t 0 ] ;;
|
|
*)
|
|
warn "Неизвестное значение INSTALL_INTERACTIVE_MODE=$INSTALL_INTERACTIVE_MODE, использую auto"
|
|
[ -t 0 ]
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# --- Проверка stow ---
|
|
if ! command -v "$STOW_BIN" &>/dev/null; then
|
|
error "stow не найден. Установите: sudo apt install stow"
|
|
exit 1
|
|
fi
|
|
|
|
# --- Stow пакеты ---
|
|
PACKAGES=(bash tmux vim zed vscode agents codex gemini)
|
|
AVAILABLE_PACKAGES=()
|
|
|
|
info "Применяю stow пакеты..."
|
|
cd "$DOTFILES_DIR"
|
|
for pkg in "${PACKAGES[@]}"; do
|
|
if [ -d "$pkg" ]; then
|
|
AVAILABLE_PACKAGES+=("$pkg")
|
|
else
|
|
warn "Пакет $pkg не найден, пропускаю"
|
|
fi
|
|
done
|
|
|
|
if [ "${#AVAILABLE_PACKAGES[@]}" -gt 0 ]; then
|
|
stow_package "${AVAILABLE_PACKAGES[@]}"
|
|
fi
|
|
|
|
SKILLS_SRC="$DOTFILES_DIR/agents/.agents/skills"
|
|
|
|
# --- Opencode: установка без stow ---
|
|
OPENCODE_DIR="$HOME/.config/opencode"
|
|
OPENCODE_SRC_DIR="$DOTFILES_DIR/opencode/.config/opencode"
|
|
OPENCODE_JSON_SRC="$OPENCODE_SRC_DIR/opencode.json"
|
|
|
|
mkdir -p "$OPENCODE_DIR"
|
|
|
|
if [ -d "$OPENCODE_SRC_DIR/agents" ]; then
|
|
ensure_symlink "$OPENCODE_SRC_DIR/agents" "$OPENCODE_DIR/agents"
|
|
fi
|
|
|
|
if [ -d "$SKILLS_SRC" ]; then
|
|
ensure_symlink "$SKILLS_SRC" "$OPENCODE_DIR/skills"
|
|
fi
|
|
|
|
if [ -f "$OPENCODE_JSON_SRC" ]; then
|
|
copy_managed_file "$OPENCODE_JSON_SRC" "$OPENCODE_DIR/opencode.json" "opencode.json"
|
|
fi
|
|
|
|
# --- Claude Code: установка без stow ---
|
|
# settings.json КОПИРУЕТСЯ (не линкуется), чтобы Claude Code не модифицировал
|
|
# файл в репозитории. Локальные настройки — в settings.local.json.
|
|
|
|
# Миграция: если ~/.claude — stow-симлинк, раскрываем в реальную директорию
|
|
if [ -L "$HOME/.claude" ]; then
|
|
info "Миграция: ~/.claude — симлинк, преобразую в директорию..."
|
|
CLAUDE_STOW_DIR="$(readlink -f "$HOME/.claude" 2>/dev/null || true)"
|
|
rm "$HOME/.claude"
|
|
mkdir -p "$HOME/.claude"
|
|
if [ -n "$CLAUDE_STOW_DIR" ] && [ -d "$CLAUDE_STOW_DIR" ]; then
|
|
# Переносим рантайм-данные из stow-директории
|
|
for item in "$CLAUDE_STOW_DIR"/* "$CLAUDE_STOW_DIR"/.[!.]*; do
|
|
[ -e "$item" ] || continue
|
|
base=$(basename "$item")
|
|
# Пропускаем git-tracked файлы — они останутся в репозитории
|
|
case "$base" in .gitignore|settings.json|skills) continue ;; esac
|
|
mv "$item" "$HOME/.claude/" 2>/dev/null || true
|
|
done
|
|
info "Рантайм-данные перенесены в ~/.claude/"
|
|
else
|
|
warn "Не удалось прочитать источник старого симлинка ~/.claude, продолжаю без миграции данных"
|
|
fi
|
|
fi
|
|
|
|
mkdir -p "$HOME/.claude"
|
|
|
|
# Skills (симлинк)
|
|
if [ -d "$SKILLS_SRC" ]; then
|
|
ensure_symlink "$SKILLS_SRC" "$HOME/.claude/skills"
|
|
fi
|
|
|
|
# settings.json — копия из репозитория (не симлинк!)
|
|
CLAUDE_SETTINGS_SRC="$DOTFILES_DIR/claude/.claude/settings.json"
|
|
CLAUDE_SETTINGS_DST="$HOME/.claude/settings.json"
|
|
if [ -f "$CLAUDE_SETTINGS_SRC" ]; then
|
|
# Защищаем оригинал в репозитории
|
|
chmod 444 "$CLAUDE_SETTINGS_SRC"
|
|
# Если доступен git, копируем committed-версию без изменений рабочей директории.
|
|
if "$GIT_BIN" -C "$DOTFILES_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
if ! "$GIT_BIN" -C "$DOTFILES_DIR" show "HEAD:claude/.claude/settings.json" > "$CLAUDE_SETTINGS_DST"; then
|
|
warn "Не удалось прочитать committed settings.json, копирую текущий файл из репозитория"
|
|
cp -f "$CLAUDE_SETTINGS_SRC" "$CLAUDE_SETTINGS_DST"
|
|
fi
|
|
else
|
|
cp -f "$CLAUDE_SETTINGS_SRC" "$CLAUDE_SETTINGS_DST"
|
|
fi
|
|
chmod 644 "$CLAUDE_SETTINGS_DST"
|
|
info "settings.json скопирован (оригинал защищён chmod 444)"
|
|
fi
|
|
|
|
# settings.local.json — локальные переопределения (не в git)
|
|
CLAUDE_LOCAL="$HOME/.claude/settings.local.json"
|
|
if [ ! -f "$CLAUDE_LOCAL" ]; then
|
|
info "Создаю $CLAUDE_LOCAL (локальные настройки Claude)..."
|
|
cat > "$CLAUDE_LOCAL" <<'EOF'
|
|
{
|
|
"model": "opus",
|
|
"effortLevel": "high"
|
|
}
|
|
EOF
|
|
else
|
|
warn "$CLAUDE_LOCAL уже существует, пропускаю"
|
|
fi
|
|
|
|
# --- Bash: подключение в .bashrc ---
|
|
BASHRC="$HOME/.bashrc"
|
|
BASHRC_LINE='[ -f ~/.bash_custom ] && source ~/.bash_custom'
|
|
if grep -q 'bash_custom' "$BASHRC" 2>/dev/null; then
|
|
warn "~/.bashrc уже содержит bash_custom, пропускаю"
|
|
else
|
|
echo "" >> "$BASHRC"
|
|
echo "# dotfiles" >> "$BASHRC"
|
|
echo "$BASHRC_LINE" >> "$BASHRC"
|
|
info "Добавлено в ~/.bashrc: $BASHRC_LINE"
|
|
fi
|
|
|
|
# --- WSL: hproxy.local (автоматический резолв при старте) ---
|
|
if [ -n "${WSL_DISTRO_NAME:-}" ]; then
|
|
HPROXY_SCRIPT="$DOTFILES_DIR/scripts/setup-hproxy.sh"
|
|
WSL_BOOT_COMMAND="command = \"$HPROXY_SCRIPT\""
|
|
if grep -Fq "$WSL_BOOT_COMMAND" "$WSL_CONF_PATH" 2>/dev/null; then
|
|
warn "/etc/wsl.conf уже содержит boot command для setup-hproxy.sh, пропускаю"
|
|
elif ! is_interactive; then
|
|
warn "WSL обнаружен, но stdin не интерактивный. Пропускаю настройку /etc/wsl.conf"
|
|
info "Добавьте вручную: $WSL_BOOT_COMMAND"
|
|
else
|
|
if awk '
|
|
/^\[boot\]$/ { in_boot=1; next }
|
|
/^\[/ { in_boot=0 }
|
|
in_boot && /^[[:space:]]*command[[:space:]]*=/ { found=1 }
|
|
END { exit !found }
|
|
' "$WSL_CONF_PATH" 2>/dev/null; then
|
|
warn "/etc/wsl.conf уже содержит [boot] command. Добавьте setup-hproxy.sh вручную"
|
|
info "Нужная строка: $WSL_BOOT_COMMAND"
|
|
else
|
|
echo ""
|
|
info "Обнаружен WSL. Скрипт $HPROXY_SCRIPT может автоматически"
|
|
info "прописывать hproxy.local в /etc/hosts при каждом старте WSL (NAT mode)."
|
|
echo -ne "${YELLOW}[?]${NC} Добавить boot command в /etc/wsl.conf? [y/N] "
|
|
answer=""
|
|
if ! read -r answer; then
|
|
answer=""
|
|
fi
|
|
if [[ "$answer" =~ ^[Yy]$ ]]; then
|
|
# Добавляем [boot] секцию, если её нет
|
|
if ! grep -q '^\[boot\]' "$WSL_CONF_PATH" 2>/dev/null; then
|
|
printf '\n[boot]\n' | "$SUDO_BIN" tee -a "$WSL_CONF_PATH" > /dev/null
|
|
fi
|
|
"$SUDO_BIN" sed -i "/^\[boot\]/a command = \"$HPROXY_SCRIPT\"" "$WSL_CONF_PATH"
|
|
info "Добавлено в /etc/wsl.conf. Перезапустите WSL: wsl --shutdown"
|
|
else
|
|
warn "Пропущено. Можно настроить вручную — см. README.md"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
info "Готово!"
|