Coverage for little_loops / cli / doctor.py: 16%
79 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-29 00:55 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-29 00:55 -0500
1"""ll-doctor: Host capability preflight check."""
3from __future__ import annotations
5import argparse
6import json
7import sys
8from pathlib import Path
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
14_STATUS_SYMBOLS: dict[str, str] = {
15 "full": "✓",
16 "partial": "○",
17 "unsupported": "✗",
18 "installed": "✓",
19 "registered": "○",
20 "deferred": "○",
21 "absent": "✗",
22}
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 )
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}")
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
63 assert isinstance(report, CapabilityReport)
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
78 version_display = report.version or "(unknown)"
79 print(f"Host: {report.host}")
80 print(f"Binary: {report.binary} {version_display}")
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}")
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}")
101def main_doctor(argv: list[str] | None = None) -> int:
102 """Entry point for ll-doctor command.
104 Resolve the active host and print a ✓/✗/○ capability table covering
105 invocation modes and per-hook installation status.
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
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
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 )
135 args = parser.parse_args(argv)
136 configure_output()
137 Logger(use_color=use_color_enabled())
139 cfg = BRConfig(Path.cwd())
140 apply_host_cli_from_config(cfg)
141 runner = resolve_host()
142 report = runner.describe_capabilities()
144 _print_report(report, json_mode=args.json)
146 if not args.json:
147 _print_capture_section(cfg.analytics_capture)
148 _print_issues_section(cfg.issues)
150 return 0 if not any(c.status == "unsupported" for c in report.capabilities) else 1