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

1# SPDX-License-Identifier: MIT 

2# Copyright © 2025 Bijan Mousavi 

3 

4"""Implements the `plugins info` subcommand for the Bijux CLI. 

5 

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. 

10 

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}` 

15 

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""" 

22 

23from __future__ import annotations 

24 

25from collections.abc import Mapping 

26import json 

27import platform 

28from typing import Any 

29 

30import typer 

31 

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 

46 

47 

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. 

57 

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. 

61 

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. 

69 

70 Returns: 

71 None: 

72 

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" 

78 

79 fmt_lower = validate_common_flags(fmt, command, quiet) 

80 

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 ) 

93 

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 ) 

112 

113 payload = {"name": name, "path": str(plug_dir), **meta} 

114 

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 ) 

124 

125 

126def _build_payload( 

127 include_runtime: bool, payload: dict[str, Any] 

128) -> Mapping[str, object]: 

129 """Builds the final payload with optional runtime metadata. 

130 

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. 

135 

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