Coverage for /home/runner/work/bijux-cli/bijux-cli/src/bijux_cli/commands/plugins/info.py: 100%
31 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 `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 * Verbose: Adds `{"python": str, "platform": str}` to the payload.
14 * Error: `{"error": "...", "code": int}`
16Exit Codes:
17 * `0`: Success.
18 * `1`: The plugin was not found, or its metadata file was corrupt.
19 * `2`: An invalid flag was provided (e.g., bad format).
20 * `3`: An ASCII or encoding error was detected in the environment.
21"""
23from __future__ import annotations
25from collections.abc import Mapping
26import json
27import platform
28from typing import Any
30import typer
32from bijux_cli.commands.utilities import (
33 ascii_safe,
34 emit_error_and_exit,
35 new_run_command,
36 validate_common_flags,
37)
38from bijux_cli.core.constants import (
39 HELP_DEBUG,
40 HELP_FORMAT,
41 HELP_NO_PRETTY,
42 HELP_QUIET,
43 HELP_VERBOSE,
44)
45from bijux_cli.services.plugins import get_plugins_dir
48def info_plugin(
49 name: str = typer.Argument(..., help="Plugin name"),
50 quiet: bool = typer.Option(False, "-q", "--quiet", help=HELP_QUIET),
51 verbose: bool = typer.Option(False, "-v", "--verbose", help=HELP_VERBOSE),
52 fmt: str = typer.Option("json", "-f", "--format", help=HELP_FORMAT),
53 pretty: bool = typer.Option(True, "--pretty/--no-pretty", help=HELP_NO_PRETTY),
54 debug: bool = typer.Option(False, "-d", "--debug", help=HELP_DEBUG),
55) -> None:
56 """Shows detailed metadata for a specific installed plugin.
58 This function locates an installed plugin by its directory name, parses its
59 `plugin.json` manifest file, and emits the contents as a structured
60 payload.
62 Args:
63 name (str): The case-sensitive name of the plugin to inspect.
64 quiet (bool): If True, suppresses all output except for errors.
65 verbose (bool): If True, includes Python/platform details in the output.
66 fmt (str): The output format, "json" or "yaml".
67 pretty (bool): If True, pretty-prints the output.
68 debug (bool): If True, enables debug diagnostics.
70 Returns:
71 None:
73 Raises:
74 SystemExit: Always exits with a contract-compliant status code and
75 payload, indicating success or detailing an error.
76 """
77 command = "plugins info"
79 fmt_lower = validate_common_flags(fmt, command, quiet)
81 plug_dir = get_plugins_dir() / name
82 if not (plug_dir.is_dir() and (plug_dir / "plugin.py").is_file()):
83 emit_error_and_exit(
84 f'Plugin "{name}" not found',
85 code=1,
86 failure="not_found",
87 command=command,
88 fmt=fmt_lower,
89 quiet=quiet,
90 include_runtime=verbose,
91 debug=debug,
92 )
94 meta_file = plug_dir / "plugin.json"
95 meta: dict[str, Any] = {}
96 if meta_file.is_file():
97 try:
98 meta = json.loads(meta_file.read_text("utf-8"))
99 if not meta.get("name"):
100 raise ValueError("Missing required fields")
101 except Exception as exc:
102 emit_error_and_exit(
103 f'Plugin "{name}" metadata is corrupt: {exc}',
104 code=1,
105 failure="metadata_corrupt",
106 command=command,
107 fmt=fmt_lower,
108 quiet=quiet,
109 include_runtime=verbose,
110 debug=debug,
111 )
113 payload = {"name": name, "path": str(plug_dir), **meta}
115 new_run_command(
116 command_name=command,
117 payload_builder=lambda include: _build_payload(include, payload),
118 quiet=quiet,
119 verbose=verbose,
120 fmt=fmt_lower,
121 pretty=pretty,
122 debug=debug,
123 )
126def _build_payload(
127 include_runtime: bool, payload: dict[str, Any]
128) -> Mapping[str, object]:
129 """Builds the final payload with optional runtime metadata.
131 Args:
132 include_runtime (bool): If True, adds Python and platform info to the
133 payload.
134 payload (dict[str, Any]): The base payload containing the plugin metadata.
136 Returns:
137 Mapping[str, object]: The final payload, potentially with added runtime
138 details.
139 """
140 if include_runtime:
141 payload["python"] = ascii_safe(platform.python_version(), "python_version")
142 payload["platform"] = ascii_safe(platform.platform(), "platform")
143 return payload