Compare commits
2 Commits
6626bb4668
...
3fc719b31b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fc719b31b | ||
|
|
c810f5a880 |
@ -36,7 +36,7 @@ Mac: видеть, что делают все запущенные `claude` (в
|
|||||||
сообщения), `/activate` и `/open-wt` (через tmux), `/raw` (отладка).
|
сообщения), `/activate` и `/open-wt` (через tmux), `/raw` (отладка).
|
||||||
- без флага (`register_toolbelt`) — скрипт AutoLaunch iTerm2: регистрирует web view «Claude» на
|
- без флага (`register_toolbelt`) — скрипт AutoLaunch iTerm2: регистрирует web view «Claude» на
|
||||||
адрес службы; если порт молчит — `launchctl kickstart`. Свой сервер не поднимает.
|
адрес службы; если порт молчит — `launchctl kickstart`. Свой сервер не поднимает.
|
||||||
- `files/launchagent.py` — пишет plist службы (`write <plist> <python> <script> <log>`).
|
- `files/launchagent.py` — пишет plist службы (`write <plist> <python> <script> <log> <PATH>`; PATH берётся из оболочки `install.sh`, его наследуют панели tmux, которые открывает служба).
|
||||||
- `files/settings_hooks.py` — добавляет/убирает хуки панели в `settings.json` (`add` / `remove`).
|
- `files/settings_hooks.py` — добавляет/убирает хуки панели в `settings.json` (`add` / `remove`).
|
||||||
- `files/config.example.json` — все ключи настроек; рабочий файл `~/.config/claude-agents/config.json`
|
- `files/config.example.json` — все ключи настроек; рабочий файл `~/.config/claude-agents/config.json`
|
||||||
перечитывается на лету, кроме `port`.
|
перечитывается на лету, кроме `port`.
|
||||||
|
|||||||
@ -69,7 +69,10 @@ cd claude-panel
|
|||||||
- `~/Library/Logs/claude-agents/panel.log` — лог службы;
|
- `~/Library/Logs/claude-agents/panel.log` — лог службы;
|
||||||
- `launchctl print gui/$(id -u)/com.andrey.claude-agents` — состояние службы; ошибки запуска (например, «не найден python») видны только здесь, а не в `panel.log`;
|
- `launchctl print gui/$(id -u)/com.andrey.claude-agents` — состояние службы; ошибки запуска (например, «не найден python») видны только здесь, а не в `panel.log`;
|
||||||
- после переустановки или обновления Python (в том числе `brew upgrade`/`brew cleanup`) запустите
|
- после переустановки или обновления Python (в том числе `brew upgrade`/`brew cleanup`) запустите
|
||||||
`./install.sh` заново — служба хранит путь к `python3`, и без этого launchd не сможет её запустить.
|
`./install.sh` заново — служба хранит путь к `python3`, и без этого launchd не сможет её запустить;
|
||||||
|
- «⎇ папка» мигает разделителем и панель сразу закрывается — служба не нашла `lf`. Служба берёт
|
||||||
|
`PATH` из оболочки, в которой запускали `install.sh`: запустите его заново из терминала, где
|
||||||
|
`command -v lf` находит файловый менеджер.
|
||||||
|
|
||||||
## Удаление
|
## Удаление
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Пишет описание службы launchd для сервера панели: запуск при входе, перезапуск при падении.
|
"""Пишет описание службы launchd для сервера панели: запуск при входе, перезапуск при падении.
|
||||||
|
|
||||||
launchagent.py write <plist> <python> <claude_agents.py> <log>
|
launchagent.py write <plist> <python> <claude_agents.py> <log> <PATH>
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import plistlib
|
import plistlib
|
||||||
@ -10,10 +10,12 @@ import sys
|
|||||||
LABEL = "com.andrey.claude-agents" # то же, что SERVICE_LABEL в claude_agents.py
|
LABEL = "com.andrey.claude-agents" # то же, что SERVICE_LABEL в claude_agents.py
|
||||||
|
|
||||||
|
|
||||||
def plist(python, script, log):
|
def plist(python, script, log, path):
|
||||||
return {
|
return {
|
||||||
"Label": LABEL,
|
"Label": LABEL,
|
||||||
"ProgramArguments": [python, script, "--headless"],
|
"ProgramArguments": [python, script, "--headless"],
|
||||||
|
# у launchd PATH без Homebrew, а панели tmux, открытые службой (lf), получают её PATH
|
||||||
|
"EnvironmentVariables": {"PATH": path},
|
||||||
"RunAtLoad": True,
|
"RunAtLoad": True,
|
||||||
"KeepAlive": True,
|
"KeepAlive": True,
|
||||||
"StandardOutPath": log,
|
"StandardOutPath": log,
|
||||||
@ -22,15 +24,16 @@ def plist(python, script, log):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if len(sys.argv) != 6 or sys.argv[1] != "write":
|
if len(sys.argv) != 7 or sys.argv[1] != "write":
|
||||||
print(__doc__.strip(), file=sys.stderr)
|
print(__doc__.strip(), file=sys.stderr)
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
path, python, script, log = (os.path.expanduser(a) for a in sys.argv[2:])
|
path, python, script, log = (os.path.expanduser(a) for a in sys.argv[2:6])
|
||||||
|
env_path = sys.argv[6]
|
||||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
os.makedirs(os.path.dirname(log), exist_ok=True)
|
os.makedirs(os.path.dirname(log), exist_ok=True)
|
||||||
tmp = path + ".tmp"
|
tmp = path + ".tmp"
|
||||||
with open(tmp, "wb") as f:
|
with open(tmp, "wb") as f:
|
||||||
plistlib.dump(plist(python, script, log), f, fmt=plistlib.FMT_XML)
|
plistlib.dump(plist(python, script, log, env_path), f, fmt=plistlib.FMT_XML)
|
||||||
os.replace(tmp, path)
|
os.replace(tmp, path)
|
||||||
print(path)
|
print(path)
|
||||||
|
|
||||||
|
|||||||
@ -73,7 +73,8 @@ else ok "пример настроек: $CONFIG_DIR/config.example.json (ско
|
|||||||
say "Фоновая служба"
|
say "Фоновая служба"
|
||||||
mkdir -p "$SERVICE_DIR"
|
mkdir -p "$SERVICE_DIR"
|
||||||
install -m 644 "$FILES/claude_agents.py" "$SERVICE_DIR/claude_agents.py"
|
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
|
# PATH из текущей оболочки: панели tmux, открытые службой (lf), должны находить то же, что и в терминале
|
||||||
|
python3 "$FILES/launchagent.py" write "$PLIST" "$PY" "$SERVICE_DIR/claude_agents.py" "$SERVICE_LOG" "$PATH" >/dev/null
|
||||||
ok "$PLIST"
|
ok "$PLIST"
|
||||||
launchctl bootout "$GUI/$LABEL" >/dev/null 2>&1 || true
|
launchctl bootout "$GUI/$LABEL" >/dev/null 2>&1 || true
|
||||||
# прежняя копия панели (сервер внутри iTerm2) держит порт — останавливаем, чтобы его заняла служба
|
# прежняя копия панели (сервер внутри iTerm2) держит порт — останавливаем, чтобы его заняла служба
|
||||||
|
|||||||
@ -178,3 +178,35 @@ tmux; фон выставлен под тему Termius и держится ме
|
|||||||
- 2026-10-18 (финальное ревью) — R-6 (design #17): тесты к исправлениям написаны без отдельного шага
|
- 2026-10-18 (финальное ревью) — R-6 (design #17): тесты к исправлениям написаны без отдельного шага
|
||||||
RED; вместо этого новые тесты прогнаны на коде до исправлений (2e8ab31) — все 6 новых тестов
|
RED; вместо этого новые тесты прогнаны на коде до исправлений (2e8ab31) — все 6 новых тестов
|
||||||
(I-1, I-3 ×2, I-4, M-2, M-12) там падают. Цена ошибки: нет.
|
(I-1, I-3 ×2, I-4, M-2, M-12) там падают. Цена ошибки: нет.
|
||||||
|
- 2026-10-18 (приёмка) — R-7 (design #18): служба получает `PATH` той оболочки, из которой запущен
|
||||||
|
`install.sh` (`EnvironmentVariables.PATH` в plist). Обоснование: tmux отдаёт новой панели `PATH`
|
||||||
|
вызвавшего `split-window`, а у launchd `PATH=/usr/bin:/bin:/usr/sbin:/sbin` — `exec lf` не находился,
|
||||||
|
панель «⎇ папка» мигала и закрывалась (H-1). Так панели, открытые службой, видят то же, что и
|
||||||
|
терминал (редактор, превьюеры lf). Цена ошибки: если `install.sh` запущен из урезанного окружения,
|
||||||
|
`lf` снова не найдётся — README подсказывает перезапустить установщик из обычного терминала.
|
||||||
|
|
||||||
|
## Результаты приёмки (Task 6, 2026-10-18)
|
||||||
|
Проверял человек с iPad (Termius, SSH + tmux) и контроллер из сессии Claude Code на том же Mac.
|
||||||
|
- Step 1 — ок: служба `com.andrey.claude-agents` запущена launchd (родитель PID 1, python из
|
||||||
|
`/opt/homebrew/bin/python3`), один слушатель `127.0.0.1:8766`; лимиты и баланс загрузились,
|
||||||
|
запроса доступа к Keychain не было. Чужой `Host` → 403.
|
||||||
|
- Step 2 — ок: iTerm2 закрыт (⌘Q через `osascript`), с iPad через туннель Termius видны дерево,
|
||||||
|
лимиты и баланс; tmux-сервер отвязан от iTerm2 (PPID 1) и пережил закрытие.
|
||||||
|
- Step 3 — «↗ вкладка» ок; «⎇ папка» сначала мигала разделителем и закрывалась (H-1, причина —
|
||||||
|
`PATH` службы), после c810f5a и переустановки — ок.
|
||||||
|
- Step 4 — ок: после `kill` служба снова отвечает 200 примерно через 2 с.
|
||||||
|
- Step 5 — частично: после `open -a iTerm` поднялся один процесс регистрации, порт держит служба;
|
||||||
|
вид Toolbelt глазами не проверен (человек не за Mac).
|
||||||
|
- Step 6 — ок: светлый образец, автоперезагрузка не закрывает строку образцов (I-4), «сброс»,
|
||||||
|
«свой цвет» открывает палитру iPad. Размер областей нажатия (M-11) жалоб не вызвал.
|
||||||
|
- Step 7 — не проверен: сохраняется ли фон в Toolbelt после перезапуска iTerm2, видно только на
|
||||||
|
экране Mac.
|
||||||
|
- Step 8 — ок, три повторных запуска: одна служба, `bootstrap` без «Bootstrap failed» (M-5 не
|
||||||
|
проявился), один процесс регистрации (M-1), 9 хуков в `settings.json` без дублей.
|
||||||
|
- Step 9 — ок с `SSH_CONNECTION=test`: отказ «за самим Mac» до каких-либо действий. Наблюдение:
|
||||||
|
в окне tmux, открытом из Termius, `SSH_CONNECTION` пуст, и охрана не срабатывает (подтверждает
|
||||||
|
M-3); установка из такого окна проходит нормально, т. к. tmux-сервер живёт в GUI-сессии.
|
||||||
|
- Step 10 — не выполнялся (по желанию).
|
||||||
|
- Наблюдение O-3: во время приёмки `./install.sh` один раз запустили из основного checkout (main,
|
||||||
|
старая версия без охраны) — он перезаписал скрипт AutoLaunch старым сервером; launchd сразу
|
||||||
|
поднял службу. Исправлено повторным `install.sh` из ветки; после merge неактуально.
|
||||||
|
|||||||
@ -20,9 +20,11 @@ class Plist(unittest.TestCase):
|
|||||||
|
|
||||||
def test_plist_shape(self):
|
def test_plist_shape(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
launchagent.plist("/opt/homebrew/bin/python3", "/x/claude_agents.py", "/l/panel.log"),
|
launchagent.plist("/opt/homebrew/bin/python3", "/x/claude_agents.py", "/l/panel.log",
|
||||||
|
"/opt/homebrew/bin:/usr/bin:/bin"),
|
||||||
{"Label": "com.andrey.claude-agents",
|
{"Label": "com.andrey.claude-agents",
|
||||||
"ProgramArguments": ["/opt/homebrew/bin/python3", "/x/claude_agents.py", "--headless"],
|
"ProgramArguments": ["/opt/homebrew/bin/python3", "/x/claude_agents.py", "--headless"],
|
||||||
|
"EnvironmentVariables": {"PATH": "/opt/homebrew/bin:/usr/bin:/bin"},
|
||||||
"RunAtLoad": True, "KeepAlive": True,
|
"RunAtLoad": True, "KeepAlive": True,
|
||||||
"StandardOutPath": "/l/panel.log", "StandardErrorPath": "/l/panel.log"})
|
"StandardOutPath": "/l/panel.log", "StandardErrorPath": "/l/panel.log"})
|
||||||
|
|
||||||
@ -32,7 +34,7 @@ class Cli(unittest.TestCase):
|
|||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
target = f"{tmp}/LaunchAgents/a.plist"
|
target = f"{tmp}/LaunchAgents/a.plist"
|
||||||
args = ("/usr/bin/python3", f"{tmp}/Application Support/claude_agents.py",
|
args = ("/usr/bin/python3", f"{tmp}/Application Support/claude_agents.py",
|
||||||
f"{tmp}/Logs/claude-agents/panel.log")
|
f"{tmp}/Logs/claude-agents/panel.log", "/opt/homebrew/bin:/usr/bin:/bin")
|
||||||
r = subprocess.run([sys.executable, SCRIPT, "write", target, *args],
|
r = subprocess.run([sys.executable, SCRIPT, "write", target, *args],
|
||||||
capture_output=True, text=True)
|
capture_output=True, text=True)
|
||||||
self.assertEqual(r.returncode, 0, r.stderr)
|
self.assertEqual(r.returncode, 0, r.stderr)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user