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>
This commit is contained in:
Andrey Epifancev 2026-10-18 22:44:58 +04:00
parent 6626bb4668
commit c810f5a880
5 changed files with 19 additions and 10 deletions

View File

@ -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`.

View File

@ -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` находит файловый менеджер.
## Удаление ## Удаление

View File

@ -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)

View File

@ -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) держит порт — останавливаем, чтобы его заняла служба

View File

@ -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)