feat: install panel as launchd service, refuse install over SSH (#4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-18 20:57:08 +04:00
parent 88124cf6d8
commit 7958314547
6 changed files with 182 additions and 75 deletions

View File

@ -1,15 +1,17 @@
# Архитектура
Два процесса, связанные только файлами в `~/.cache/claude-agents/`: хук внутри каждой сессии
Claude Code пишет события, скрипт iTerm2 их читает и рисует панель.
Три части, связанные файлами в `~/.cache/claude-agents/` и адресом `127.0.0.1:8766`: хук внутри
каждой сессии Claude Code пишет события, служба launchd их читает и отдаёт страницу, скрипт iTerm2
показывает эту страницу в Toolbelt. iTerm2 для работы панели не нужен.
```
claude (каждая сессия) ──hook──▶ agents_panel_hook.sh ──append──▶ ~/.cache/claude-agents/<sid>.jsonl
│
iTerm2 AutoLaunch: claude_agents.py ◀───── read (poll 1.5 s) ────────────┘
├─ + iTerm2 API (вкладки, профиль), ps, tmux, транскрипты ~/.claude/projects, git
launchd: claude_agents.py --headless ◀──── read (poll 1.5 s) ────────────┘
├─ + ps, tmux, транскрипты ~/.claude/projects, git
├─ + api.anthropic.com oauth/usage, prepaid/credits (OAuth из Keychain)
└─ HTTP 127.0.0.1:8766 ──▶ web view tool «Claude» в Toolbelt
└─ HTTP 127.0.0.1:8766 ──▶ web view «Claude» в Toolbelt (регистрирует claude_agents.py в iTerm2)
└─▶ браузер; с iPad — SSH local forward в Termius
```
## Модули
@ -17,19 +19,23 @@ iTerm2 AutoLaunch: claude_agents.py ◀───── read (poll 1.5 s) ──
| Модуль | Ответственность | Зависит от |
|---|---|---|
| `files/agents_panel_hook.sh` | Событие хука → строка jsonl с PID, вкладкой iTerm2 или панелью tmux | `jq`, `ps` |
| `files/claude_agents.py` — сессии | `load_sessions`/`replay`: jsonl → состояние сессий и субагентов; `build_tree` сводит с живыми вкладками, процессами, tmux | файлы хука, iTerm2 API, `ps`, `tmux`, транскрипты, `git` |
| `files/claude_agents.py` — сессии | `load_sessions`/`replay`: jsonl → состояние сессий и субагентов; `build_tree` сводит с процессами и tmux | файлы хука, `ps`, `tmux`, транскрипты, `git` |
| `files/claude_agents.py` — лимиты | `fetch_usage`/`fetch_balance` + `parse_limits`/`parse_balance` | Keychain/`~/.claude/.credentials.json`, `~/.claude.json`, api.anthropic.com, claude.ai |
| `files/claude_agents.py` — UI | `render_*`, `handle` (HTTP), `activate`/`open_worktree`, `theme_poller` | `state`, iTerm2 API |
| `files/claude_agents.py` — UI | `render_*`, `handle` (HTTP), `activate`/`open_worktree` (tmux) | `state` |
| `files/claude_agents.py` — запуск | `run`: `serve` (служба, `--headless`) или `register_toolbelt` (iTerm2) | launchd, `iterm2` |
| `files/launchagent.py` | plist службы `com.andrey.claude-agents` | — |
| `files/settings_hooks.py` | Идемпотентно добавить/убрать хуки в `~/.claude/settings.json` | — |
| `install.sh` / `uninstall.sh` | Разложить/убрать файлы, включить Python API iTerm2, перезапустить панель | `settings_hooks.py` |
| `install.sh` / `uninstall.sh` | Разложить/убрать файлы, поставить/снять службу, включить Python API iTerm2 | `settings_hooks.py`, `launchagent.py`, `launchctl` |
## Границы
- Хук не зависит от панели и не должен влиять на Claude Code: без вывода, всегда `exit 0`.
- Панель только читает данные Claude Code (jsonl, транскрипты, `~/.claude.json`) и ничего туда не пишет;
её единственная запись — удаление собственных логов старше `keep_logs_days`.
- `claude_agents.py` — только стандартная библиотека + `iterm2`; импорт без iTerm2 должен работать.
- Порт занимает только служба; скрипт iTerm2 сервер не поднимает и данные iTerm2 не передаёт.
- `claude_agents.py` — только стандартная библиотека + опционально `iterm2`; импорт без iTerm2 должен работать.
- HTTP слушает только `127.0.0.1`.
- Фон панели выбирается на клиенте и хранится в браузере/web view; сервер о нём не знает.
## Открытые вопросы
- Docstring `claude_agents.py` ссылается на несуществующий `settings-hooks.json` (теперь это `settings_hooks.py` + `install.sh`).
- Автотестов нет; `claude_agents.py` — один файл на 1500 строк.
- После перезагрузки Mac без входа на нём (только SSH) служба не стартует, Keychain заблокирован.
- `claude_agents.py` — один файл на ~1400 строк.

View File

@ -1,55 +1,63 @@
# cc-agents-iterm-plugin
## Что это
Панель «Claude» для iTerm2 Toolbelt. Сверху — дерево активных сессий Claude Code и их субагентов
(название, модель, размер контекста, worktree, статус), снизу — лимиты подписки (5 ч, неделя,
разбивка по продуктам) и баланс extra usage. Для одного пользователя на его Mac: видеть, что
делают все запущенные `claude` и сколько осталось лимитов, и в один клик переходить в нужную
вкладку или панель tmux.
Панель «Claude» — страница на `http://127.0.0.1:8766/`. Сверху — дерево активных сессий Claude Code
и их субагентов (название, модель, размер контекста, worktree, статус), снизу — лимиты подписки
(5 ч, неделя, разбивка по продуктам) и баланс extra usage. Её показывает iTerm2 Toolbelt и
браузер — в том числе Safari на iPad через SSH-туннель Termius. Для одного пользователя на его
Mac: видеть, что делают все запущенные `claude` (в tmux), сколько осталось лимитов, и в один клик
переходить в нужную панель tmux.
## Стек и ограничения
- Python 3.8+, только стандартная библиотека; модуль `iterm2` предоставляет сам iTerm2
(скрипт AutoLaunch). Без зависимостей из pip — пользователь ставит панель `./install.sh`.
- Python 3.8+, только стандартная библиотека; модуль `iterm2` нужен только регистратору Toolbelt
(его даёт iTerm2). Без зависимостей из pip — пользователь ставит панель `./install.sh`.
- Хук — POSIX `sh` + `jq`. Хук никогда не мешает Claude Code: ничего не выводит, всегда `exit 0`.
- Только macOS + iTerm2 с включённым Python API. Токен OAuth берётся из Keychain
(`Claude Code-credentials`) или `~/.claude/.credentials.json`.
- macOS, launchd. Токен OAuth берётся из Keychain (`Claude Code-credentials`) или
`~/.claude/.credentials.json`, поэтому служба работает в GUI-сессии пользователя.
- Сервер слушает только `127.0.0.1` — в панели видны тексты сообщений сессий.
- Установщик не трогает чужие хуки и настройки: правит `~/.claude/settings.json` только через
`settings_hooks.py` (маркер `agents_panel_hook`), перед заменой кладёт копии в
`~/.claude-panel-backup/<дата>/`. Повторный запуск `install.sh` идемпотентен.
`~/.claude-panel-backup/<дата>/`. Повторный запуск `install.sh` идемпотентен. Запуск по SSH
`install.sh`/`uninstall.sh` отклоняют.
## Архитектура
- `files/agents_panel_hook.sh` — хук Claude Code на SessionStart/End, UserPromptSubmit, Stop,
Notification, SubagentStart/Stop, PostModelSwitch, PreToolUse(Agent|Task). Дописывает событие
в `~/.cache/claude-agents/<session_id>.jsonl` вместе с PID процесса `claude`,
`ITERM_SESSION_ID` или сокетом/панелью tmux.
- `files/claude_agents.py` — скрипт iTerm2 AutoLaunch, один файл:
- `refresher` раз в `poll_sec` перечитывает jsonl (`load_sessions` → `replay`), сверяет с живыми
сессиями iTerm2, процессами (`ps`) и панелями tmux, дочитывает из транскриптов модель и токены,
из git — worktree; собирает `state["tree"]` (`build_tree`).
- `usage_poller` / `balance_poller` ходят в `api.anthropic.com/api/oauth/usage` и
`.../prepaid/credits` (запасной путь — claude.ai с `sessionKey` из Keychain).
- HTTP-сервер на `127.0.0.1:8766` (`handle`) отдаёт страницу, которую iTerm2 показывает как web view
tool: `/`, `/fragment` (дерево), `/limits`, `/message` (последние сообщения), `/activate`,
`/open-wt`, `/raw` (отладка).
- `theme_poller` подстраивает цвета под профиль iTerm2.
- `files/claude_agents.py` — один файл, два режима (`run`):
- `--headless` (`serve`) — служба launchd `com.andrey.claude-agents`, единственный владелец порта:
- `refresher` раз в `poll_sec` перечитывает jsonl (`load_sessions` → `replay`), сверяет с
процессами (`ps`) и панелями tmux, дочитывает из транскриптов модель и токены, из git —
worktree; собирает `state["tree"]` (`build_tree`, без данных iTerm2).
- `usage_poller` / `balance_poller` ходят в `api.anthropic.com/api/oauth/usage` и
`.../prepaid/credits` (запасной путь — claude.ai с `sessionKey` из Keychain).
- HTTP-сервер (`handle`): `/`, `/fragment` (дерево), `/limits`, `/message` (последние
сообщения), `/activate` и `/open-wt` (через tmux), `/raw` (отладка).
- без флага (`register_toolbelt`) — скрипт AutoLaunch iTerm2: регистрирует web view «Claude» на
адрес службы; если порт молчит — `launchctl kickstart`. Свой сервер не поднимает.
- `files/launchagent.py` — пишет plist службы (`write <plist> <python> <script> <log>`).
- `files/settings_hooks.py` — добавляет/убирает хуки панели в `settings.json` (`add` / `remove`).
- `files/config.example.json` — все ключи настроек; рабочий файл `~/.config/claude-agents/config.json`
перечитывается на лету, кроме `port`.
- `install.sh` / `uninstall.sh [--purge]` — раскладывают файлы по местам (таблица в README.md).
- `install.sh` / `uninstall.sh [--purge]` — раскладывают файлы, ставят/убирают службу (таблица в README.md).
## Команды
- `./install.sh` — установить или обновить у себя; затем Toolbelt → Claude и перезапуск сессий `claude`.
- `./install.sh` — установить или обновить у себя (за самим Mac); затем Toolbelt → Claude и перезапуск сессий `claude`.
- `./uninstall.sh` — удалить (`--purge` — вместе с настройками и логами).
- `python3 -m unittest discover -s tests -v` — весь набор тестов (stdlib, без pytest; `node` нужен
для теста клиентского JS). Зелёным считается только весь набор.
- `python3 -m py_compile files/*.py && sh -n files/agents_panel_hook.sh && bash -n install.sh && bash -n uninstall.sh` —
синтаксическая проверка; сборки нет.
- `curl -s http://127.0.0.1:8766/raw` — что видит запущенная панель: логи хука, причины скрытия
сессий, ошибки лимитов и баланса.
сессий, ошибки лимитов и баланса. Лог службы — `~/Library/Logs/claude-agents/panel.log`,
состояние — `launchctl print gui/$(id -u)/com.andrey.claude-agents`.
## Тесты
- Автотестов пока нет. `claude_agents.py` импортируется без iTerm2 (`iterm2 = None`), так что чистые
функции (`replay`, `build_tree`, `parse_limits`, `parse_balance`, `session_name`, `settings_hooks.add/remove`)
можно покрывать pytest без iTerm2. Сеть, Keychain, `ps` и tmux в тестах мокаются.
- Фикстуры — образцы jsonl хука и ответов API в `tests/fixtures/`.
- `tests/` — `unittest`. `claude_agents.py` импортируется без iTerm2 (`iterm2 = None`). Сеть,
Keychain, launchd, `ps` и tmux в тестах подменяются; реальные `~/.claude` и `~/Library` не трогаются.
- `install.sh` и `uninstall.sh` в тестах запускаются только с `SSH_CONNECTION`/`SSH_TTY` (проверка
отказа): без охраны они остановили бы настоящую панель на порту 8766.
## Где лежит код
- Код — в `files/`; хук require-plan защищает его через `.claude/code-paths`. `install.sh` и

View File

@ -1,10 +1,15 @@
# Claude — панель для iTerm2 Toolbelt
# Claude — панель сессий Claude Code
Сверху — дерево активных сессий Claude Code и их субагентов (модель, контекст, worktree),
снизу — лимиты подписки и баланс extra usage.
снизу — лимиты подписки и баланс extra usage. Панель — страница на `http://127.0.0.1:8766/`:
её показывает iTerm2 Toolbelt, её можно открыть в браузере, в том числе с iPad через SSH-туннель.
Сервер панели — фоновая служба macOS, iTerm2 для неё не нужен.
## Установка
Запускать за самим Mac, не по SSH (из SSH-сессии launchd и iTerm2 не настроить —
установщик откажется):
```bash
tar xzf claude-panel.tar.gz
cd claude-panel
@ -14,26 +19,40 @@ cd claude-panel
Затем в iTerm2: **View → Toolbelt → Show Toolbelt**, **Toolbelt → Claude**, и перезапустите
открытые сессии `claude`. Повторный запуск `install.sh` обновляет установленное.
Нужно: macOS, iTerm2, python3, jq (`brew install jq`, в macOS 15+ уже есть).
Нужно: macOS, python3, jq (`brew install jq`, в macOS 15+ уже есть); iTerm2 — для Toolbelt.
## Что ставится
| Что | Куда |
|---|---|
| скрипт панели | `~/Library/Application Support/iTerm2/Scripts/AutoLaunch/claude_agents.py` |
| служба (сервер панели) | `~/.local/share/claude-agents/claude_agents.py` |
| описание службы launchd | `~/Library/LaunchAgents/com.andrey.claude-agents.plist` |
| лог службы | `~/Library/Logs/claude-agents/panel.log` |
| скрипт Toolbelt | `~/Library/Application Support/iTerm2/Scripts/AutoLaunch/claude_agents.py` |
| хук Claude Code | `~/.claude/hooks/agents_panel_hook.sh` + записи в `~/.claude/settings.json` |
| пример настроек | `~/.config/claude-agents/config.example.json` |
Перед изменением `settings.json` и заменой файлов старые копии кладутся в
`~/.claude-panel-backup/<дата>/`. Ваши остальные хуки и настройки не трогаются.
Служба запускается при входе в систему и перезапускается при падении. Перед изменением
`settings.json` и заменой файлов старые копии кладутся в `~/.claude-panel-backup/<дата>/`.
Ваши остальные хуки и настройки не трогаются.
## С iPad (Termius)
1. В Termius: **Port Forwarding → Local**: хост — ваш Mac, local port `8766`,
bind address `127.0.0.1`, destination `127.0.0.1`, destination port `8766`.
2. Запустите forward и откройте в Safari `http://127.0.0.1:8766` — именно `127.0.0.1`,
не `localhost` (сервер слушает только IPv4).
Сервер доступен только с самого Mac, в сеть не открывается: с других устройств — только через туннель.
Клики работают через tmux, так что переходы и `lf` появляются в том же tmux, что у вас в Termius.
## Работа с панелью
- клик по сессии или агенту — последние сообщения (копятся, пока окно открыто);
- ⌘-клик по сессии — перейти в её вкладку;
- клик по `⎇ папка` — открыть в ней `lf` в соседней панели;
- ⌘-клик по сессии — перейти в её панель tmux;
- клик по `⎇ папка` — открыть в ней `lf` в соседней панели tmux;
- цвет точки: зелёный — работает, жёлтый — ждёт вас, серый — свободен;
- сессии внутри tmux (и `tmux -CC`) тоже видны: клик по `⎇` делит панель tmux, ⌘-клик переключает на неё;
- переходы и `lf` работают для сессий в tmux; сессии вне tmux только показываются;
- цвет токенов: жёлтый > 500k, красный > 700k.
## Настройки
@ -43,8 +62,10 @@ cd claude-panel
## Если что-то не так
`http://127.0.0.1:8766/raw` — что панель видит: логи хука, причины скрытия сессий,
ошибки лимитов и баланса.
- `http://127.0.0.1:8766/raw` — что панель видит: логи хука, причины скрытия сессий,
ошибки лимитов и баланса;
- `~/Library/Logs/claude-agents/panel.log` — лог службы;
- `launchctl print gui/$(id -u)/com.andrey.claude-agents` — состояние службы.
## Удаление

View File

@ -1,8 +1,18 @@
#!/bin/bash
# Установка панели «Claude» для iTerm2 Toolbelt: дерево сессий и агентов Claude Code,
# лимиты подписки и баланс. Повторный запуск безопасен — обновляет установленное.
# Установка панели «Claude»: дерево сессий и агентов Claude Code, лимиты подписки и баланс.
# Сервер панели — служба launchd (работает и без iTerm2), iTerm2 показывает её в Toolbelt.
# Повторный запуск безопасен — обновляет установленное.
set -euo pipefail
say() { printf '\033[1m==>\033[0m %s\n' "$*"; }
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
warn() { printf ' \033[33m!\033[0m %s\n' "$*"; }
die() { printf '\033[31mОшибка:\033[0m %s\n' "$*" >&2; exit 1; }
# из SSH-сессии launchd (gui/<uid>) и iTerm2 не настроить, а прежний сервер уже был бы остановлен
[ -z "${SSH_CONNECTION:-}${SSH_TTY:-}" ] \
|| die "запустите за самим Mac, не по SSH — из SSH-сессии launchd и iTerm2 не настроить"
HERE="$(cd "$(dirname "$0")" && pwd)"
FILES="$HERE/files"
AUTOLAUNCH="$HOME/Library/Application Support/iTerm2/Scripts/AutoLaunch"
@ -11,11 +21,12 @@ SETTINGS="$HOME/.claude/settings.json"
CONFIG_DIR="$HOME/.config/claude-agents"
BACKUP_DIR="$HOME/.claude-panel-backup/$(date +%Y%m%d-%H%M%S)-$$"
PORT=8766
LABEL=com.andrey.claude-agents
SERVICE_DIR="$HOME/.local/share/claude-agents"
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
SERVICE_LOG="$HOME/Library/Logs/claude-agents/panel.log"
GUI="gui/$(id -u)"
say() { printf '\033[1m==>\033[0m %s\n' "$*"; }
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
warn() { printf ' \033[33m!\033[0m %s\n' "$*"; }
die() { printf '\033[31mОшибка:\033[0m %s\n' "$*" >&2; exit 1; }
backup() { mkdir -p "$BACKUP_DIR"; cp -p "$1" "$BACKUP_DIR/"; }
# ---------- проверки ----------
@ -23,11 +34,12 @@ say "Проверяю окружение"
[ "$(uname)" = "Darwin" ] || [ "${CLAUDE_PANEL_ANY_OS:-}" = 1 ] || die "нужен macOS с iTerm2"
python3 -c 'import sys; assert sys.version_info >= (3, 8)' 2>/dev/null \
|| die "нужен python3 ≥ 3.8 (xcode-select --install или brew install python)"
ok "python3: $(python3 -V 2>&1)"
PY="$(python3 -c 'import sys; print(sys.executable)')"
ok "python3: $(python3 -V 2>&1) ($PY)"
if command -v jq >/dev/null; then ok "jq: $(command -v jq)"
else warn "нет jq — хук без него молча ничего не пишет. Установите: brew install jq"; fi
if [ -d "/Applications/iTerm.app" ] || [ -d "$HOME/Applications/iTerm.app" ]; then ok "iTerm2 найден"
else warn "iTerm2 не найден в /Applications — панель будет работать только в нём"; fi
else warn "iTerm2 не найден в /Applications — панель будет доступна только в браузере"; fi
# ---------- хук Claude Code ----------
say "Хук Claude Code"
@ -41,7 +53,7 @@ if [ "$added" = "—" ]; then ok "settings.json: хуки уже на месте
else ok "settings.json: добавлены хуки $added (копия старого — в $BACKUP_DIR)"; fi
# ---------- скрипт iTerm2 ----------
say "Скрипт iTerm2"
say "Скрипт iTerm2 (регистрация в Toolbelt)"
mkdir -p "$AUTOLAUNCH"
if [ -f "$AUTOLAUNCH/claude_usage.py" ]; then # прежняя отдельная панель лимитов
backup "$AUTOLAUNCH/claude_usage.py" && rm "$AUTOLAUNCH/claude_usage.py"
@ -57,6 +69,27 @@ install -m 644 "$FILES/config.example.json" "$CONFIG_DIR/config.example.json"
if [ -f "$CONFIG_DIR/config.json" ]; then ok "ваш config.json не тронут"
else ok "пример настроек: $CONFIG_DIR/config.example.json (скопируйте в config.json, чтобы менять)"; fi
# ---------- фоновая служба ----------
say "Фоновая служба"
mkdir -p "$SERVICE_DIR"
install -m 644 "$FILES/claude_agents.py" "$SERVICE_DIR/claude_agents.py"
python3 "$FILES/launchagent.py" write "$PLIST" "$PY" "$SERVICE_DIR/claude_agents.py" "$SERVICE_LOG" >/dev/null
ok "$PLIST"
launchctl bootout "$GUI/$LABEL" >/dev/null 2>&1 || true
# прежняя копия панели (сервер внутри iTerm2) держит порт — останавливаем, чтобы его заняла служба
if command -v lsof >/dev/null; then
pids=$(lsof -ti "tcp:$PORT" -sTCP:LISTEN 2>/dev/null || true)
if [ -n "$pids" ]; then kill $pids 2>/dev/null || true; sleep 1; ok "остановлена прежняя версия панели"; fi
fi
launchctl bootstrap "$GUI" "$PLIST" || die "launchd не принял службу — смотрите $SERVICE_LOG"
up=0
for _ in 1 2 3 4 5 6 7 8 9 10; do
if curl -s -o /dev/null --max-time 1 "http://127.0.0.1:$PORT/"; then up=1; break; fi
sleep 1
done
if [ $up = 1 ]; then ok "служба запущена: http://127.0.0.1:$PORT/"
else warn "служба не ответила — смотрите $SERVICE_LOG"; fi
# ---------- Python API в iTerm2 ----------
say "iTerm2"
iterm_running=0
@ -74,28 +107,23 @@ else
ok "Python API уже включён"
fi
# старая копия панели держит порт — останавливаем, чтобы запустилась новая
if command -v lsof >/dev/null; then
pids=$(lsof -ti "tcp:$PORT" -sTCP:LISTEN 2>/dev/null || true)
if [ -n "$pids" ]; then kill $pids 2>/dev/null || true; sleep 1; ok "остановлена прежняя версия панели"; fi
fi
if [ $iterm_running = 1 ] && [ "$api" = 1 ]; then
if osascript -e 'tell application "iTerm2" to launch API script named "claude_agents"' >/dev/null 2>&1; then
ok "панель запущена"
ok "панель зарегистрирована в Toolbelt"
else
warn "не смог запустить сам — Scripts → AutoLaunch → claude_agents.py"
warn "не смог зарегистрировать сам — Scripts → AutoLaunch → claude_agents.py"
fi
else
warn "панель запустится сама при следующем запуске iTerm2"
warn "в Toolbelt панель появится при следующем запуске iTerm2"
fi
cat <<EOF
Готово. Осталось:
1. В iTerm2: View → Toolbelt → Show Toolbelt, затем Toolbelt → Claude.
2. Перезапустите открытые сессии claude — хуки подхватываются при старте сессии.
Готово. Панель работает и без iTerm2: http://127.0.0.1:$PORT/
(с iPad — через SSH-туннель на этот адрес, см. README).
В iTerm2: View → Toolbelt → Show Toolbelt, затем Toolbelt → Claude.
Перезапустите открытые сессии claude — хуки подхватываются при старте сессии.
Проверка, что всё работает: http://127.0.0.1:$PORT/raw
Проверка: http://127.0.0.1:$PORT/raw · лог службы: $SERVICE_LOG
Удаление: $HERE/uninstall.sh
EOF

32
tests/test_install.py Normal file
View File

@ -0,0 +1,32 @@
import os
import subprocess
import tempfile
import unittest
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
SSH_ENVS = ({"SSH_CONNECTION": "10.0.0.2 50000 10.0.0.1 22"}, {"SSH_TTY": "/dev/ttys009"})
class RefuseOverSsh(unittest.TestCase):
"""Только охранная проверка: реальная установка в этих тестах не запускается."""
def assert_refused(self, *cmd):
for extra in SSH_ENVS:
with self.subTest(cmd=cmd, env=extra), tempfile.TemporaryDirectory() as home:
env = {k: v for k, v in os.environ.items() if k not in ("SSH_CONNECTION", "SSH_TTY")}
env.update(HOME=home, **extra)
r = subprocess.run(["bash", *cmd], cwd=ROOT, env=env, capture_output=True, text=True)
self.assertNotEqual(r.returncode, 0)
self.assertIn("за самим Mac", r.stdout + r.stderr)
self.assertEqual(os.listdir(home), [])
def test_install_refuses_over_ssh(self):
self.assert_refused("install.sh")
def test_uninstall_refuses_over_ssh(self):
self.assert_refused("uninstall.sh")
self.assert_refused("uninstall.sh", "--purge")
if __name__ == "__main__":
unittest.main()

View File

@ -1,14 +1,26 @@
#!/bin/bash
# Удаление панели «Claude». С --purge удаляет ещё настройки и логи хука.
# Удаление панели «Claude». С --purge удаляет ещё настройки и логи.
set -euo pipefail
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
die() { printf '\033[31mОшибка:\033[0m %s\n' "$*" >&2; exit 1; }
[ -z "${SSH_CONNECTION:-}${SSH_TTY:-}" ] \
|| die "запустите за самим Mac, не по SSH — из SSH-сессии launchd и iTerm2 не настроить"
HERE="$(cd "$(dirname "$0")" && pwd)"
AUTOLAUNCH="$HOME/Library/Application Support/iTerm2/Scripts/AutoLaunch"
SETTINGS="$HOME/.claude/settings.json"
BACKUP_DIR="$HOME/.claude-panel-backup/$(date +%Y%m%d-%H%M%S)-$$-uninstall"
PORT=8766
LABEL=com.andrey.claude-agents
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
# служба — первой, иначе launchd поднимет сервер снова
launchctl bootout "gui/$(id -u)/$LABEL" >/dev/null 2>&1 && ok "служба остановлена" || true
rm -f "$PLIST"
rm -rf "$HOME/.local/share/claude-agents"
ok "служба удалена"
if command -v lsof >/dev/null; then
pids=$(lsof -ti "tcp:$PORT" -sTCP:LISTEN 2>/dev/null || true)
@ -25,9 +37,9 @@ fi
rm -f "$HOME/.claude/hooks/agents_panel_hook.sh" && ok "хук удалён"
if [ "${1:-}" = "--purge" ]; then
rm -rf "$HOME/.config/claude-agents" "$HOME/.cache/claude-agents"
rm -rf "$HOME/.config/claude-agents" "$HOME/.cache/claude-agents" "$HOME/Library/Logs/claude-agents"
ok "настройки и логи удалены"
else
echo " Настройки (~/.config/claude-agents) и логи (~/.cache/claude-agents) оставлены; --purge удалит и их."
echo " Настройки (~/.config/claude-agents) и логи (~/.cache/claude-agents, ~/Library/Logs/claude-agents) оставлены; --purge удалит и их."
fi
echo "Панель «Claude» исчезнет из меню Toolbelt после перезапуска iTerm2."