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

2

3 from __future__ import annotations

4

5 import argparse

6 import json

7 import sys

8 from pathlib import Path

9

10 from little_loops.cli.output import configure_output, use_color_enabled

11 from little_loops.logger import Logger

12 from 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

25 def _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

47 def _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

59 def _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

101 def 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="""

119 Examples:

120 %(prog)s # Print capability table

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

122

123 Exit 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