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

40 statements  

« prev     ^ index     » next       coverage.py v7.10.4, created at 2025-08-19 23:36 +0000

1# SPDX-License-Identifier: MIT 

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 * Verbose: Adds `{"python": str, "platform": str, "timestamp": float}`. 

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

15 

16Exit Codes: 

17 * `0`: Success. 

18 * `1`: Internal or fatal error. 

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

20 * `3`: ASCII or encoding error. 

21""" 

22 

23from __future__ import annotations 

24 

25from collections.abc import Mapping 

26import os 

27import platform 

28import re 

29import time 

30 

31import typer 

32 

33from bijux_cli.__version__ import __version__ as cli_version 

34from bijux_cli.commands.utilities import ( 

35 ascii_safe, 

36 new_run_command, 

37 validate_common_flags, 

38) 

39from bijux_cli.contracts import EmitterProtocol, TelemetryProtocol 

40from bijux_cli.core.constants import ( 

41 HELP_DEBUG, 

42 HELP_FORMAT, 

43 HELP_NO_PRETTY, 

44 HELP_QUIET, 

45 HELP_VERBOSE, 

46) 

47from bijux_cli.core.di import DIContainer 

48 

49typer.core.rich = None # type: ignore[attr-defined,assignment] 

50 

51version_app = typer.Typer( # pytype: skip-file 

52 name="version", 

53 help="Show the CLI version.", 

54 rich_markup_mode=None, 

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

56 no_args_is_help=False, 

57) 

58 

59 

60def _build_payload(include_runtime: bool) -> Mapping[str, object]: 

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

62 

63 The version can be overridden by the `BIJUXCLI_VERSION` environment 

64 variable, which is validated for correctness. 

65 

66 Args: 

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

68 and a timestamp to the payload. 

69 

70 Returns: 

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

72 optional runtime metadata. 

73 

74 Raises: 

75 ValueError: If `BIJUXCLI_VERSION` is set but is empty, too long, 

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

77 """ 

78 version_env = os.environ.get("BIJUXCLI_VERSION") 

79 if version_env is not None: 

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

81 raise ValueError("BIJUXCLI_VERSION is empty or too long") 

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

83 raise ValueError("BIJUXCLI_VERSION contains non-ASCII") 

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

85 raise ValueError("BIJUXCLI_VERSION is not valid semantic version (x.y.z)") 

86 version_ = version_env 

87 else: 

88 version_ = cli_version 

89 

90 payload: dict[str, object] = { 

91 "version": ascii_safe(version_, "BIJUXCLI_VERSION"), 

92 } 

93 

94 if include_runtime: 

95 payload["python"] = ascii_safe(platform.python_version(), "python_version") 

96 payload["platform"] = ascii_safe(platform.platform(), "platform") 

97 payload["timestamp"] = time.time() 

98 

99 return payload 

100 

101 

102@version_app.callback(invoke_without_command=True) 

103def version( 

104 ctx: typer.Context, 

105 quiet: bool = typer.Option(False, "-q", "--quiet", help=HELP_QUIET), 

106 verbose: bool = typer.Option(False, "-v", "--verbose", help=HELP_VERBOSE), 

107 fmt: str = typer.Option("json", "-f", "--format", help=HELP_FORMAT), 

108 pretty: bool = typer.Option(True, "--pretty/--no-pretty", help=HELP_NO_PRETTY), 

109 debug: bool = typer.Option(False, "-d", "--debug", help=HELP_DEBUG), 

110) -> None: 

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

112 

113 This function orchestrates the version reporting process by validating 

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

115 emit the final payload. 

116 

117 Args: 

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

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

120 primary indicator of the outcome. 

121 verbose (bool): If True, includes Python, platform, and timestamp 

122 details in the output payload. 

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

124 pretty (bool): If True, pretty-prints the output for human readability. 

125 debug (bool): If True, enables debug diagnostics, implying `verbose` 

126 and `pretty`. 

127 

128 Returns: 

129 None: 

130 

131 Raises: 

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

133 payload upon completion or error. 

134 """ 

135 if ctx.invoked_subcommand: 

136 return 

137 

138 DIContainer.current().resolve(EmitterProtocol) 

139 DIContainer.current().resolve(TelemetryProtocol) 

140 command = "version" 

141 

142 fmt_lower = validate_common_flags(fmt, command, quiet) 

143 

144 new_run_command( 

145 command_name=command, 

146 payload_builder=lambda include: _build_payload(include), 

147 quiet=quiet, 

148 verbose=verbose, 

149 fmt=fmt_lower, 

150 pretty=pretty, 

151 debug=debug, 

152 )