refactor(install): причёсан стиль и структура install.sh
- Зачем:
- скрипт, написанный Codex, отличался по стилю от остального репозитория,
трудно читался из-за вложенных условий и отсутствия визуальной структуры.
- Что:
- добавлены секционные заголовки в стиле profile.ps1 (# ── Name ──────).
- извлечён ask_yes_no() — убрано дублирование промптов.
- WSL-логика вынесена в setup_wsl_hproxy() с flat early returns.
- миграция stow-симлинка вынесена в migrate_claude_stow_symlink().
- добавлены однострочные комментарии к хелперам и нетривиальным веткам.
- Проверка:
- bash tests/test_install.sh
- ./install.sh (идемпотентный повторный прогон)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
877dd6cc4d
commit
8567b2262c
1 file changed
+124
-90
+124
-90
@@ -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 "Готово!"
|
||||
Reference in new issue
Block a user