feat: enhance service status reporting with JSON output and add related tests

This commit is contained in:
2026-05-20 15:29:21 -06:00
parent b21889c4ab
commit 8c58210c0c
4 changed files with 35 additions and 4 deletions

View File

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

View File

@@ -8,6 +8,7 @@ It reads `profiles/<profile>/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

View File

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

View File

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