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
« 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 `version` command for the Bijux CLI.
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.
11Output Contract:
12 * Success: `{"version": str}`
13 * Error: `{"error": str, "code": int}`
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"""
22from __future__ import annotations
24import os
25import platform
26import re
27import time
29import typer
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
56typer.core.rich = None # type: ignore[attr-defined]
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)
67def _build_payload(include_runtime: bool) -> dict[str, object]:
68 """Builds the structured payload for the version command.
70 The version can be overridden by a dedicated environment variable,
71 which is validated for correctness.
73 Args:
74 include_runtime (bool): If True, appends Python/platform details
75 and a timestamp to the payload.
77 Returns:
78 Mapping[str, object]: A dictionary containing the CLI version and
79 optional runtime metadata.
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
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
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.
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.
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`.
132 Returns:
133 None:
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
142 DIContainer.current().resolve(Emitter)
143 DIContainer.current().resolve(TelemetryProtocol)
144 command = "version"
145 validate_common_flags(fmt, command, quiet)
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 )
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 )