Coverage for / home / runner / work / bijux-cli / bijux-cli / src / bijux_cli / cli / commands / diagnostics / doctor.py: 100%
51 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-01-26 17:59 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-01-26 17:59 +0000
1# SPDX-License-Identifier: Apache-2.0
2# Copyright © 2025 Bijan Mousavi
4"""Implements the `doctor` command for the Bijux CLI.
6This module provides the functionality for the `bijux doctor` command, which runs
7a series of health diagnostics on the CLI's operating environment. It checks for
8common configuration issues and reports a summary of its findings in a
9structured, machine-readable format suitable for automation.
11Output Contract:
12 * Success: `{"status": str, "summary": list[str]}`
13 * Error: `{"error": str, "code": int}`
15Exit Codes:
16 * `0`: Success (command ran without errors, regardless of health status).
17 * `1`: Internal or fatal error (e.g., dependency injection failure).
18 * `2`: CLI argument or flag error.
19"""
21from __future__ import annotations
23import os
24import platform
26import typer
28from bijux_cli.cli.core.command import (
29 ascii_safe,
30 new_run_command,
31 raise_exit_intent,
32 validate_common_flags,
33)
34from bijux_cli.cli.core.constants import (
35 ENV_TEST_FORCE_UNHEALTHY,
36 OPT_FORMAT,
37 OPT_LOG_LEVEL,
38 OPT_PRETTY,
39 OPT_QUIET,
40)
41from bijux_cli.cli.core.help_text import (
42 HELP_FORMAT,
43 HELP_LOG_LEVEL,
44 HELP_NO_PRETTY,
45 HELP_QUIET,
46)
47from bijux_cli.core.di import DIContainer
48from bijux_cli.core.enums import ErrorType
49from bijux_cli.core.precedence import current_execution_policy
50from bijux_cli.core.runtime import AsyncTyper
51from bijux_cli.infra.contracts import Emitter
52from bijux_cli.services.contracts import TelemetryProtocol
54typer.core.rich = None # type: ignore[attr-defined]
56doctor_app = AsyncTyper(
57 name="doctor",
58 help="Run CLI health diagnostics and environment checks.",
59 rich_markup_mode=None,
60 context_settings={"help_option_names": ["-h", "--help"]},
61 no_args_is_help=False,
62)
65def _build_payload(include_runtime: bool) -> dict[str, object]:
66 """Builds the payload summarizing CLI environment health.
68 This function performs a series of checks on the environment and aggregates
69 the findings into a structured payload.
71 Args:
72 include_runtime (bool): If True, appends Python and platform version
73 information to the payload.
75 Returns:
76 Mapping[str, object]: A dictionary containing the health status, a
77 summary of findings, and optional runtime details.
78 """
79 healthy = True
80 summary: list[str] = []
82 if not os.environ.get("PATH", ""):
83 healthy = False
84 summary.append("Environment PATH is empty")
86 if os.environ.get(ENV_TEST_FORCE_UNHEALTHY) == "1":
87 healthy = False
88 summary.append("Forced unhealthy by test environment")
90 if not summary:
91 summary.append(
92 "All core checks passed" if healthy else "Unknown issue detected"
93 )
95 payload: dict[str, object] = {
96 "status": "healthy" if healthy else "unhealthy",
97 "summary": summary,
98 }
100 if include_runtime:
101 return {
102 "status": payload["status"],
103 "summary": payload["summary"],
104 "python": ascii_safe(platform.python_version(), "python_version"),
105 "platform": ascii_safe(platform.platform(), "platform"),
106 }
108 return payload
111@doctor_app.callback(invoke_without_command=True)
112def doctor(
113 ctx: typer.Context,
114 quiet: bool = typer.Option(False, *OPT_QUIET, help=HELP_QUIET),
115 fmt: str = typer.Option("json", *OPT_FORMAT, help=HELP_FORMAT),
116 pretty: bool = typer.Option(True, OPT_PRETTY, help=HELP_NO_PRETTY),
117 log_level: str = typer.Option("info", *OPT_LOG_LEVEL, help=HELP_LOG_LEVEL),
118) -> None:
119 """Defines the entrypoint and logic for the `bijux doctor` command.
121 This function orchestrates the health check process. It validates all CLI
122 flags, performs critical pre-flight checks (like dependency availability),
123 and then invokes the main run utility to build and emit the health payload.
125 Args:
126 ctx (typer.Context): The Typer context for managing command state.
127 quiet (bool): If True, suppresses all output; the exit code is the
128 primary indicator of the outcome.
129 output payload.
130 fmt (str): The output format, either "json" or "yaml". Defaults to "json".
131 pretty (bool): If True, pretty-prints the output for human readability. log_level (str): Logging level for diagnostics.
132 and `pretty`.
134 Returns:
135 None:
137 Raises:
138 SystemExit: Exits the application with a contract-compliant status code
139 and payload upon any error, such as invalid arguments or an
140 internal system failure.
141 """
142 if ctx.invoked_subcommand:
143 return
145 command = "doctor"
146 policy = current_execution_policy()
147 quiet = policy.quiet
148 include_runtime = policy.include_runtime
149 log_level_value = policy.log_level
150 pretty = policy.pretty
151 fmt_lower = validate_common_flags(
152 fmt,
153 command,
154 quiet,
155 include_runtime=include_runtime,
156 log_level=log_level_value,
157 )
158 if ctx.args:
159 stray = ctx.args[0]
160 msg = (
161 f"No such option: {stray}"
162 if stray.startswith("-")
163 else f"Too many arguments: {' '.join(ctx.args)}"
164 )
165 raise_exit_intent(
166 msg,
167 code=2,
168 failure="args",
169 error_type=ErrorType.USAGE,
170 command=command,
171 fmt=fmt_lower,
172 quiet=quiet,
173 include_runtime=include_runtime,
174 log_level=log_level_value,
175 )
177 try:
178 DIContainer.current().resolve(Emitter)
179 DIContainer.current().resolve(TelemetryProtocol)
180 except Exception as exc:
181 raise_exit_intent(
182 str(exc),
183 code=1,
184 failure="internal",
185 error_type=ErrorType.INTERNAL,
186 command=command,
187 fmt=fmt_lower,
188 quiet=quiet,
189 include_runtime=include_runtime,
190 log_level=log_level_value,
191 )
193 new_run_command(
194 command_name=command,
195 payload_builder=_build_payload,
196 quiet=quiet,
197 fmt=fmt_lower,
198 pretty=pretty,
199 log_level=log_level,
200 )