Coverage for little_loops / cli / doctor.py: 100%
46 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-05-22 16:19 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-05-22 16:19 -0500
1"""ll-doctor: Host capability preflight check."""
3from __future__ import annotations
5import argparse
6import json
7from pathlib import Path
9from little_loops.cli.output import configure_output, use_color_enabled
10from little_loops.logger import Logger
12_STATUS_SYMBOLS: dict[str, str] = {
13 "full": "✓",
14 "partial": "○",
15 "unsupported": "✗",
16 "installed": "✓",
17 "registered": "○",
18 "deferred": "○",
19 "absent": "✗",
20}
23def _print_report(report: object, *, json_mode: bool = False) -> None:
24 """Print a CapabilityReport in text or JSON format."""
25 from little_loops.host_runner import CapabilityReport
27 assert isinstance(report, CapabilityReport)
29 if json_mode:
30 data = {
31 "host": report.host,
32 "binary": report.binary,
33 "version": report.version or "(unknown)",
34 "capabilities": [
35 {"name": c.name, "status": c.status, "note": c.note} for c in report.capabilities
36 ],
37 "hooks": [{"name": h.name, "status": h.status, "note": h.note} for h in report.hooks],
38 }
39 print(json.dumps(data, indent=2))
40 return
42 version_display = report.version or "(unknown)"
43 print(f"Host: {report.host}")
44 print(f"Binary: {report.binary} {version_display}")
46 if report.capabilities:
47 print()
48 print("Capabilities")
49 print("─" * 40)
50 for cap in report.capabilities:
51 symbol = _STATUS_SYMBOLS.get(cap.status, "?")
52 note = f" {cap.note}" if cap.note else ""
53 print(f" {symbol} {cap.name}{note}")
55 if report.hooks:
56 print()
57 print("Hooks")
58 print("─" * 40)
59 for hook in report.hooks:
60 symbol = _STATUS_SYMBOLS.get(hook.status, "?")
61 note = f" {hook.note}" if hook.note else ""
62 print(f" {symbol} {hook.name}{note}")
65def main_doctor(argv: list[str] | None = None) -> int:
66 """Entry point for ll-doctor command.
68 Resolve the active host and print a ✓/✗/○ capability table covering
69 invocation modes and per-hook installation status.
71 Returns:
72 Exit code (0 = all capabilities present, 1 = critical capability missing)
73 """
74 from little_loops.config import BRConfig
75 from little_loops.host_runner import apply_host_cli_from_config, resolve_host
77 parser = argparse.ArgumentParser(
78 prog="ll-doctor",
79 description="Check host CLI capability support for little-loops features",
80 formatter_class=argparse.RawDescriptionHelpFormatter,
81 epilog="""
82Examples:
83 %(prog)s # Print capability table
84 %(prog)s --json # Output as JSON
86Exit codes:
87 0 - All capabilities present
88 1 - One or more capabilities unsupported
89""",
90 )
91 parser.add_argument(
92 "-j",
93 "--json",
94 action="store_true",
95 help="Output as JSON",
96 )
98 args = parser.parse_args(argv)
99 configure_output()
100 Logger(use_color=use_color_enabled())
102 apply_host_cli_from_config(BRConfig(Path.cwd()))
103 runner = resolve_host()
104 report = runner.describe_capabilities()
106 _print_report(report, json_mode=args.json)
108 return 0 if not any(c.status == "unsupported" for c in report.capabilities) else 1