refactor(agents): убраны настройки Claude из git и пакет codex

Claude: из репозитория удалены settings.local.json (разрешения не нужны
при autoapprove), statusline-command.sh и устаревший CLAUDE.md; линкуются
только skills. install.sh убирает артефакты прежних прогонов.

Codex: пакет удалён целиком, ~/.codex - обычный каталог. Персональные
мостики скиллов убраны; миграция раскрывает свёрнутый симлинк и переносит
рантайм-данные. Чищены chezmoi-скрипты.

install.sh: migrate_claude_stow_symlink обобщена в migrate_folded_symlink.
Тесты: 3 устаревших удалены, 3 добавлены; assert_not_exists ловит битые
симлинки.
This commit is contained in:
Dmitry Dementiev committed 2026-10-10 17:15:34 +02:00
1 parent 346b3bfc61
commit c3efe60a00
14 files changed
+122 -407

No files matched your search

-13
View File
@@ -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"
@@ -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") {
-4
View File
@@ -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/
+1 -1
View File
@@ -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
+5 -5
View File
@@ -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/<name>/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.
-3
View File
@@ -2,6 +2,3 @@
*
!skills
!.gitignore
!settings.local.json
!statusline-command.sh
!CLAUDE.md
-19
View File
@@ -1,19 +0,0 @@
# Глобальный словарь рабочего процесса
Термины владельца, общие для всех проектов на этой машине.
## Ревью по температуре
Ось — сколько авторского контекста несёт ревьюер:
- **Горячее ревью** — автор сам перечитывает свой артефакт глазами читателя,
не автора. Контекст полный, дёшево. Текстовые огрехи правит по месту молча;
находку уровня решений (противоречие, дыра в охвате) предъявляет владельцу,
не исправляя и не замалчивая.
- **Тёплое ревью** — другой агент, получивший бриф. Контекст частичный: знает
замысел, но не варился в нём.
- **Холодное ревью** — свежая сессия или субагент без общего контекста.
Независимо, дороже всех.
Просьба «перечитай свежим взглядом» двусмысленна — уточни температуру;
по умолчанию имеется в виду горячее.
-120
View File
@@ -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"
}
}
-116
View File
@@ -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 <<EOF
$(printf '%s' "$input" | jq -r '
def num: if type == "number" then tostring else "" end;
[ (.workspace.current_dir // ""),
(.model.display_name // ""),
(.context_window.used_percentage | num),
(.rate_limits.five_hour.used_percentage | num),
(.rate_limits.seven_day.used_percentage | num)
] | @tsv' 2>/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"
+1 -1
View File
@@ -1,3 +1,3 @@
# Claude Code управляется вручную через install.sh (копирование, не симлинк).
# Claude Code управляется вручную через install.sh (точечные симлинки).
# Этот файл не даёт stow случайно создать симлинк ~/.claude.
\.claude
-1
View File
@@ -1 +0,0 @@
../../../agents/.agents/skills/conventional-commits
-1
View File
@@ -1 +0,0 @@
../../../agents/.agents/skills/coordinator-loop
+49 -52
View File
@@ -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 ────────────────────────────────────────────────────
+66 -63
View File
@@ -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