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

1# SPDX-License-Identifier: Apache-2.0 

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 * Error: `{"error": "...", "code": int}` 

14 

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

21 

22from __future__ import annotations 

23 

24from collections.abc import Mapping 

25import json 

26import platform 

27from typing import Any 

28 

29import typer 

30 

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 

52 

53 

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. 

62 

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. 

66 

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. 

72 

73 Returns: 

74 None: 

75 

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" 

81 

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 

92 

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 

108 

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 

138 

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 ) 

147 

148 

149def _build_payload( 

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

151) -> Mapping[str, object]: 

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

153 

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. 

158 

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