feat: панель как фоновая служба без iTerm2 + цвет фона на клиенте #5

Merged
aep merged 16 commits from feat/4-headless-panel into main 2026-10-18 21:58:14 +03:00
2 changed files with 88 additions and 0 deletions
Showing only changes of commit 88124cf6d8 - Show all commits

39
files/launchagent.py Normal file
View File

@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Пишет описание службы launchd для сервера панели: запуск при входе, перезапуск при падении.
launchagent.py write <plist> <python> <claude_agents.py> <log>
"""
import os
import plistlib
import sys
LABEL = "com.andrey.claude-agents" # то же, что SERVICE_LABEL в claude_agents.py
def plist(python, script, log):
return {
"Label": LABEL,
"ProgramArguments": [python, script, "--headless"],
"RunAtLoad": True,
"KeepAlive": True,
"StandardOutPath": log,
"StandardErrorPath": log,
}
def main():
if len(sys.argv) != 6 or sys.argv[1] != "write":
print(__doc__.strip(), file=sys.stderr)
sys.exit(2)
path, python, script, log = (os.path.expanduser(a) for a in sys.argv[2:])
os.makedirs(os.path.dirname(path), exist_ok=True)
os.makedirs(os.path.dirname(log), exist_ok=True)
tmp = path + ".tmp"
with open(tmp, "wb") as f:
plistlib.dump(plist(python, script, log), f, fmt=plistlib.FMT_XML)
os.replace(tmp, path)
print(path)
if __name__ == "__main__":
main()

49
tests/test_launchagent.py Normal file
View File

@ -0,0 +1,49 @@
import os
import plistlib
import subprocess
import sys
import tempfile
import unittest
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
sys.path.insert(0, os.path.join(ROOT, "files"))
import claude_agents # noqa: E402
import launchagent # noqa: E402
SCRIPT = os.path.join(ROOT, "files", "launchagent.py")
class Plist(unittest.TestCase):
def test_label_matches_server(self):
self.assertEqual(launchagent.LABEL, "com.andrey.claude-agents")
self.assertEqual(launchagent.LABEL, claude_agents.SERVICE_LABEL)
def test_plist_shape(self):
self.assertEqual(
launchagent.plist("/opt/homebrew/bin/python3", "/x/claude_agents.py", "/l/panel.log"),
{"Label": "com.andrey.claude-agents",
"ProgramArguments": ["/opt/homebrew/bin/python3", "/x/claude_agents.py", "--headless"],
"RunAtLoad": True, "KeepAlive": True,
"StandardOutPath": "/l/panel.log", "StandardErrorPath": "/l/panel.log"})
class Cli(unittest.TestCase):
def test_cli_writes_plist_with_spaces(self):
with tempfile.TemporaryDirectory() as tmp:
target = f"{tmp}/LaunchAgents/a.plist"
args = ("/usr/bin/python3", f"{tmp}/Application Support/claude_agents.py",
f"{tmp}/Logs/claude-agents/panel.log")
r = subprocess.run([sys.executable, SCRIPT, "write", target, *args],
capture_output=True, text=True)
self.assertEqual(r.returncode, 0, r.stderr)
with open(target, "rb") as f:
self.assertEqual(plistlib.load(f), launchagent.plist(*args))
self.assertTrue(os.path.isdir(f"{tmp}/Logs/claude-agents"))
def test_cli_bad_args(self):
r = subprocess.run([sys.executable, SCRIPT, "write"], capture_output=True, text=True)
self.assertEqual(r.returncode, 2)
if __name__ == "__main__":
unittest.main()