Coverage for little_loops / cli / doctor.py: 16%

79 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-06-26 17:38 -0500

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

2 

3from __future__ import annotations 

4 

5import argparse 

6import json 

7import sys 

8from pathlib import Path 

9 

10from little_loops.cli.output import configure_output, use_color_enabled 

11from little_loops.logger import Logger 

12from little_loops.session_store import DEFAULT_DB_PATH, cli_event_context 

13 

14_STATUS_SYMBOLS: dict[str, str] = { 

15 "full": "✓", 

16 "partial": "○", 

17 "unsupported": "✗", 

18 "installed": "✓", 

19 "registered": "○", 

20 "deferred": "○", 

21 "absent": "✗", 

22} 

23 

24 

25def _print_capture_section(capture: object) -> None: 

26 """Print the Analytics Capture config-state section.""" 

27 print() 

28 print("Analytics Capture") 

29 print("─" * 40) 

30 full = _STATUS_SYMBOLS["full"] 

31 skills = getattr(capture, "skills", ["*"]) 

32 cli_commands = getattr(capture, "cli_commands", ["*"]) 

33 corrections = getattr(capture, "corrections", True) 

34 file_events = getattr(capture, "file_events", True) 

35 correction_patterns = getattr(capture, "correction_patterns", []) 

36 print(f" {full} skills: {skills}") 

37 print(f" {full} cli_commands: {cli_commands}") 

38 corr_sym = _STATUS_SYMBOLS["full" if corrections else "unsupported"] 

39 print(f" {corr_sym} corrections: {'enabled' if corrections else 'disabled'}") 

40 fe_sym = _STATUS_SYMBOLS["full" if file_events else "unsupported"] 

41 print(f" {fe_sym} file_events: {'enabled' if file_events else 'disabled'}") 

42 print( 

43 f" {full} correction_patterns: {correction_patterns if correction_patterns else '(none)'}" 

44 ) 

45 

46 

47def _print_issues_section(issues_cfg: object) -> None: 

48 """Print the Issues config-state section.""" 

49 print() 

50 print("Issues") 

51 print("─" * 40) 

52 auto_commit = getattr(issues_cfg, "auto_commit", False) 

53 auto_commit_prefix = getattr(issues_cfg, "auto_commit_prefix", "chore(issues)") 

54 ac_sym = _STATUS_SYMBOLS["full" if auto_commit else "unsupported"] 

55 print(f" {ac_sym} auto_commit: {'enabled' if auto_commit else 'disabled'}") 

56 print(f" {_STATUS_SYMBOLS['full']} auto_commit_prefix: {auto_commit_prefix}") 

57 

58 

59def _print_report(report: object, *, json_mode: bool = False) -> None: 

60 """Print a CapabilityReport in text or JSON format.""" 

61 from little_loops.host_runner import CapabilityReport 

62 

63 assert isinstance(report, CapabilityReport) 

64 

65 if json_mode: 

66 data = { 

67 "host": report.host, 

68 "binary": report.binary, 

69 "version": report.version or "(unknown)", 

70 "capabilities": [ 

71 {"name": c.name, "status": c.status, "note": c.note} for c in report.capabilities 

72 ], 

73 "hooks": [{"name": h.name, "status": h.status, "note": h.note} for h in report.hooks], 

74 } 

75 print(json.dumps(data, indent=2)) 

76 return 

77 

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

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

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

81 

82 if report.capabilities: 

83 print() 

84 print("Capabilities") 

85 print("─" * 40) 

86 for cap in report.capabilities: 

87 symbol = _STATUS_SYMBOLS.get(cap.status, "?") 

88 note = f" {cap.note}" if cap.note else "" 

89 print(f" {symbol} {cap.name}{note}") 

90 

91 if report.hooks: 

92 print() 

93 print("Hooks") 

94 print("─" * 40) 

95 for hook in report.hooks: 

96 symbol = _STATUS_SYMBOLS.get(hook.status, "?") 

97 note = f" {hook.note}" if hook.note else "" 

98 print(f" {symbol} {hook.name}{note}") 

99 

100 

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

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

103 

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

105 invocation modes and per-hook installation status. 

106 

107 Returns: 

108 Exit code (0 = all capabilities present, 1 = critical capability missing) 

109 """ 

110 with cli_event_context(DEFAULT_DB_PATH, "ll-doctor", sys.argv[1:]): 

111 from little_loops.config import BRConfig 

112 from little_loops.host_runner import apply_host_cli_from_config, resolve_host 

113 

114 parser = argparse.ArgumentParser( 

115 prog="ll-doctor", 

116 description="Check host CLI capability support for little-loops features", 

117 formatter_class=argparse.RawDescriptionHelpFormatter, 

118 epilog=""" 

119Examples: 

120 %(prog)s # Print capability table 

121 %(prog)s --json # Output as JSON 

122 

123Exit codes: 

124 0 - All capabilities present 

125 1 - One or more capabilities unsupported 

126""", 

127 ) 

128 parser.add_argument( 

129 "-j", 

130 "--json", 

131 action="store_true", 

132 help="Output as JSON", 

133 ) 

134 

135 args = parser.parse_args(argv) 

136 configure_output() 

137 Logger(use_color=use_color_enabled()) 

138 

139 cfg = BRConfig(Path.cwd()) 

140 apply_host_cli_from_config(cfg) 

141 runner = resolve_host() 

142 report = runner.describe_capabilities() 

143 

144 _print_report(report, json_mode=args.json) 

145 

146 if not args.json: 

147 _print_capture_section(cfg.analytics_capture) 

148 _print_issues_section(cfg.issues) 

149 

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