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

1# SPDX-License-Identifier: Apache-2.0 

2# Copyright © 2025 Bijan Mousavi 

3 

4"""Implements the `doctor` command for the Bijux CLI. 

5 

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. 

10 

11Output Contract: 

12 * Success: `{"status": str, "summary": list[str]}` 

13 * Error: `{"error": str, "code": int}` 

14 

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

20 

21from __future__ import annotations 

22 

23import os 

24import platform 

25 

26import typer 

27 

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 

53 

54typer.core.rich = None # type: ignore[attr-defined] 

55 

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) 

63 

64 

65def _build_payload(include_runtime: bool) -> dict[str, object]: 

66 """Builds the payload summarizing CLI environment health. 

67 

68 This function performs a series of checks on the environment and aggregates 

69 the findings into a structured payload. 

70 

71 Args: 

72 include_runtime (bool): If True, appends Python and platform version 

73 information to the payload. 

74 

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] = [] 

81 

82 if not os.environ.get("PATH", ""): 

83 healthy = False 

84 summary.append("Environment PATH is empty") 

85 

86 if os.environ.get(ENV_TEST_FORCE_UNHEALTHY) == "1": 

87 healthy = False 

88 summary.append("Forced unhealthy by test environment") 

89 

90 if not summary: 

91 summary.append( 

92 "All core checks passed" if healthy else "Unknown issue detected" 

93 ) 

94 

95 payload: dict[str, object] = { 

96 "status": "healthy" if healthy else "unhealthy", 

97 "summary": summary, 

98 } 

99 

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 } 

107 

108 return payload 

109 

110 

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. 

120 

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. 

124 

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`. 

133 

134 Returns: 

135 None: 

136 

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 

144 

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 ) 

176 

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 ) 

192 

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 )