Andrey Epifancev c810f5a880 fix: pass shell PATH to panel service so tmux panes find lf (#4, H-1)
The launchd service runs with PATH=/usr/bin:/bin:/usr/sbin:/sbin; tmux
split-window gives the new pane the caller's PATH, so 'exec lf' failed
and the pane closed at once. install.sh now records its own PATH in the
plist EnvironmentVariables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:44:58 +04:00

137 lines
7.6 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.

#!/bin/bash
# Установка панели «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 не настроить. Если вы и так за Mac (например, в панели tmux, открытой раньше из SSH), запустите в обычной вкладке iTerm2 или после unset SSH_CONNECTION SSH_TTY"
HERE="$(cd "$(dirname "$0")" && pwd)"
FILES="$HERE/files"
AUTOLAUNCH="$HOME/Library/Application Support/iTerm2/Scripts/AutoLaunch"
HOOKS_DIR="$HOME/.claude/hooks"
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)"
backup() { mkdir -p "$BACKUP_DIR"; cp -p "$1" "$BACKUP_DIR/"; }
# ---------- проверки ----------
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)"
PY="$(command -v python3)" # не sys.executable: версионный путь Homebrew исчезает после brew cleanup
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
# ---------- хук Claude Code ----------
say "Хук Claude Code"
mkdir -p "$HOOKS_DIR"
install -m 755 "$FILES/agents_panel_hook.sh" "$HOOKS_DIR/agents_panel_hook.sh"
ok "$HOOKS_DIR/agents_panel_hook.sh"
[ -f "$SETTINGS" ] && backup "$SETTINGS"
added=$(python3 "$FILES/settings_hooks.py" add "$SETTINGS") \
|| die "не смог обновить $SETTINGS (битый JSON?) — копия в $BACKUP_DIR"
if [ "$added" = "—" ]; then ok "settings.json: хуки уже на месте"
else ok "settings.json: добавлены хуки $added (копия старого — в $BACKUP_DIR)"; fi
# ---------- скрипт iTerm2 ----------
say "Скрипт iTerm2 (регистрация в Toolbelt)"
mkdir -p "$AUTOLAUNCH"
if [ -f "$AUTOLAUNCH/claude_usage.py" ]; then # прежняя отдельная панель лимитов
backup "$AUTOLAUNCH/claude_usage.py" && rm "$AUTOLAUNCH/claude_usage.py"
ok "старый claude_usage.py убран из AutoLaunch (копия в $BACKUP_DIR)"
fi
[ -f "$AUTOLAUNCH/claude_agents.py" ] && backup "$AUTOLAUNCH/claude_agents.py"
install -m 644 "$FILES/claude_agents.py" "$AUTOLAUNCH/claude_agents.py"
ok "$AUTOLAUNCH/claude_agents.py"
# ---------- настройки панели ----------
mkdir -p "$CONFIG_DIR"
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"
# PATH из текущей оболочки: панели tmux, открытые службой (lf), должны находить то же, что и в терминале
python3 "$FILES/launchagent.py" write "$PLIST" "$PY" "$SERVICE_DIR/claude_agents.py" "$SERVICE_LOG" "$PATH" >/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
# сразу после bootout bootstrap иногда падает с «Input/output error» — повторяем
for attempt in 1 2 3 4 5; do
launchctl bootstrap "$GUI" "$PLIST" && break
[ "$attempt" = 5 ] && die "launchd не принял службу — смотрите launchctl print $GUI/$LABEL и $SERVICE_LOG"
sleep 1
done
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
pgrep -xq iTerm2 2>/dev/null && iterm_running=1
api=$(defaults read com.googlecode.iterm2 EnableAPIServer 2>/dev/null || echo 0)
if [ "$api" != 1 ]; then
if [ $iterm_running = 0 ] && command -v defaults >/dev/null; then
defaults write com.googlecode.iterm2 EnableAPIServer -bool true
ok "Python API включён"
else
warn "включите Python API: iTerm2 → Settings → General → Magic → Enable Python API"
[ $iterm_running = 1 ] && warn "(пока iTerm2 запущен, установщик не меняет его настройки — он бы их перезаписал)"
fi
else
ok "Python API уже включён"
fi
if [ $iterm_running = 1 ] && [ "$api" = 1 ]; then
pkill -f "Scripts/AutoLaunch/claude_agents.py" || true # прежний регистратор, иначе их станет по одному на запуск
if osascript -e 'tell application "iTerm2" to launch API script named "claude_agents"' >/dev/null 2>&1; then
ok "панель зарегистрирована в Toolbelt"
else
warn "не смог зарегистрировать сам — Scripts → AutoLaunch → claude_agents.py"
fi
else
warn "в Toolbelt панель появится при следующем запуске iTerm2"
fi
cat <<EOF
Готово. Панель работает и без 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 · лог службы: $SERVICE_LOG
Удаление: $HERE/uninstall.sh
EOF