diff --git a/.chezmoiscripts/run_before_prepare.ps1 b/.chezmoiscripts/run_before_prepare.ps1 index 28c87fb..9cffe18 100644 --- a/.chezmoiscripts/run_before_prepare.ps1 +++ b/.chezmoiscripts/run_before_prepare.ps1 @@ -30,19 +30,6 @@ if ((Test-Path "claude") -and -not (Test-Path "dot_claude")) { Write-Host " [OK] claude -> dot_claude" } -# codex → dot_codex (будет ~/.codex) -if ((Test-Path "codex") -and -not (Test-Path "dot_codex")) { - Rename-Item "codex" "dot_codex" - # Перемещаем содержимое .codex в корень и удаляем файлы skills - if (Test-Path "dot_codex/.codex") { - Move-Item "dot_codex/.codex/*" "dot_codex/" -ErrorAction SilentlyContinue - Remove-Item "dot_codex/.codex" -Recurse -Force -ErrorAction SilentlyContinue - } - # Удаляем старые файлы skills (будут созданы как симлинки) - Remove-Item "dot_codex/skills" -Recurse -Force -ErrorAction SilentlyContinue - Write-Host " [OK] codex -> dot_codex" -} - # vim/.vimrc → dot_vimrc (будет ~/.vimrc) if ((Test-Path "vim/.vimrc") -and -not (Test-Path "dot_vimrc")) { Move-Item "vim/.vimrc" "dot_vimrc" diff --git a/.chezmoiscripts/run_once_after_setup-skills.ps1 b/.chezmoiscripts/run_once_after_setup-skills.ps1 index 90f977a..dfc79fb 100644 --- a/.chezmoiscripts/run_once_after_setup-skills.ps1 +++ b/.chezmoiscripts/run_once_after_setup-skills.ps1 @@ -30,14 +30,6 @@ if (Test-Path "$env:USERPROFILE\.claude\skills") { New-Item -ItemType SymbolicLink -Path "$env:USERPROFILE\.claude\skills" -Target $skillsSource Write-Host "Created: ~/.claude/skills -> ~/.agents/skills" -# Codex -New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.codex\skills" | Out-Null -if (Test-Path "$env:USERPROFILE\.codex\skills\conventional-commits") { - Remove-Item "$env:USERPROFILE\.codex\skills\conventional-commits" -Force -} -New-Item -ItemType SymbolicLink -Path "$env:USERPROFILE\.codex\skills\conventional-commits" -Target "$skillsSource\conventional-commits" -Write-Host "Created: ~/.codex/skills/conventional-commits -> ~/.agents/skills/conventional-commits" - # Opencode New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.config\opencode" | Out-Null if (Test-Path "$env:USERPROFILE\.config\opencode\skills") { diff --git a/.gitignore b/.gitignore index c607fd5..cc89732 100644 --- a/.gitignore +++ b/.gitignore @@ -10,10 +10,6 @@ opencode/.config/opencode/package.json opencode/.config/opencode/node_modules/ opencode/.config/opencode/bun.lock -# Codex local files -codex/.codex/ -!codex/.codex/skills/ - # Gemini local files gemini/.gemini/ !gemini/.gemini/policies/ diff --git a/AGENTS.md b/AGENTS.md index df3551d..e374b1c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,7 @@ This repository contains personal dotfiles and configurations for a highly produ * **Gemini CLI:** Configured via `gemini/.gemini/`. * **Policies:** `readonly-commands.toml` allows critical commands like `git status`, `git diff`, and `git add` to run without confirmation. * **Opencode:** MCP-based assistant configuration (`opencode.json`) with support for documentation, search, and web-reading. -* **Skill Bridges:** Symlinks are used to share skills across Claude, Codex, and Opencode. +* **Skill Bridges:** Symlinks are used to share skills across Claude and Opencode. ## 🚀 Usage & Maintenance diff --git a/README.md b/README.md index 27ad237..0172021 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Chezmoi использует отдельный source dir `chezmoi/` внутр | `vscode` | `server-env-setup` для VS Code Server в WSL: секреты + авто-прокси | | `chatgpt` | Отдельный ярлык `ChatGPT Proxy` с запретом прямого запуска при ошибке | -`install.sh` дополнительно настраивает opencode (симлинки, MCP-конфиг), claude (симлинкает `settings.local.json` из репозитория) и подключает bash_custom. +`install.sh` дополнительно настраивает opencode (симлинки, MCP-конфиг), claude (симлинк `~/.claude/skills` на общие скиллы) и подключает bash_custom. ### Windows (chezmoi) @@ -76,9 +76,9 @@ Chezmoi использует отдельный source dir `chezmoi/` внутр | Инструмент | Что настраивается | |------------|-------------------| | **Agents** | Каноничное хранилище скиллов: `~/.agents/skills//SKILL.md` | -| **Claude** | `~/.claude/skills` -> симлинк на agents; `settings.local.json` -> симлинк на репозиторий (базовые permissions + statusLine); `statusline-command.sh` -> копия из репозитория; `settings.json` - управляется Claude Code | +| **Claude** | `~/.claude/skills` -> симлинк на agents; настройки (`settings.json`, `settings.local.json`) - управляются Claude Code на машине | | **[Opencode](https://github.com/anomalyco/opencode)** | MCP-серверы (context7, web-reader, playwright и др.), `~/.config/opencode/skills` -> симлинк на agents | -| **Codex** | `~/.codex/skills/` -> симлинки на agents | +| **Codex** | `~/.codex` - обычный каталог, управляется Codex; скиллы - из общего хранилища agents | | **Gemini** | Политики для CLI: pre-approved readonly команды | ## Настройка @@ -221,5 +221,5 @@ stow проще и покрывает весь Linux/WSL workflow. chezmoi ну **Почему Windows PowerShell 5.1 не трогается?** Если форсировать UTF-8 и менять профиль PS 5.1, можно сломать старые скрипты, `.bat`/`.cmd` и консольные утилиты. PS 7 - основной shell, PS 5.1 - режим совместимости. -**Почему Claude `settings.local.json` - симлинк, а `settings.json` - нет?** -Claude Code перезаписывает `settings.json` при каждом "always allow" - этот файл принадлежит Claude Code. Базовые permissions и statusLine живут в `settings.local.json`, который Claude Code только читает и мержит с `settings.json`. Симлинк из `~/.claude/settings.local.json` на файл в репозитории безопасен. Машинно-специфичные настройки (`model`, `effortLevel`) - в `settings.json`, управляются через Claude Code. +**Почему у Claude в репозитории нет настроек?** +Все настройки Claude Code (`settings.json`, `settings.local.json`) принадлежат самому Claude Code: он перезаписывает их при каждом "always allow" и при смене модели. Список разрешений не ведём вручную - работаем с autoapprove. Из репозитория линкуются только скиллы. Машинно-специфичные настройки (`model`, `effortLevel`) - в `settings.json`, управляются через Claude Code. diff --git a/claude/.claude/.gitignore b/claude/.claude/.gitignore index 4902892..b84b874 100644 --- a/claude/.claude/.gitignore +++ b/claude/.claude/.gitignore @@ -2,6 +2,3 @@ * !skills !.gitignore -!settings.local.json -!statusline-command.sh -!CLAUDE.md diff --git a/claude/.claude/CLAUDE.md b/claude/.claude/CLAUDE.md deleted file mode 100644 index 3e9da61..0000000 --- a/claude/.claude/CLAUDE.md +++ /dev/null @@ -1,19 +0,0 @@ -# Глобальный словарь рабочего процесса - -Термины владельца, общие для всех проектов на этой машине. - -## Ревью по температуре - -Ось — сколько авторского контекста несёт ревьюер: - -- **Горячее ревью** — автор сам перечитывает свой артефакт глазами читателя, - не автора. Контекст полный, дёшево. Текстовые огрехи правит по месту молча; - находку уровня решений (противоречие, дыра в охвате) предъявляет владельцу, - не исправляя и не замалчивая. -- **Тёплое ревью** — другой агент, получивший бриф. Контекст частичный: знает - замысел, но не варился в нём. -- **Холодное ревью** — свежая сессия или субагент без общего контекста. - Независимо, дороже всех. - -Просьба «перечитай свежим взглядом» двусмысленна — уточни температуру; -по умолчанию имеется в виду горячее. diff --git a/claude/.claude/settings.local.json b/claude/.claude/settings.local.json deleted file mode 100644 index cd22325..0000000 --- a/claude/.claude/settings.local.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(cat *)", - "Bash(ls)", - "Bash(ls *)", - "Bash(wc *)", - "Bash(head *)", - "Bash(tail *)", - "Bash(find *)", - "Bash(tree)", - "Bash(tree *)", - "Bash(file *)", - "Bash(stat *)", - "Bash(diff *)", - "Bash(realpath *)", - "Bash(readlink *)", - "Bash(basename *)", - "Bash(dirname *)", - "Bash(sort *)", - "Bash(uniq *)", - "Bash(cut *)", - "Bash(awk *)", - "Bash(sed *)", - "Bash(grep *)", - "Bash(rg *)", - "Bash(jq *)", - "Bash(yq *)", - "Bash(tr *)", - "Bash(echo *)", - "Bash(printf *)", - "Bash(which *)", - "Bash(type *)", - "Bash(command -v *)", - "Bash(env)", - "Bash(env *)", - "Bash(printenv)", - "Bash(printenv *)", - "Bash(pwd)", - "Bash(uname *)", - "Bash(whoami)", - "Bash(id)", - "Bash(date)", - "Bash(date *)", - "Bash(hostname)", - "Bash(git log*)", - "Bash(git diff*)", - "Bash(git status*)", - "Bash(git show*)", - "Bash(git branch*)", - "Bash(git remote -v*)", - "Bash(git remote show*)", - "Bash(git tag*)", - "Bash(git stash list*)", - "Bash(git rev-parse*)", - "Bash(git ls-files*)", - "Bash(git blame*)", - "Bash(git shortlog*)", - "Bash(git config --get*)", - "Bash(git config --list*)", - "Bash(git fetch*)", - "Bash(git add*)", - "Bash(docker ps*)", - "Bash(docker logs*)", - "Bash(docker inspect*)", - "Bash(docker images*)", - "Bash(docker stats*)", - "Bash(docker info*)", - "Bash(docker version*)", - "Bash(docker compose ps*)", - "Bash(docker compose logs*)", - "Bash(docker compose:*)", - "Bash(docker restart:*)", - "Bash(docker exec:*)", - "Bash(docker run:*)", - "Bash(docker stop:*)", - "Bash(docker rm:*)", - "Bash(curl *)", - "Bash(wget *)", - "Bash(mkdir *)", - "Bash(cp *)", - "Bash(mv *)", - "Bash(touch *)", - "Bash(chmod *)", - "Bash(ln *)", - "Bash(npm *)", - "Bash(npx *)", - "Bash(yarn *)", - "Bash(pnpm *)", - "Bash(bun *)", - "Bash(go *)", - "Bash(cargo *)", - "Bash(make *)", - "Bash(python *)", - "Bash(python3 *)", - "Bash(pip *)", - "Bash(pip3 *)", - "Bash(pytest *)", - "Bash(node *)", - "Bash(deno *)", - "Bash(stow *)", - "Bash(gh api:*)", - "Bash(gh search:*)", - "Bash(gh issue:*)", - "Bash(gh repo:*)", - "Bash(codex *)", - "Bash(timeout 600 codex exec *)", - "Bash(rm -f /tmp/claude-plan-*)", - "Bash(rm -f /tmp/codex-*)", - "WebSearch", - "mcp__claude_ai_Context7__resolve-library-id", - "Write(/tmp/claude-plan-*)", - "Write(/tmp/codex-*)" - ] - }, - "statusLine": { - "type": "command", - "command": "bash ~/.claude/statusline-command.sh" - } -} diff --git a/claude/.claude/statusline-command.sh b/claude/.claude/statusline-command.sh deleted file mode 100755 index 686c43d..0000000 --- a/claude/.claude/statusline-command.sh +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env bash -# Статусная строка Claude Code. JSON приходит на stdin. -# -# user@host:.../dir (branch) | Model Name (NM context) ctx:N% | 5h:N% 7d:N% -# -# Работает в Linux и в Git Bash на Windows: без `hostname -s`, jq не обязателен. - -# Порог, после которого путь сокращается до .../<последний каталог> -MAX_PATH_LEN="${STATUSLINE_MAX_PATH_LEN:-28}" - -input="$(cat)" - -# ── Разбор JSON ─────────────────────────────────────────────────────── - -# Числовое поле $2 внутри объекта $1 (резервный разбор, без jq). -json_num() { - printf '%s' "$input" \ - | tr -d ' \t\n\r' \ - | grep -o "\"$1\":{[^}]*}" \ - | grep -o "\"$2\":[0-9.eE+-]*" \ - | head -1 \ - | cut -d: -f2 -} - -# Строковое поле $2 внутри объекта $1 (резервный разбор, без jq). -json_str() { - printf '%s' "$input" \ - | tr -d '\n\r' \ - | grep -o "\"$1\"[[:space:]]*:[[:space:]]*{[^}]*}" \ - | grep -o "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" \ - | head -1 \ - | sed 's/.*"[[:space:]]*:[[:space:]]*"//; s/"$//' -} - -if command -v jq >/dev/null 2>&1; then - IFS=$'\t' read -r cwd display_name ctx_pct rate5h rate7d </dev/null) -EOF -else - cwd="$(json_str workspace current_dir)" - display_name="$(json_str model display_name)" - ctx_pct="$(json_num context_window used_percentage)" - rate5h="$(json_num five_hour used_percentage)" - rate7d="$(json_num seven_day used_percentage)" -fi - -# Проценты в JSON дробные — округляем до целых. -round_pct() { - case "$1" in - ''|null) return 0 ;; - esac - awk -v v="$1" 'BEGIN { printf "%d", (v < 0 ? -int(-v + 0.5) : int(v + 0.5)) }' 2>/dev/null -} - -ctx_pct="$(round_pct "$ctx_pct")" -rate5h="$(round_pct "$rate5h")" -rate7d="$(round_pct "$rate7d")" - -# ── Путь ────────────────────────────────────────────────────────────── - -# В Git Bash current_dir может прийти с обратными слэшами. -path="${cwd//\\//}" -home="${HOME//\\//}" - -if [ -n "$home" ]; then - if [ "$path" = "$home" ]; then - path="~" - else - case "$path" in - "$home"/*) path="~${path#"$home"}" ;; - esac - fi -fi - -if [ "${#path}" -gt "$MAX_PATH_LEN" ]; then - path=".../${path##*/}" -fi - -# ── Остальные секции ────────────────────────────────────────────────── - -branch="" -if [ -n "$cwd" ]; then - branch="$(git -C "$cwd" rev-parse --abbrev-ref HEAD 2>/dev/null)" -fi - -# Размер контекста уже внутри display_name, убираем только префикс «Claude ». -model="${display_name#Claude }" - -user="${USER:-${USERNAME:-$(id -un 2>/dev/null)}}" -host="${HOSTNAME:-$(hostname 2>/dev/null)}" -host="${host%%.*}" - -GREEN=$'\033[32m' -YELLOW=$'\033[33m' -PURPLE=$'\033[35m' -CYAN=$'\033[36m' -RESET=$'\033[0m' - -out="${GREEN}${user}@${host}${RESET}:${YELLOW}${path}${RESET}" -[ -n "$branch" ] && out="${out} ${PURPLE}(${branch})${RESET}" -out="${out} | ${CYAN}${model}${RESET}" -[ -n "$ctx_pct" ] && out="${out} ctx:${ctx_pct}%" - -rates="" -[ -n "$rate5h" ] && rates="${rates} 5h:${rate5h}%" -[ -n "$rate7d" ] && rates="${rates} 7d:${rate7d}%" -[ -n "$rates" ] && out="${out} |${rates}" - -printf '%s\n' "$out" diff --git a/claude/.stow-local-ignore b/claude/.stow-local-ignore index a3be5d9..6694106 100644 --- a/claude/.stow-local-ignore +++ b/claude/.stow-local-ignore @@ -1,3 +1,3 @@ -# Claude Code управляется вручную через install.sh (копирование, не симлинк). +# Claude Code управляется вручную через install.sh (точечные симлинки). # Этот файл не даёт stow случайно создать симлинк ~/.claude. \.claude diff --git a/codex/.codex/skills/conventional-commits b/codex/.codex/skills/conventional-commits deleted file mode 120000 index 390be38..0000000 --- a/codex/.codex/skills/conventional-commits +++ /dev/null @@ -1 +0,0 @@ -../../../agents/.agents/skills/conventional-commits \ No newline at end of file diff --git a/codex/.codex/skills/coordinator-loop b/codex/.codex/skills/coordinator-loop deleted file mode 120000 index 450ba52..0000000 --- a/codex/.codex/skills/coordinator-loop +++ /dev/null @@ -1 +0,0 @@ -../../../agents/.agents/skills/coordinator-loop \ No newline at end of file diff --git a/install.sh b/install.sh index d114d36..0d32558 100755 --- a/install.sh +++ b/install.sh @@ -144,28 +144,36 @@ wsl_conf_has_boot_command() { ' "$1" 2>/dev/null } -# Миграция: если ~/.claude — stow-симлинк, раскрыть в директорию. -migrate_claude_stow_symlink() { - [ -L "$HOME/.claude" ] || return 0 +# Миграция: если $1 — stow-симлинк (пакет был свёрнут в репозиторий), +# раскрыть в настоящий каталог и перенести содержимое. Остальные аргументы — +# имена git-tracked файлов, которые остаются в репозитории. +migrate_folded_symlink() { + local link="$1" + shift + [ -L "$link" ] || return 0 - info "Миграция: ~/.claude — симлинк, преобразую в директорию..." + info "Миграция: $link — симлинк, преобразую в директорию..." local stow_dir - stow_dir="$(readlink -f "$HOME/.claude" 2>/dev/null || true)" - rm "$HOME/.claude" - mkdir -p "$HOME/.claude" + stow_dir="$(readlink -f "$link" 2>/dev/null || true)" + rm "$link" + mkdir -p "$link" if [ -n "$stow_dir" ] && [ -d "$stow_dir" ]; then for item in "$stow_dir"/* "$stow_dir"/.[!.]*; do - [ -e "$item" ] || continue - local base + [ -e "$item" ] || [ -L "$item" ] || continue + local base skip s base=$(basename "$item") # git-tracked файлы остаются в репозитории - case "$base" in .gitignore|settings.local.json|skills) continue ;; esac - mv "$item" "$HOME/.claude/" 2>/dev/null || true + skip="" + for s in "$@"; do + [ "$base" = "$s" ] && skip=1 + done + [ -n "$skip" ] && continue + mv "$item" "$link/" 2>/dev/null || true done - info "Рантайм-данные перенесены в ~/.claude/" + info "Рантайм-данные перенесены в $link/" else - warn "Не удалось прочитать источник старого симлинка ~/.claude, продолжаю без миграции данных" + warn "Не удалось прочитать источник старого симлинка $link, продолжаю без миграции данных" fi } @@ -178,7 +186,7 @@ migrate_claude_stow_symlink() { # # Обход повторяет логику stow, а не перебирает файлы подряд: если каталога в # HOME нет, конфликта быть не может и всё поддерево пропускается. Это заодно -# отсекает свёрнутые пакеты (~/.codex, ~/.vscode-server) с их гигабайтами +# отсекает свёрнутые пакеты (~/.vscode-server) с их гигабайтами # рантайм-данных. backup_conflicting_stow_targets() { local pkg="" @@ -370,7 +378,7 @@ if ! command -v "$STOW_BIN" &>/dev/null; then exit 1 fi -PACKAGES=(bash tmux vim wezterm kitty vscode agents codex gemini proxy-client chatgpt) +PACKAGES=(bash tmux vim wezterm kitty vscode agents gemini proxy-client chatgpt) AVAILABLE_PACKAGES=() info "Применяю stow пакеты..." @@ -445,52 +453,41 @@ if [ -f "$OPENCODE_JSON_SRC" ]; then fi # ── Claude Code ────────────────────────────────────────────────────── -# settings.local.json ЛИНКУЕТСЯ из репозитория (базовые permissions + statusLine). -# Claude Code не модифицирует settings.local.json — только читает и мержит -# с settings.json, который Claude Code создаёт и обновляет самостоятельно. -# Машинно-специфичные настройки (model, effortLevel) — в settings.json. +# Из репозитория линкуются только skills. Настройки (settings.json, +# settings.local.json) принадлежат Claude Code и живут только на машине. -migrate_claude_stow_symlink +migrate_folded_symlink "$HOME/.claude" .gitignore skills mkdir -p "$HOME/.claude" +# Уборка артефактов прежних версий установщика: битые симлинки на удалённые +# из репозитория файлы и забытая копия statusline (наша — по строке-маркеру). +for stale in CLAUDE.md settings.local.json; do + stale_path="$HOME/.claude/$stale" + if [ -L "$stale_path" ]; then + case "$(readlink "$stale_path" 2>/dev/null || true)" in + "$DOTFILES_DIR"/*) + rm "$stale_path" + info "Удалён устаревший симлинк ~/.claude/$stale" + ;; + esac + fi +done +if [ -f "$HOME/.claude/statusline-command.sh" ] && [ ! -L "$HOME/.claude/statusline-command.sh" ] \ + && grep -q "Статусная строка Claude Code" "$HOME/.claude/statusline-command.sh" 2>/dev/null; then + rm "$HOME/.claude/statusline-command.sh" + info "Удалена устаревшая копия ~/.claude/statusline-command.sh" +fi + # Skills (симлинк) if [ -d "$SKILLS_SRC" ]; then ensure_symlink "$SKILLS_SRC" "$HOME/.claude/skills" fi -# Глобальный CLAUDE.md — словарь рабочего процесса (симлинк) -CLAUDE_MD_SRC="$DOTFILES_DIR/claude/.claude/CLAUDE.md" -if [ -f "$CLAUDE_MD_SRC" ]; then - ensure_symlink "$CLAUDE_MD_SRC" "$HOME/.claude/CLAUDE.md" -fi +# ── Codex ──────────────────────────────────────────────────────────── +# Пакета в репозитории нет: ~/.codex — обычный каталог, его заполняет Codex. +# Миграция раскрывает старый свёрнутый симлинк и переносит рантайм-данные. -# 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 - -# statusline-command.sh — копия из репозитория (на Windows симлинки ненадёжны) -CLAUDE_STATUSLINE_SRC="$DOTFILES_DIR/claude/.claude/statusline-command.sh" -CLAUDE_STATUSLINE_DST="$HOME/.claude/statusline-command.sh" -if [ -f "$CLAUDE_STATUSLINE_SRC" ]; then - install -m 755 "$CLAUDE_STATUSLINE_SRC" "$CLAUDE_STATUSLINE_DST" - info "statusline-command.sh скопирован в ~/.claude/" -fi +migrate_folded_symlink "$HOME/.codex" .gitkeep # ── Bash: .bashrc ──────────────────────────────────────────────────── diff --git a/tests/test_install.sh b/tests/test_install.sh index 4a12913..4bd368f 100755 --- a/tests/test_install.sh +++ b/tests/test_install.sh @@ -21,7 +21,8 @@ assert_file_contains() { assert_not_exists() { local path="$1" - [ ! -e "$path" ] || fail "Path should not exist: $path" + # -e не видит битые симлинки, поэтому проверяем и -L + [ ! -e "$path" ] && [ ! -L "$path" ] || fail "Path should not exist: $path" } setup_fixture() { @@ -37,7 +38,7 @@ setup_fixture() { # zed/.config/opencode и т.п.) весят гигабайты, переполняют /tmp и ломают # stow конфликтами, поэтому в тестовое дерево не попадает. git -C "$REPO_ROOT" ls-files -z --cached --others --exclude-standard -- \ - install.sh bash tmux vim zed kitty wezterm vscode agents opencode codex gemini \ + install.sh bash tmux vim zed kitty wezterm vscode agents opencode gemini \ claude proxy-client chatgpt scripts \ | tar -C "$REPO_ROOT" --null -T - -cf - \ | tar -xf - -C "$repo_root" @@ -225,22 +226,71 @@ test_existing_wezterm_config_is_backed_up() { assert_file_contains "$backup" 'require("wezterm")' } -test_folded_package_runtime_files_are_not_backed_up() { +test_codex_runtime_dir_is_unmanaged() { local fixture repo_root home_dir backup fixture="$(setup_fixture)" repo_root="$fixture/repo" home_dir="$fixture/home" + # ~/.codex — обычный каталог с рантайм-данными Codex; install.sh его не трогает. + mkdir -p "$home_dir/.codex" + printf '%s\n' '{}' > "$home_dir/.codex/auth.json" + + run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install.out" + run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install2.out" + + [ -d "$home_dir/.codex" ] || fail "Expected ~/.codex to stay a directory" + [ ! -L "$home_dir/.codex" ] || fail "~/.codex must not become a symlink" + backup="$(find "$repo_root" "$home_dir" -name '*.bak.*' -print -quit)" + [ -z "$backup" ] || fail "Codex runtime files must not be backed up: $backup" + assert_file_contains "$home_dir/.codex/auth.json" "{}" +} + +test_migrates_folded_codex_symlink() { + local fixture repo_root home_dir legacy_dir + fixture="$(setup_fixture)" + repo_root="$fixture/repo" + home_dir="$fixture/home" mkdir -p "$home_dir" + # Старая схема: ~/.codex — симлинк на каталог внутри репозитория с рантайм-данными + legacy_dir="$fixture/legacy-repo-codex" + mkdir -p "$legacy_dir" + printf '%s\n' '{"token":"x"}' > "$legacy_dir/auth.json" + printf '%s\n' 'HTTPS_PROXY=http://hproxy.local' > "$legacy_dir/.env" + ln -s "$legacy_dir" "$home_dir/.codex" + run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install.out" - # ~/.codex свёрнут в симлинк на пакет: файлы внутри — уже наши, не конфликт. - printf '%s\n' '{}' > "$home_dir/.codex/auth.json" - run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install2.out" + [ -d "$home_dir/.codex" ] || fail "Expected ~/.codex to become a directory" + [ ! -L "$home_dir/.codex" ] || fail "Expected ~/.codex to stop being a symlink" + assert_file_contains "$home_dir/.codex/auth.json" '"token":"x"' + assert_file_contains "$home_dir/.codex/.env" "hproxy.local" +} - backup="$(find "$repo_root" "$home_dir" -name '*.bak.*' -print -quit)" - [ -z "$backup" ] || fail "Folded package contents must not be backed up: $backup" - assert_file_contains "$home_dir/.codex/auth.json" "{}" +test_claude_stale_artifacts_are_removed() { + local fixture repo_root home_dir + fixture="$(setup_fixture)" + repo_root="$fixture/repo" + home_dir="$fixture/home" + mkdir -p "$home_dir/.claude" + + # Артефакты прежнего установщика: симлинки на удалённые из репозитория файлы + # и наша копия statusline (узнаётся по строке-маркеру). + ln -s "$repo_root/claude/.claude/CLAUDE.md" "$home_dir/.claude/CLAUDE.md" + ln -s "$repo_root/claude/.claude/settings.local.json" "$home_dir/.claude/settings.local.json" + printf '#!/usr/bin/env bash\n# Статусная строка Claude Code. JSON приходит на stdin.\n' \ + > "$home_dir/.claude/statusline-command.sh" + # Чужой файл с похожим именем трогать нельзя + printf 'echo mine\n' > "$home_dir/.claude/statusline-mine.sh" + chmod +x "$home_dir/.claude/statusline-command.sh" + + run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >"$fixture/install.out" + + assert_not_exists "$home_dir/.claude/CLAUDE.md" + assert_not_exists "$home_dir/.claude/settings.local.json" + assert_not_exists "$home_dir/.claude/statusline-command.sh" + assert_file_contains "$home_dir/.claude/statusline-mine.sh" "echo mine" + [ -L "$home_dir/.claude/skills" ] || fail "Expected skills symlink to be created" } test_migrates_legacy_zed_config_symlink() { @@ -358,53 +408,7 @@ test_does_not_follow_unrelated_local_symlink() { assert_not_exists "$unrelated_local/bin" } -test_settings_local_is_symlinked_to_repo() { - local fixture repo_root home_dir - fixture="$(setup_fixture)" - repo_root="$fixture/repo" - home_dir="$fixture/home" - mkdir -p "$home_dir" - - run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out - - [ -L "$home_dir/.claude/settings.local.json" ] || fail "Expected settings.local.json to be a symlink" - local target - target="$(readlink -f "$home_dir/.claude/settings.local.json")" - local expected - expected="$(readlink -f "$repo_root/claude/.claude/settings.local.json")" - [ "$target" = "$expected" ] || fail "settings.local.json points to $target, expected $expected" -} - -test_settings_local_backup_on_existing_file() { - local fixture repo_root home_dir - fixture="$(setup_fixture)" - repo_root="$fixture/repo" - home_dir="$fixture/home" - mkdir -p "$home_dir/.claude" - echo '{"model":"sonnet"}' > "$home_dir/.claude/settings.local.json" - - run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out - - [ -L "$home_dir/.claude/settings.local.json" ] || fail "Expected settings.local.json to become a symlink" - local backup - backup="$(find "$home_dir/.claude" -maxdepth 1 -name 'settings.local.json.bak.*' -print -quit)" - [ -n "$backup" ] || fail "Expected backup for existing settings.local.json" - assert_file_contains "$backup" '"model":"sonnet"' -} - -test_settings_local_dir_is_skipped() { - local fixture repo_root home_dir - fixture="$(setup_fixture)" - repo_root="$fixture/repo" - home_dir="$fixture/home" - mkdir -p "$home_dir/.claude/settings.local.json" - - run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out - - [ -d "$home_dir/.claude/settings.local.json" ] || fail "Expected directory to be preserved (not replaced)" -} - -test_stow_migration_preserves_legacy_settings_local() { +test_stow_migration_preserves_legacy_files() { local fixture repo_root home_dir stow_target fixture="$(setup_fixture)" repo_root="$fixture/repo" @@ -414,14 +418,14 @@ test_stow_migration_preserves_legacy_settings_local() { # Simulate old stow layout: ~/.claude → repo dir stow_target="$repo_root/claude/.claude" ln -s "$stow_target" "$home_dir/.claude" - # Put a legacy settings.local.json in the stow target (would have been gitignored before) + # Put a legacy local file in the stow target (раньше settings.local.json + # был git-tracked и пропускался; теперь это машинные данные — переносим) echo '{"model":"opus"}' > "$stow_target/legacy-local.json" run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out [ -d "$home_dir/.claude" ] || fail "Expected ~/.claude to become a directory" [ ! -L "$home_dir/.claude" ] || fail "Expected ~/.claude to stop being a symlink" - [ -L "$home_dir/.claude/settings.local.json" ] || fail "Expected settings.local.json symlink after migration" # legacy-local.json should have been moved from stow dir to real ~/.claude [ -f "$home_dir/.claude/legacy-local.json" ] || fail "Expected legacy file to be migrated" } @@ -643,15 +647,14 @@ main() { test_foreign_kitty_config_symlink_is_backed_up test_rerun_does_not_backup_managed_kitty_config test_existing_wezterm_config_is_backed_up - test_folded_package_runtime_files_are_not_backed_up + test_codex_runtime_dir_is_unmanaged + test_migrates_folded_codex_symlink + test_claude_stale_artifacts_are_removed test_migrates_legacy_zed_config_symlink test_stops_legacy_migration_when_listing_fails test_does_not_migrate_unrelated_config_symlink test_does_not_follow_unrelated_local_symlink - test_settings_local_is_symlinked_to_repo - test_settings_local_backup_on_existing_file - test_settings_local_dir_is_skipped - test_stow_migration_preserves_legacy_settings_local + test_stow_migration_preserves_legacy_files test_rerun_does_not_duplicate_bashrc_line test_repairs_nested_skills_directory_bug test_preserves_nonempty_skills_directory