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
|
```bash
|
||||||
python3 scripts/aiw/services.py status --profile fidelity
|
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 doctor --profile fidelity --json
|
||||||
python3 scripts/aiw/services.py start --profile fidelity
|
python3 scripts/aiw/services.py start --profile fidelity
|
||||||
python3 scripts/aiw/services.py stop --profile fidelity
|
python3 scripts/aiw/services.py stop --profile fidelity
|
||||||
python3 scripts/aiw/services.py restart aiw-context-mcp --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
|
## 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
|
## 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.
|
2. Add profile selector and service action buttons.
|
||||||
3. Add LaunchAgent support for start at login.
|
3. Add LaunchAgent support for start at login.
|
||||||
4. Replace shell parsing with a daemon API if daily use proves stable.
|
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
|
```bash
|
||||||
python3 scripts/aiw/services.py status --profile fidelity
|
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
|
||||||
python3 scripts/aiw/services.py doctor --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 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']})")
|
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:
|
def tail_log(profile: str, service: str, lines: int) -> None:
|
||||||
path = log_path(profile, service)
|
path = log_path(profile, service)
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
@@ -480,8 +490,11 @@ def main() -> None:
|
|||||||
for ref in refs:
|
for ref in refs:
|
||||||
start_service(args.profile, ref, manifest, started)
|
start_service(args.profile, ref, manifest, started)
|
||||||
elif args.action == "status":
|
elif args.action == "status":
|
||||||
for ref in refs:
|
if args.json:
|
||||||
status_service(args.profile, ref)
|
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":
|
elif args.action == "logs":
|
||||||
if not args.services:
|
if not args.services:
|
||||||
raise SystemExit("logs requires at least one service name")
|
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.assertEqual(report["services"][0]["name"], "alpha")
|
||||||
self.assertIn("health", report["services"][0])
|
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:
|
def test_read_pid_ignores_invalid_pid_file(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
pid_dir = Path(tmp) / "pids"
|
pid_dir = Path(tmp) / "pids"
|
||||||
|
|||||||
Reference in New Issue
Block a user