diff --git a/install.sh b/install.sh index 0a5b2a7..2df94d1 100755 --- a/install.sh +++ b/install.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash set -euo pipefail +# ── Переменные ──────────────────────────────────────────────────────── + DOTFILES_DIR="$(cd "$(dirname "$0")" && pwd)" STOW_BIN="${STOW_BIN:-stow}" GIT_BIN="${GIT_BIN:-git}" @@ -8,20 +10,43 @@ 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} $*"; } +# Интерактивный yes/no промпт (принимает английскую y и русскую у). +ask_yes_no() { + local answer="" + echo -ne "${YELLOW}[?]${NC} $1 [y/N] " + read -r answer || answer="" + [[ "$answer" =~ ^[YyУу]$ ]] +} + +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-лог с отступом. stow_package() { "$STOW_BIN" --target="$HOME" -v "$@" 2>&1 | sed "s/^/ /" } +# Создать симлинк src → dst. Обрабатывает миграцию с директории на симлинк. ensure_symlink() { local src="$1" local dst="$2" @@ -32,12 +57,14 @@ ensure_symlink() { fi if [ -d "$dst" ]; then + # Миграция: директория с единственным симлинком skills → заменяем целиком 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" @@ -56,6 +83,7 @@ ensure_symlink() { ln -s "$src" "$dst" } +# Сохранить бэкап с временной меткой (и PID при коллизии). backup_existing_path() { local path="$1" local label="$2" @@ -70,6 +98,7 @@ backup_existing_path() { warn "$label уже существовал, сохранил резервную копию: $backup_path" } +# Копировать файл с бэкапом. Не трогает директории и спецфайлы. copy_managed_file() { local src="$1" local dst="$2" @@ -81,6 +110,7 @@ copy_managed_file() { 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" @@ -88,6 +118,7 @@ copy_managed_file() { backup_existing_path "$dst" "$label" fi elif [ -f "$dst" ]; then + # Контент совпадает → ничего не делаем if cmp -s "$src" "$dst"; then return 0 fi @@ -103,18 +134,7 @@ copy_managed_file() { 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 -} - +# Парсит [boot] секцию wsl.conf, проверяет наличие command = ... wsl_conf_has_boot_command() { awk ' /^\[boot\]$/ { in_boot=1; next } @@ -124,13 +144,93 @@ wsl_conf_has_boot_command() { ' "$1" 2>/dev/null } -# --- Проверка stow --- +# Миграция: если ~/.claude — stow-симлинк, раскрыть в директорию. +migrate_claude_stow_symlink() { + [ -L "$HOME/.claude" ] || return 0 + + info "Миграция: ~/.claude — симлинк, преобразую в директорию..." + local stow_dir + stow_dir="$(readlink -f "$HOME/.claude" 2>/dev/null || true)" + rm "$HOME/.claude" + mkdir -p "$HOME/.claude" + + if [ -n "$stow_dir" ] && [ -d "$stow_dir" ]; then + for item in "$stow_dir"/* "$stow_dir"/.[!.]*; do + [ -e "$item" ] || continue + local base + 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 +} + +# Настроить boot command для hproxy.local в /etc/wsl.conf. +setup_wsl_hproxy() { + [ -n "${WSL_DISTRO_NAME:-}" ] || return 0 + + local script="$DOTFILES_DIR/scripts/setup-hproxy.sh" + local boot_cmd="command = \"$script\"" + local legacy_cmd="command = \"ip route show default | awk '{print \$3 \\\" hproxy.local\\\"}' >> /etc/hosts\"" + + # Уже прописан наш скрипт + if grep -Fq "$boot_cmd" "$WSL_CONF_PATH" 2>/dev/null; then + warn "/etc/wsl.conf уже содержит boot command для setup-hproxy.sh, пропускаю" + return 0 + fi + + # Legacy inline command → предложить замену + if grep -Fq "$legacy_cmd" "$WSL_CONF_PATH" 2>/dev/null; then + warn "/etc/wsl.conf содержит legacy boot command для hproxy.local" + info "Рекомендуется заменить на: $boot_cmd" + if is_interactive && ask_yes_no "Заменить legacy command на setup-hproxy.sh?"; then + "$SUDO_BIN" sed -i '/ip route.*hproxy\.local/d' "$WSL_CONF_PATH" + "$SUDO_BIN" sed -i "/^\[boot\]/a command = \"$script\"" "$WSL_CONF_PATH" + info "Заменено. Перезапустите WSL: wsl --shutdown" + fi + return 0 + fi + + # Есть какой-то другой boot command + if wsl_conf_has_boot_command "$WSL_CONF_PATH"; then + warn "/etc/wsl.conf уже содержит [boot] command. Добавьте setup-hproxy.sh вручную" + info "Нужная строка: $boot_cmd" + return 0 + fi + + # Не интерактивный режим → только подсказка + if ! is_interactive; then + warn "WSL обнаружен, но stdin не интерактивный. Пропускаю настройку /etc/wsl.conf" + info "Добавьте вручную: $boot_cmd" + return 0 + fi + + # Интерактивный режим → спросить + echo "" + info "Обнаружен WSL. Скрипт $script может автоматически" + info "прописывать hproxy.local в /etc/hosts при каждом старте WSL (NAT mode)." + if ask_yes_no "Добавить boot command в /etc/wsl.conf?"; then + 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 = \"$script\"" "$WSL_CONF_PATH" + info "Добавлено в /etc/wsl.conf. Перезапустите WSL: wsl --shutdown" + else + warn "Пропущено. Можно настроить вручную — см. README.md" + fi +} + +# ── 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=() @@ -150,7 +250,8 @@ fi SKILLS_SRC="$DOTFILES_DIR/agents/.agents/skills" -# --- Opencode: установка без stow --- +# ── Opencode ───────────────────────────────────────────────────────── + OPENCODE_DIR="$HOME/.config/opencode" OPENCODE_SRC_DIR="$DOTFILES_DIR/opencode/.config/opencode" OPENCODE_JSON_SRC="$OPENCODE_SRC_DIR/opencode.json" @@ -169,31 +270,11 @@ if [ -f "$OPENCODE_JSON_SRC" ]; then copy_managed_file "$OPENCODE_JSON_SRC" "$OPENCODE_DIR/opencode.json" "opencode.json" fi -# --- Claude Code: установка без stow --- +# ── Claude Code ────────────────────────────────────────────────────── # 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 - +migrate_claude_stow_symlink mkdir -p "$HOME/.claude" # Skills (симлинк) @@ -205,9 +286,7 @@ fi 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, копирую текущий файл из репозитория" @@ -234,7 +313,8 @@ else warn "$CLAUDE_LOCAL уже существует, пропускаю" fi -# --- Bash: подключение в .bashrc --- +# ── Bash: .bashrc ──────────────────────────────────────────────────── + BASHRC="$HOME/.bashrc" BASHRC_LINE='[ -f ~/.bash_custom ] && source ~/.bash_custom' if grep -q 'bash_custom' "$BASHRC" 2>/dev/null; then @@ -246,54 +326,8 @@ else 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\"" - LEGACY_WSL_BOOT_COMMAND="command = \"ip route show default | awk '{print \$3 \\\" hproxy.local\\\"}' >> /etc/hosts\"" - if grep -Fq "$WSL_BOOT_COMMAND" "$WSL_CONF_PATH" 2>/dev/null; then - warn "/etc/wsl.conf уже содержит boot command для setup-hproxy.sh, пропускаю" - elif grep -Fq "$LEGACY_WSL_BOOT_COMMAND" "$WSL_CONF_PATH" 2>/dev/null; then - warn "/etc/wsl.conf содержит legacy boot command для hproxy.local" - info "Рекомендуется заменить на: $WSL_BOOT_COMMAND" - if is_interactive; then - echo -ne "${YELLOW}[?]${NC} Заменить legacy command на setup-hproxy.sh? [y/N] " - answer="" - if ! read -r answer; then - answer="" - fi - if [[ "$answer" =~ ^[YyУу]$ ]]; then - "$SUDO_BIN" sed -i '/ip route.*hproxy\.local/d' "$WSL_CONF_PATH" - "$SUDO_BIN" sed -i "/^\[boot\]/a command = \"$HPROXY_SCRIPT\"" "$WSL_CONF_PATH" - info "Заменено. Перезапустите WSL: wsl --shutdown" - fi - fi - elif wsl_conf_has_boot_command "$WSL_CONF_PATH"; then - warn "/etc/wsl.conf уже содержит [boot] command. Добавьте setup-hproxy.sh вручную" - info "Нужная строка: $WSL_BOOT_COMMAND" - elif ! is_interactive; then - warn "WSL обнаружен, но stdin не интерактивный. Пропускаю настройку /etc/wsl.conf" - 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 +# ── WSL: hproxy.local ──────────────────────────────────────────────── + +setup_wsl_hproxy info "Готово!"