Coverage for / home / runner / work / bijux-cli / bijux-cli / src / bijux_cli / cli / plugins / commands / info.py: 90%
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 `plugins info` subcommand for the Bijux CLI.
6This module contains the logic for displaying detailed metadata about a single
7installed plugin. It locates the plugin by name, reads its `plugin.json`
8manifest file, and presents the contents in a structured, machine-readable
9format.
11Output Contract:
12 * Success: `{"name": str, "path": str, ... (plugin.json contents)}`
13 * Error: `{"error": "...", "code": int}`
15Exit Codes:
16 * `0`: Success.
17 * `1`: The plugin was not found, or its metadata file was corrupt.
18 * `2`: An invalid flag was provided (e.g., bad format).
19 * `3`: An ASCII or encoding error was detected in the environment.
20"""
22from __future__ import annotations
24from collections.abc import Mapping
25import json
26import platform
27from typing import Any
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 OPT_FORMAT,
38 OPT_LOG_LEVEL,
39 OPT_PRETTY,
40 OPT_QUIET,
41)
42from bijux_cli.cli.core.help_text import (
43 HELP_FORMAT,
44 HELP_LOG_LEVEL,
45 HELP_NO_PRETTY,
46 HELP_QUIET,
47)
48from bijux_cli.core.enums import ErrorType
49from bijux_cli.core.exit_policy import ExitIntentError
50from bijux_cli.core.precedence import current_execution_policy, resolve_exit_intent
51from bijux_cli.plugins.metadata import get_plugin_metadata
54def info_plugin(
55 name: str = typer.Argument(..., help="Plugin name"),
56 quiet: bool = typer.Option(False, *OPT_QUIET, help=HELP_QUIET),
57 fmt: str = typer.Option("json", *OPT_FORMAT, help=HELP_FORMAT),
58 pretty: bool = typer.Option(True, OPT_PRETTY, help=HELP_NO_PRETTY),
59 log_level: str = typer.Option("info", *OPT_LOG_LEVEL, help=HELP_LOG_LEVEL),
60) -> None:
61 """Shows detailed metadata for a specific installed plugin.
63 This function locates an installed plugin by its directory name, parses its
64 `plugin.json` manifest file, and emits the contents as a structured
65 payload.
67 Args:
68 name (str): The case-sensitive name of the plugin to inspect.
69 quiet (bool): If True, suppresses all output except for errors.
70 fmt (str): The output format, "json" or "yaml".
71 pretty (bool): If True, pretty-prints the output. log_level (str): Logging level for diagnostics.
73 Returns:
74 None:
76 Raises:
77 SystemExit: Always exits with a contract-compliant status code and
78 payload, indicating success or detailing an error.
79 """
80 command = "plugins info"
82 effective = current_execution_policy()
83 fmt_lower = validate_common_flags(
84 fmt,
85 command,
86 effective.quiet,
87 include_runtime=effective.include_runtime,
88 log_level=effective.log_level,
89 )
90 quiet = effective.quiet
91 pretty = effective.pretty
93 try:
94 meta = get_plugin_metadata(name)
95 except Exception as exc:
96 intent = resolve_exit_intent(
97 message=str(exc),
98 code=1,
99 failure="metadata_error",
100 command=command,
101 fmt=fmt_lower,
102 quiet=quiet,
103 include_runtime=effective.include_runtime,
104 error_type=ErrorType.INTERNAL,
105 log_level=effective.log_level,
106 )
107 raise ExitIntentError(intent) from exc
109 payload: dict[str, Any] = {
110 "name": meta.name,
111 "version": meta.version,
112 "enabled": meta.enabled,
113 "source": meta.source,
114 "requires_cli": meta.requires_cli,
115 }
116 if meta.dist_name:
117 payload["package"] = meta.dist_name
118 if meta.path:
119 payload["path"] = str(meta.path)
120 meta_file = meta.path / "plugin.json"
121 try:
122 extra = json.loads(meta_file.read_text("utf-8"))
123 if isinstance(extra, dict): 123 ↛ 139line 123 didn't jump to line 139 because the condition on line 123 was always true
124 payload.update(extra)
125 except Exception as exc:
126 intent = resolve_exit_intent(
127 message=f'Plugin "{name}" metadata is corrupt: {exc}',
128 code=1,
129 failure="metadata_corrupt",
130 command=command,
131 fmt=fmt_lower,
132 quiet=quiet,
133 include_runtime=effective.include_runtime,
134 error_type=ErrorType.INTERNAL,
135 log_level=effective.log_level,
136 )
137 raise ExitIntentError(intent) from exc
139 new_run_command(
140 command_name=command,
141 payload_builder=lambda include: _build_payload(include, payload),
142 quiet=quiet,
143 fmt=fmt_lower,
144 pretty=pretty,
145 log_level=log_level,
146 )
149def _build_payload(
150 include_runtime: bool, payload: dict[str, Any]
151) -> Mapping[str, object]:
152 """Builds the final payload with optional runtime metadata.
154 Args:
155 include_runtime (bool): If True, adds Python and platform info to the
156 payload.
157 payload (dict[str, Any]): The base payload containing the plugin metadata.
159 Returns:
160 Mapping[str, object]: The final payload, potentially with added runtime
161 details.
162 """
163 if include_runtime:
164 payload["python"] = ascii_safe(platform.python_version(), "python_version")
165 payload["platform"] = ascii_safe(platform.platform(), "platform")
166 return payload