feat: enhance service status reporting with JSON output and add related tests
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user