Files
dotfiles/install.sh
T
ddadmin 6e431d875e refactor(proxy-client): переименован интерфейс прокси
- Зачем:
  - прежнее имя выпячивало Xray, хотя клиент поддерживает также внешний прокси, а два каталога конфигурации были неочевидны.
- Что:
  - пакет, CLI, конфигурация, состояние, тесты и документация переименованы в proxy-client.
  - машинный конфиг, профиль и два явных примера собраны в одном пользовательском каталоге.
  - свежая установка защищена от сворачивания каталога конфигурации внутрь репозитория.
- Проверка:
  - bash tests/test_proxy_client_full.sh.
  - bash tests/test_install.sh.
  - ручные start, status и xray run -test; незавершённая сетевая приёмка записана в handoff.
2026-07-21 09:31:21 -04:00

406 lines
16 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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} $*"; }
# Интерактивный 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"
if [ -L "$dst" ]; then
ln -sfnT "$src" "$dst"
return 0
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"
return 0
fi
warn "$dst уже существует как директория, пропускаю замену"
return 0
fi
if [ -e "$dst" ]; then
warn "$dst уже существует и не является симлинком, пропускаю замену"
return 0
fi
ln -s "$src" "$dst"
}
# Сохранить бэкап с временной меткой (и PID при коллизии).
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"
}
# Парсит [boot] секцию wsl.conf, проверяет наличие command = ...
wsl_conf_has_boot_command() {
awk '
/^\[boot\]$/ { in_boot=1; next }
/^\[/ { in_boot=0 }
in_boot && /^[[:space:]]*command[[:space:]]*=/ { found=1 }
END { exit !found }
' "$1" 2>/dev/null
}
# Миграция: если ~/.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.local.json|skills) continue ;; esac
mv "$item" "$HOME/.claude/" 2>/dev/null || true
done
info "Рантайм-данные перенесены в ~/.claude/"
else
warn "Не удалось прочитать источник старого симлинка ~/.claude, продолжаю без миграции данных"
fi
}
# Миграция: раскрыть старую stow-ссылку ~/.config -> zed/.config.
# В пакете zed остаётся только управляемый каталог zed; остальные настройки
# возвращаются в домашний каталог и больше не попадают в репозиторий.
migrate_zed_config_stow_symlink() {
local config_path="$HOME/.config"
local zed_config="$DOTFILES_DIR/zed/.config"
local config_real=""
local zed_config_real=""
local replacement=""
local items_file=""
local item=""
local base=""
local moved_items=()
[ -L "$config_path" ] || return 0
config_real="$(readlink -f "$config_path" 2>/dev/null || true)"
zed_config_real="$(readlink -f "$zed_config" 2>/dev/null || true)"
if [ -z "$zed_config_real" ] || [ "$config_real" != "$zed_config_real" ]; then
return 0
fi
info "Миграция: ~/.config больше не должен принадлежать пакету zed..."
replacement="$(mktemp -d "$HOME/.config.migrate.XXXXXX")"
chmod --reference="$zed_config_real" "$replacement"
items_file="$(mktemp "$HOME/.config.migrate-items.XXXXXX")"
if ! find "$zed_config_real" -mindepth 1 -maxdepth 1 ! -name zed -print0 > "$items_file"; then
error "Не удалось получить полный список старого ~/.config"
rm -f -- "$items_file"
rmdir "$replacement" 2>/dev/null || true
return 1
fi
while IFS= read -r -d '' item; do
base="${item##*/}"
if ! mv -- "$item" "$replacement/$base"; then
error "Не удалось перенести ~/.config/$base"
for base in "${moved_items[@]}"; do
mv -- "$replacement/$base" "$zed_config_real/$base" 2>/dev/null || true
done
rm -f -- "$items_file"
rmdir "$replacement" 2>/dev/null || true
return 1
fi
moved_items+=("$base")
done < "$items_file"
rm -f -- "$items_file"
if ! rm -- "$config_path"; then
error "Не удалось удалить старую ссылку ~/.config"
for base in "${moved_items[@]}"; do
mv -- "$replacement/$base" "$zed_config_real/$base" 2>/dev/null || true
done
rmdir "$replacement" 2>/dev/null || true
return 1
fi
if ! mv -- "$replacement" "$config_path"; then
error "Не удалось создать новый каталог ~/.config"
ln -s "$zed_config_real" "$config_path" 2>/dev/null || true
for base in "${moved_items[@]}"; do
mv -- "$replacement/$base" "$zed_config_real/$base" 2>/dev/null || true
done
rmdir "$replacement" 2>/dev/null || true
return 1
fi
info "Настройки перенесены из пакета zed в ~/.config"
}
# Настроить 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
PACKAGES=(bash tmux vim zed vscode agents codex gemini proxy-client)
AVAILABLE_PACKAGES=()
info "Применяю stow пакеты..."
cd "$DOTFILES_DIR"
for pkg in "${PACKAGES[@]}"; do
if [ -d "$pkg" ]; then
AVAILABLE_PACKAGES+=("$pkg")
else
warn "Пакет $pkg не найден, пропускаю"
fi
done
migrate_zed_config_stow_symlink
if [ ! -L "$HOME/.config" ]; then
mkdir -p "$HOME/.config"
fi
if [ -d "$HOME/.config" ] && [ ! -L "$HOME/.config" ] \
&& [ ! -L "$HOME/.config/proxy-client" ]; then
mkdir -p "$HOME/.config/proxy-client"
fi
if [ "${#AVAILABLE_PACKAGES[@]}" -gt 0 ]; then
stow_package "${AVAILABLE_PACKAGES[@]}"
fi
SKILLS_SRC="$DOTFILES_DIR/agents/.agents/skills"
# ── Opencode ─────────────────────────────────────────────────────────
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 ──────────────────────────────────────────────────────
# settings.local.json ЛИНКУЕТСЯ из репозитория (базовые permissions + statusLine).
# Claude Code не модифицирует settings.local.json — только читает и мержит
# с settings.json, который Claude Code создаёт и обновляет самостоятельно.
# Машинно-специфичные настройки (model, effortLevel) — в settings.json.
migrate_claude_stow_symlink
mkdir -p "$HOME/.claude"
# Skills (симлинк)
if [ -d "$SKILLS_SRC" ]; then
ensure_symlink "$SKILLS_SRC" "$HOME/.claude/skills"
fi
# settings.local.json — базовые настройки из репозитория (симлинк)
CLAUDE_LOCAL_SRC="$DOTFILES_DIR/claude/.claude/settings.local.json"
CLAUDE_LOCAL_DST="$HOME/.claude/settings.local.json"
if [ -f "$CLAUDE_LOCAL_SRC" ]; then
if [ -L "$CLAUDE_LOCAL_DST" ]; then
ln -sfnT "$CLAUDE_LOCAL_SRC" "$CLAUDE_LOCAL_DST"
info "settings.local.json → репозиторий"
elif [ -f "$CLAUDE_LOCAL_DST" ]; then
backup_existing_path "$CLAUDE_LOCAL_DST" "settings.local.json"
ln -s "$CLAUDE_LOCAL_SRC" "$CLAUDE_LOCAL_DST"
info "settings.local.json → репозиторий"
info "Машинно-специфичные настройки (model, effortLevel) теперь задаются через Claude Code"
elif [ -d "$CLAUDE_LOCAL_DST" ] || [ -e "$CLAUDE_LOCAL_DST" ]; then
warn "$CLAUDE_LOCAL_DST уже существует (не файл и не симлинк), удалите вручную"
else
ln -s "$CLAUDE_LOCAL_SRC" "$CLAUDE_LOCAL_DST"
info "settings.local.json → репозиторий"
fi
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 ────────────────────────────────────────────────
setup_wsl_hproxy
info "Готово!"