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
« 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
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 * Verbose: Adds `{"python": str, "platform": str, "timestamp": float}`.
14 * Error: `{"error": str, "code": int}`
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"""
23from __future__ import annotations
25from collections.abc import Mapping
26import os
27import platform
28import re
29import time
31import typer
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
49typer.core.rich = None # type: ignore[attr-defined,assignment]
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)
60def _build_payload(include_runtime: bool) -> Mapping[str, object]:
61 """Builds the structured payload for the version command.
63 The version can be overridden by the `BIJUXCLI_VERSION` environment
64 variable, which is validated for correctness.
66 Args:
67 include_runtime (bool): If True, appends Python/platform details
68 and a timestamp to the payload.
70 Returns:
71 Mapping[str, object]: A dictionary containing the CLI version and
72 optional runtime metadata.
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
90 payload: dict[str, object] = {
91 "version": ascii_safe(version_, "BIJUXCLI_VERSION"),
92 }
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()
99 return payload
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.
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.
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`.
128 Returns:
129 None:
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
138 DIContainer.current().resolve(EmitterProtocol)
139 DIContainer.current().resolve(TelemetryProtocol)
140 command = "version"
142 fmt_lower = validate_common_flags(fmt, command, quiet)
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 )