diff --git a/core/services/menu-bar-app-design.md b/core/services/menu-bar-app-design.md index 0409df1..91f729a 100644 --- a/core/services/menu-bar-app-design.md +++ b/core/services/menu-bar-app-design.md @@ -51,13 +51,14 @@ The first version can shell out to: ```bash python3 scripts/aiw/services.py status --profile fidelity +python3 scripts/aiw/services.py status --profile fidelity --json python3 scripts/aiw/services.py doctor --profile fidelity --json python3 scripts/aiw/services.py start --profile fidelity python3 scripts/aiw/services.py stop --profile fidelity python3 scripts/aiw/services.py restart aiw-context-mcp --profile fidelity ``` -Longer term, prefer a small local daemon HTTP/Unix-socket API so the UI does not parse terminal text except `doctor --json`. +Use `status --json` for frequent UI refreshes and `doctor --json` for explicit diagnostics. Longer term, prefer a small local daemon HTTP/Unix-socket API so the UI does not parse terminal text. ## Production-Ready Rules @@ -70,7 +71,7 @@ Longer term, prefer a small local daemon HTTP/Unix-socket API so the UI does not ## Implementation Phases -1. CLI-backed SwiftUI menu bar app using `doctor --json` for status. +1. CLI-backed SwiftUI menu bar app using `status --json` for live status and `doctor --json` for diagnostics. 2. Add profile selector and service action buttons. 3. Add LaunchAgent support for start at login. 4. Replace shell parsing with a daemon API if daily use proves stable. diff --git a/scripts/aiw/README.md b/scripts/aiw/README.md index 4c0e679..b4cd790 100644 --- a/scripts/aiw/README.md +++ b/scripts/aiw/README.md @@ -8,6 +8,7 @@ It reads `profiles//services.json`, starts/stops enabled services, reco ```bash python3 scripts/aiw/services.py status --profile fidelity +python3 scripts/aiw/services.py status --profile fidelity --json python3 scripts/aiw/services.py doctor --profile fidelity python3 scripts/aiw/services.py doctor --profile fidelity --json python3 scripts/aiw/services.py start --profile fidelity diff --git a/scripts/aiw/services.py b/scripts/aiw/services.py index 5dad729..33c38eb 100644 --- a/scripts/aiw/services.py +++ b/scripts/aiw/services.py @@ -363,6 +363,16 @@ def status_service(profile: str, ref: ServiceRef) -> None: print(f"{ref.name}: {status['status']} pid={status['pid'] or '-'} ({status['health']['detail']})") +def status_report(profile: str, refs: list[ServiceRef]) -> dict[str, Any]: + """Return lightweight machine-readable live service state.""" + return { + "profile": profile, + "workspace": str(ROOT), + "runtime": str(RUNTIME_DIR), + "services": [service_status(profile, ref) for ref in refs], + } + + def tail_log(profile: str, service: str, lines: int) -> None: path = log_path(profile, service) if not path.is_file(): @@ -480,8 +490,11 @@ def main() -> None: for ref in refs: start_service(args.profile, ref, manifest, started) elif args.action == "status": - for ref in refs: - status_service(args.profile, ref) + if args.json: + print(json.dumps(status_report(args.profile, refs), ensure_ascii=False, indent=2, sort_keys=True)) + else: + for ref in refs: + status_service(args.profile, ref) elif args.action == "logs": if not args.services: raise SystemExit("logs requires at least one service name") diff --git a/scripts/aiw/test_services.py b/scripts/aiw/test_services.py index 26a2c00..db07dfa 100644 --- a/scripts/aiw/test_services.py +++ b/scripts/aiw/test_services.py @@ -133,6 +133,22 @@ class ServiceManagerTests(unittest.TestCase): self.assertEqual(report["services"][0]["name"], "alpha") self.assertIn("health", report["services"][0]) + def test_status_report_is_lightweight_machine_readable(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + refs = services.service_items(sample_manifest(), include_disabled=True) + with patch.object(services, "ROOT", root), \ + patch.object(services, "RUNTIME_DIR", root / ".aiw" / "runtime"), \ + patch.object(services, "PID_DIR", root / ".aiw" / "runtime" / "pids"), \ + patch.object(services, "LOG_DIR", root / ".aiw" / "runtime" / "logs"), \ + patch.object(services, "STATE_DIR", root / ".aiw" / "runtime" / "state"): + report = services.status_report("test", refs) + + self.assertEqual(report["profile"], "test") + self.assertEqual(len(report["services"]), 3) + self.assertIn("status", report["services"][0]) + self.assertNotIn("checks", report["services"][0]) + def test_read_pid_ignores_invalid_pid_file(self) -> None: with tempfile.TemporaryDirectory() as tmp: pid_dir = Path(tmp) / "pids"