Coverage for  / home / runner / work / bijux-cli / bijux-cli / src / bijux_cli / cli / commands / version.py: 100%

43 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 `version` command for the Bijux CLI. 

5 

6This module reports the CLI's version and runtime environment information. 

7The output is machine-readable, available in JSON or YAML, and is designed 

8to be safe for automation and scripting by adhering to a strict output 

9contract and ASCII hygiene. 

10 

11Output Contract: 

12 * Success: `{"version": str}` 

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

14 

15Exit Codes: 

16 * `0`: Success. 

17 * `1`: Internal or fatal error. 

18 * `2`: CLI argument, flag, or format error. 

19 * `3`: ASCII or encoding error. 

20""" 

21 

22from __future__ import annotations 

23 

24import os 

25import platform 

26import re 

27import time 

28 

29import typer 

30 

31from bijux_cli.cli.core.command import ( 

32 ascii_safe, 

33 new_run_command, 

34 validate_common_flags, 

35) 

36from bijux_cli.cli.core.constants import ( 

37 ENV_VERSION, 

38 OPT_FORMAT, 

39 OPT_LOG_LEVEL, 

40 OPT_PRETTY, 

41 OPT_QUIET, 

42) 

43from bijux_cli.cli.core.help_text import ( 

44 HELP_FORMAT, 

45 HELP_LOG_LEVEL, 

46 HELP_NO_PRETTY, 

47 HELP_QUIET, 

48) 

49from bijux_cli.core.di import DIContainer 

50from bijux_cli.core.precedence import current_execution_policy 

51from bijux_cli.core.runtime import AsyncTyper 

52from bijux_cli.core.version import __version__ as cli_version 

53from bijux_cli.infra.contracts import Emitter 

54from bijux_cli.services.contracts import TelemetryProtocol 

55 

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

57 

58version_app = AsyncTyper( 

59 name="version", 

60 help="Show the CLI version.", 

61 rich_markup_mode=None, 

62 context_settings={"help_option_names": ["-h", "--help"]}, 

63 no_args_is_help=False, 

64) 

65 

66 

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

68 """Builds the structured payload for the version command. 

69 

70 The version can be overridden by a dedicated environment variable, 

71 which is validated for correctness. 

72 

73 Args: 

74 include_runtime (bool): If True, appends Python/platform details 

75 and a timestamp to the payload. 

76 

77 Returns: 

78 Mapping[str, object]: A dictionary containing the CLI version and 

79 optional runtime metadata. 

80 

81 Raises: 

82 ValueError: If the override env var is set but is empty, too long, 

83 contains non-ASCII characters, or is not a valid semantic version. 

84 """ 

85 version_env = os.environ.get(ENV_VERSION) 

86 if version_env is not None: 

87 if not (1 <= len(version_env) <= 1024): 

88 raise ValueError(f"{ENV_VERSION} is empty or too long") 

89 if not all(ord(c) < 128 for c in version_env): 

90 raise ValueError(f"{ENV_VERSION} contains non-ASCII") 

91 if not re.fullmatch(r"\d+\.\d+\.\d+", version_env): 

92 raise ValueError(f"{ENV_VERSION} is not valid semantic version (x.y.z)") 

93 version_ = version_env 

94 else: 

95 version_ = cli_version 

96 

97 payload: dict[str, object] = {"version": ascii_safe(version_, ENV_VERSION)} 

98 if include_runtime: 

99 payload.update( 

100 { 

101 "python": ascii_safe(platform.python_version(), "python_version"), 

102 "platform": ascii_safe(platform.platform(), "platform"), 

103 "timestamp": time.time(), 

104 } 

105 ) 

106 return payload 

107 

108 

109@version_app.callback(invoke_without_command=True) 

110def version( 

111 ctx: typer.Context, 

112 quiet: bool = typer.Option(False, *OPT_QUIET, help=HELP_QUIET), 

113 fmt: str = typer.Option("json", *OPT_FORMAT, help=HELP_FORMAT), 

114 pretty: bool = typer.Option(True, OPT_PRETTY, help=HELP_NO_PRETTY), 

115 log_level: str = typer.Option("info", *OPT_LOG_LEVEL, help=HELP_LOG_LEVEL), 

116) -> None: 

117 """Defines the entrypoint and logic for the `bijux version` command. 

118 

119 This function orchestrates the version reporting process by validating 

120 flags and then using the shared `new_run_command` helper to build and 

121 emit the final payload. 

122 

123 Args: 

124 ctx (typer.Context): The Typer context for the CLI. 

125 quiet (bool): If True, suppresses all output; the exit code is the 

126 primary indicator of the outcome. 

127 details in the output payload. 

128 fmt (str): The output format, either "json" or "yaml". Defaults to "json". 

129 pretty (bool): If True, pretty-prints the output for human readability. log_level (str): Logging level for diagnostics. 

130 and `pretty`. 

131 

132 Returns: 

133 None: 

134 

135 Raises: 

136 SystemExit: Always exits with a contract-compliant status code and 

137 payload upon completion or error. 

138 """ 

139 if ctx.invoked_subcommand: 

140 return 

141 

142 DIContainer.current().resolve(Emitter) 

143 DIContainer.current().resolve(TelemetryProtocol) 

144 command = "version" 

145 validate_common_flags(fmt, command, quiet) 

146 

147 effective = current_execution_policy() 

148 fmt_lower = validate_common_flags( 

149 fmt, 

150 command, 

151 effective.quiet, 

152 include_runtime=effective.include_runtime, 

153 log_level=effective.log_level, 

154 ) 

155 

156 new_run_command( 

157 command_name=command, 

158 payload_builder=lambda include: _build_payload(include), 

159 quiet=effective.quiet, 

160 fmt=fmt_lower, 

161 pretty=effective.pretty, 

162 log_level=effective.log_level, 

163 )