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

1"""ll-doctor: Host capability preflight check.""" 

2 

3from __future__ import annotations 

4 

5import argparse 

6import json 

7from pathlib import Path 

8 

9from little_loops.cli.output import configure_output, use_color_enabled 

10from little_loops.logger import Logger 

11 

12_STATUS_SYMBOLS: dict[str, str] = { 

13 "full": "✓", 

14 "partial": "○", 

15 "unsupported": "✗", 

16 "installed": "✓", 

17 "registered": "○", 

18 "deferred": "○", 

19 "absent": "✗", 

20} 

21 

22 

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 

26 

27 assert isinstance(report, CapabilityReport) 

28 

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 

41 

42 version_display = report.version or "(unknown)" 

43 print(f"Host: {report.host}") 

44 print(f"Binary: {report.binary} {version_display}") 

45 

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

54 

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

63 

64 

65def main_doctor(argv: list[str] | None = None) -> int: 

66 """Entry point for ll-doctor command. 

67 

68 Resolve the active host and print a ✓/✗/○ capability table covering 

69 invocation modes and per-hook installation status. 

70 

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 

76 

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 

85 

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 ) 

97 

98 args = parser.parse_args(argv) 

99 configure_output() 

100 Logger(use_color=use_color_enabled()) 

101 

102 apply_host_cli_from_config(BRConfig(Path.cwd())) 

103 runner = resolve_host() 

104 report = runner.describe_capabilities() 

105 

106 _print_report(report, json_mode=args.json) 

107 

108 return 0 if not any(c.status == "unsupported" for c in report.capabilities) else 1