talkops-opscloud 0.3.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. opscloud/__init__.py +5 -0
  2. opscloud/__main__.py +6 -0
  3. opscloud/_constants.py +53 -0
  4. opscloud/_debug.py +145 -0
  5. opscloud/_debug_buffer.py +250 -0
  6. opscloud/_textual_patches.py +367 -0
  7. opscloud/_version.py +15 -0
  8. opscloud/agent/__init__.py +27 -0
  9. opscloud/agent/config.py +168 -0
  10. opscloud/agent/factory.py +1103 -0
  11. opscloud/approval_mode.py +44 -0
  12. opscloud/backend/__init__.py +49 -0
  13. opscloud/backend/composite.py +89 -0
  14. opscloud/backend/local.py +183 -0
  15. opscloud/backend/registry.py +79 -0
  16. opscloud/backend/sandbox_config.py +107 -0
  17. opscloud/backend/sandbox_factory.py +432 -0
  18. opscloud/built_in_skills/remember/SKILL.md +124 -0
  19. opscloud/built_in_skills/skill-creator/SKILL.md +348 -0
  20. opscloud/built_in_skills/skill-creator/scripts/init_skill.py +371 -0
  21. opscloud/built_in_skills/skill-creator/scripts/quick_validate.py +165 -0
  22. opscloud/cli/__init__.py +1 -0
  23. opscloud/cli/commands/__init__.py +15 -0
  24. opscloud/cli/commands/auth.py +45 -0
  25. opscloud/cli/commands/doctor.py +100 -0
  26. opscloud/cli/commands/mcp.py +137 -0
  27. opscloud/cli/commands/threads.py +51 -0
  28. opscloud/cli/interactive.py +154 -0
  29. opscloud/cli/main.py +903 -0
  30. opscloud/cli/non_interactive.py +425 -0
  31. opscloud/cli/server_manager.py +58 -0
  32. opscloud/commands/__init__.py +19 -0
  33. opscloud/commands/_base.py +110 -0
  34. opscloud/commands/_guards.py +52 -0
  35. opscloud/commands/_router.py +146 -0
  36. opscloud/commands/_types.py +36 -0
  37. opscloud/commands/core/__init__.py +47 -0
  38. opscloud/commands/core/auth.py +94 -0
  39. opscloud/commands/core/bug.py +46 -0
  40. opscloud/commands/core/clear.py +106 -0
  41. opscloud/commands/core/cloud.py +185 -0
  42. opscloud/commands/core/compact.py +128 -0
  43. opscloud/commands/core/config_cmd.py +120 -0
  44. opscloud/commands/core/context.py +220 -0
  45. opscloud/commands/core/cost.py +44 -0
  46. opscloud/commands/core/doctor.py +535 -0
  47. opscloud/commands/core/effort.py +108 -0
  48. opscloud/commands/core/exit_cmd.py +41 -0
  49. opscloud/commands/core/fast.py +148 -0
  50. opscloud/commands/core/help_cmd.py +94 -0
  51. opscloud/commands/core/mcp.py +134 -0
  52. opscloud/commands/core/model.py +109 -0
  53. opscloud/commands/core/permissions.py +196 -0
  54. opscloud/commands/core/plugins.py +92 -0
  55. opscloud/commands/core/pool.py +108 -0
  56. opscloud/commands/core/resume.py +97 -0
  57. opscloud/commands/core/skills.py +178 -0
  58. opscloud/commands/power/__init__.py +51 -0
  59. opscloud/commands/power/agents.py +98 -0
  60. opscloud/commands/power/btw.py +85 -0
  61. opscloud/commands/power/copy.py +97 -0
  62. opscloud/commands/power/goal.py +548 -0
  63. opscloud/commands/power/loop.py +295 -0
  64. opscloud/commands/power/memory.py +287 -0
  65. opscloud/commands/power/review.py +152 -0
  66. opscloud/commands/power/rubric.py +287 -0
  67. opscloud/commands/power/runtime.py +321 -0
  68. opscloud/commands/power/skill_creator.py +70 -0
  69. opscloud/commands/power/skill_invoke.py +165 -0
  70. opscloud/commands/power/tasks.py +209 -0
  71. opscloud/commands/power/trace.py +117 -0
  72. opscloud/commands/power/ui_toggles.py +135 -0
  73. opscloud/commands/power/version.py +52 -0
  74. opscloud/commands/registry.py +176 -0
  75. opscloud/config/__init__.py +114 -0
  76. opscloud/config/adapters/__init__.py +5 -0
  77. opscloud/config/adapters/postgres.py +776 -0
  78. opscloud/config/adapters/sqlite.py +769 -0
  79. opscloud/config/aws.py +248 -0
  80. opscloud/config/cloud_profiles.py +437 -0
  81. opscloud/config/env_vars.py +81 -0
  82. opscloud/config/langsmith.py +319 -0
  83. opscloud/config/manifest.py +817 -0
  84. opscloud/config/metadata.py +205 -0
  85. opscloud/config/paths.py +604 -0
  86. opscloud/config/plugins.py +130 -0
  87. opscloud/config/settings.py +834 -0
  88. opscloud/config/store.py +453 -0
  89. opscloud/config/store_factory.py +97 -0
  90. opscloud/config/toml_config.py +562 -0
  91. opscloud/editor.py +173 -0
  92. opscloud/exceptions.py +65 -0
  93. opscloud/file_ops.py +217 -0
  94. opscloud/hooks/__init__.py +139 -0
  95. opscloud/hooks/env.py +44 -0
  96. opscloud/hooks/interrupt.py +121 -0
  97. opscloud/hooks/models/__init__.py +109 -0
  98. opscloud/hooks/models/adapters.py +19 -0
  99. opscloud/hooks/models/config.py +79 -0
  100. opscloud/hooks/models/domain.py +434 -0
  101. opscloud/hooks/models/transport.py +37 -0
  102. opscloud/hooks/tools.py +231 -0
  103. opscloud/input.py +48 -0
  104. opscloud/integrations/__init__.py +80 -0
  105. opscloud/integrations/base.py +153 -0
  106. opscloud/integrations/content_filter.py +119 -0
  107. opscloud/integrations/event_bus.py +480 -0
  108. opscloud/integrations/hooks.py +238 -0
  109. opscloud/integrations/identity.py +181 -0
  110. opscloud/integrations/notifications.py +134 -0
  111. opscloud/integrations/stream_bridge.py +397 -0
  112. opscloud/integrations/thread_store.py +81 -0
  113. opscloud/json_types.py +20 -0
  114. opscloud/mcp/__init__.py +57 -0
  115. opscloud/mcp/config.py +203 -0
  116. opscloud/mcp/discovery.py +229 -0
  117. opscloud/mcp/mcp_info.py +109 -0
  118. opscloud/mcp/middleware.py +102 -0
  119. opscloud/mcp/preload.py +505 -0
  120. opscloud/mcp/raw_config.py +149 -0
  121. opscloud/mcp/semantic_profiler.py +305 -0
  122. opscloud/mcp/session_manager.py +727 -0
  123. opscloud/mcp/trust.py +105 -0
  124. opscloud/memory/__init__.py +27 -0
  125. opscloud/memory/branch.py +61 -0
  126. opscloud/memory/guard.py +19 -0
  127. opscloud/memory/onboarding.py +120 -0
  128. opscloud/memory/registry.py +134 -0
  129. opscloud/memory/store.py +223 -0
  130. opscloud/middleware/__init__.py +106 -0
  131. opscloud/middleware/_repository_bounds.py +459 -0
  132. opscloud/middleware/ask_user.py +152 -0
  133. opscloud/middleware/auto_mode.py +60 -0
  134. opscloud/middleware/auto_mode_hitl.py +2434 -0
  135. opscloud/middleware/compaction.py +566 -0
  136. opscloud/middleware/configurable_model.py +233 -0
  137. opscloud/middleware/cost_tracking.py +1021 -0
  138. opscloud/middleware/glm_stall_recovery.py +109 -0
  139. opscloud/middleware/goal_criteria.py +1234 -0
  140. opscloud/middleware/goal_state_notice.py +712 -0
  141. opscloud/middleware/goal_tools.py +407 -0
  142. opscloud/middleware/headless_mcp_guard.py +88 -0
  143. opscloud/middleware/jev_model_router.py +1018 -0
  144. opscloud/middleware/local_context.py +460 -0
  145. opscloud/middleware/mcp_context.py +151 -0
  146. opscloud/middleware/mcp_middleware.py +146 -0
  147. opscloud/middleware/memory_guard.py +393 -0
  148. opscloud/middleware/model_retry.py +205 -0
  149. opscloud/middleware/registry.py +137 -0
  150. opscloud/middleware/reliable_rubric.py +709 -0
  151. opscloud/middleware/resume_state.py +91 -0
  152. opscloud/middleware/server_hooks.py +1189 -0
  153. opscloud/middleware/shell_allow_list.py +90 -0
  154. opscloud/middleware/skills.py +382 -0
  155. opscloud/middleware/subagent_artifacts.py +84 -0
  156. opscloud/middleware/subagent_telemetry.py +261 -0
  157. opscloud/middleware/subagents.py +243 -0
  158. opscloud/middleware/tool_filter.py +384 -0
  159. opscloud/middleware/unified_system_message.py +62 -0
  160. opscloud/model/__init__.py +37 -0
  161. opscloud/model/config.py +2992 -0
  162. opscloud/model/factory.py +477 -0
  163. opscloud/model/pool.py +459 -0
  164. opscloud/model/reasoning.py +282 -0
  165. opscloud/offload.py +173 -0
  166. opscloud/output.py +64 -0
  167. opscloud/plugins/__init__.py +189 -0
  168. opscloud/plugins/_json.py +22 -0
  169. opscloud/plugins/adapters/__init__.py +13 -0
  170. opscloud/plugins/adapters/agents.py +112 -0
  171. opscloud/plugins/adapters/commands.py +190 -0
  172. opscloud/plugins/adapters/mcp.py +363 -0
  173. opscloud/plugins/adapters/skills.py +132 -0
  174. opscloud/plugins/commands_cli.py +541 -0
  175. opscloud/plugins/discovery.py +512 -0
  176. opscloud/plugins/manifest.py +335 -0
  177. opscloud/plugins/marketplace.py +893 -0
  178. opscloud/plugins/models.py +334 -0
  179. opscloud/plugins/project_plugins.py +317 -0
  180. opscloud/plugins/protocol.py +31 -0
  181. opscloud/plugins/store.py +905 -0
  182. opscloud/plugins/substitution.py +122 -0
  183. opscloud/project_utils.py +145 -0
  184. opscloud/prompts/__init__.py +37 -0
  185. opscloud/prompts/system.py +483 -0
  186. opscloud/prompts/templates/system_prompt.md +166 -0
  187. opscloud/prompts/types.py +54 -0
  188. opscloud/rubrics/__init__.py +42 -0
  189. opscloud/rubrics/evaluator.py +404 -0
  190. opscloud/rubrics/evidence_extractor.py +229 -0
  191. opscloud/rubrics/generator.py +239 -0
  192. opscloud/rubrics/jev_compiler.py +175 -0
  193. opscloud/rubrics/jev_grader.py +297 -0
  194. opscloud/rubrics/middleware.py +8 -0
  195. opscloud/schema/__init__.py +107 -0
  196. opscloud/schema/interrupts.py +981 -0
  197. opscloud/security/__init__.py +91 -0
  198. opscloud/security/approval_mode.py +528 -0
  199. opscloud/security/approval_mode_source.py +230 -0
  200. opscloud/security/cli_ast_evaluator.py +552 -0
  201. opscloud/security/jev_classifier.py +651 -0
  202. opscloud/security/shell_safety.py +340 -0
  203. opscloud/security/unicode_security.py +439 -0
  204. opscloud/security/url_validation.py +160 -0
  205. opscloud/server/__init__.py +13 -0
  206. opscloud/server/_server_config.py +158 -0
  207. opscloud/server/server.py +242 -0
  208. opscloud/server/server_graph.py +93 -0
  209. opscloud/skills/__init__.py +91 -0
  210. opscloud/skills/commands.py +1099 -0
  211. opscloud/skills/invocation.py +200 -0
  212. opscloud/skills/load.py +388 -0
  213. opscloud/skills/loader.py +23 -0
  214. opscloud/skills/merge.py +65 -0
  215. opscloud/skills/registry.py +263 -0
  216. opscloud/skills/sources.py +174 -0
  217. opscloud/skills/trust.py +417 -0
  218. opscloud/state/__init__.py +142 -0
  219. opscloud/state/base.py +154 -0
  220. opscloud/state/cloud_context.py +262 -0
  221. opscloud/state/goal_channels.py +124 -0
  222. opscloud/state/goal_state_limits.py +175 -0
  223. opscloud/state/resume_state.py +58 -0
  224. opscloud/state/service_context.py +281 -0
  225. opscloud/state/session.py +957 -0
  226. opscloud/state/state_migration.py +86 -0
  227. opscloud/subagents/__init__.py +30 -0
  228. opscloud/subagents/loader.py +331 -0
  229. opscloud/subagents/subagents_parser.py +117 -0
  230. opscloud/subagents/types.py +25 -0
  231. opscloud/tools/__init__.py +36 -0
  232. opscloud/tools/catalog.py +37 -0
  233. opscloud/tools/display.py +24 -0
  234. opscloud/tools/fetch_url.py +176 -0
  235. opscloud/tools/goal_tools.py +423 -0
  236. opscloud/tools/registry.py +105 -0
  237. opscloud/tools/thread.py +32 -0
  238. opscloud/tools/web_search.py +215 -0
  239. opscloud/ui/__init__.py +119 -0
  240. opscloud/ui/app.py +5400 -0
  241. opscloud/ui/app.tcss +479 -0
  242. opscloud/ui/clipboard.py +147 -0
  243. opscloud/ui/command_registry.py +482 -0
  244. opscloud/ui/message_store.py +152 -0
  245. opscloud/ui/permission_store.py +360 -0
  246. opscloud/ui/preferences.py +113 -0
  247. opscloud/ui/prompt_manager.py +96 -0
  248. opscloud/ui/remote_client.py +505 -0
  249. opscloud/ui/textual_adapter.py +1269 -0
  250. opscloud/ui/theme.py +728 -0
  251. opscloud/ui/ui_help.py +415 -0
  252. opscloud/ui/widgets/__init__.py +383 -0
  253. opscloud/ui/widgets/_ask_user_types.py +81 -0
  254. opscloud/ui/widgets/_copy_spans.py +52 -0
  255. opscloud/ui/widgets/_inline_prompt.py +290 -0
  256. opscloud/ui/widgets/_js_eval_display.py +139 -0
  257. opscloud/ui/widgets/_links.py +175 -0
  258. opscloud/ui/widgets/_tool_stream.py +308 -0
  259. opscloud/ui/widgets/agent_selector.py +297 -0
  260. opscloud/ui/widgets/approval.py +741 -0
  261. opscloud/ui/widgets/ask_user.py +539 -0
  262. opscloud/ui/widgets/auth.py +695 -0
  263. opscloud/ui/widgets/auth_manager.py +214 -0
  264. opscloud/ui/widgets/auto_mode_notice.py +146 -0
  265. opscloud/ui/widgets/autocomplete.py +709 -0
  266. opscloud/ui/widgets/chat_input.py +805 -0
  267. opscloud/ui/widgets/cloud_selector.py +528 -0
  268. opscloud/ui/widgets/config_manager.py +652 -0
  269. opscloud/ui/widgets/debug_console.py +1269 -0
  270. opscloud/ui/widgets/devops_renderers.py +184 -0
  271. opscloud/ui/widgets/diff.py +237 -0
  272. opscloud/ui/widgets/effort_selector.py +166 -0
  273. opscloud/ui/widgets/goal_review.py +612 -0
  274. opscloud/ui/widgets/goal_status.py +57 -0
  275. opscloud/ui/widgets/infra_panel.py +141 -0
  276. opscloud/ui/widgets/install_confirm.py +114 -0
  277. opscloud/ui/widgets/loading.py +170 -0
  278. opscloud/ui/widgets/mcp_viewer.py +1204 -0
  279. opscloud/ui/widgets/messages.py +4461 -0
  280. opscloud/ui/widgets/model_selector.py +863 -0
  281. opscloud/ui/widgets/notification_center.py +108 -0
  282. opscloud/ui/widgets/notification_settings.py +119 -0
  283. opscloud/ui/widgets/operation_card.py +106 -0
  284. opscloud/ui/widgets/permissions_manager.py +667 -0
  285. opscloud/ui/widgets/plugin_manager.py +1212 -0
  286. opscloud/ui/widgets/pool_selector.py +376 -0
  287. opscloud/ui/widgets/preamble.py +53 -0
  288. opscloud/ui/widgets/skills_viewer.py +432 -0
  289. opscloud/ui/widgets/status.py +1037 -0
  290. opscloud/ui/widgets/subagent_panel.py +927 -0
  291. opscloud/ui/widgets/theme_selector.py +232 -0
  292. opscloud/ui/widgets/thread_selector.py +358 -0
  293. opscloud/ui/widgets/toast.py +31 -0
  294. opscloud/ui/widgets/tool_display.py +325 -0
  295. opscloud/ui/widgets/tool_renderers.py +234 -0
  296. opscloud/ui/widgets/tool_widgets.py +278 -0
  297. opscloud/ui/widgets/welcome.py +421 -0
  298. opscloud/ui/widgets/welcome_popup.py +102 -0
  299. opscloud/utils/__init__.py +24 -0
  300. opscloud/utils/cost_estimation.py +169 -0
  301. opscloud/utils/git.py +390 -0
  302. opscloud/utils/logger.py +571 -0
  303. opscloud/utils/session_stats.py +115 -0
  304. opscloud/utils/startup_error.py +37 -0
  305. talkops_opscloud-0.3.0b1.dist-info/METADATA +449 -0
  306. talkops_opscloud-0.3.0b1.dist-info/RECORD +309 -0
  307. talkops_opscloud-0.3.0b1.dist-info/WHEEL +4 -0
  308. talkops_opscloud-0.3.0b1.dist-info/entry_points.txt +2 -0
  309. talkops_opscloud-0.3.0b1.dist-info/licenses/LICENSE +175 -0
@@ -0,0 +1,541 @@
1
+ """CLI helpers for plugin management in OpsCloud."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any, cast
8
+
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Callable
11
+
12
+ from opscloud.plugins.discovery import (
13
+ add_marketplace_source,
14
+ install_plugin,
15
+ list_available_plugins,
16
+ refresh_all_marketplaces,
17
+ refresh_marketplace,
18
+ remove_marketplace,
19
+ set_installed_plugin_enabled,
20
+ uninstall_plugin,
21
+ )
22
+ from opscloud.plugins.marketplace import (
23
+ MarketplaceError,
24
+ redact_marketplace_source,
25
+ redact_urls_in_text,
26
+ )
27
+ from opscloud.plugins.models import InstallScope
28
+ from opscloud.plugins.store import load_marketplace_records
29
+
30
+
31
+ def _resolve_project_root(scope: str | None) -> Path | None:
32
+ """Resolve the project root for project/local scoped commands.
33
+
34
+ Returns ``None`` when ``scope`` is ``"user"`` or the project root
35
+ cannot be determined.
36
+ """
37
+ if scope in {None, "user"}:
38
+ return None
39
+ from opscloud.config.paths import find_project_root
40
+
41
+ root = find_project_root()
42
+ if root is None and scope in {"project", "local"}:
43
+ # Fall back to cwd when no marker is found
44
+ root = Path.cwd()
45
+ return root
46
+
47
+
48
+ def setup_plugin_parser(
49
+ subparsers: Any, # noqa: ANN401
50
+ *,
51
+ make_help_action: Callable[[Callable[[], None]], type[argparse.Action]] | None = None,
52
+ add_output_args: Callable[[argparse.ArgumentParser], None] | None = None,
53
+ ) -> argparse.ArgumentParser:
54
+ """Set up the `plugin` CLI parser.
55
+
56
+ Args:
57
+ subparsers: Parent argparse subparsers object.
58
+ make_help_action: Optional factory for parser-specific help actions.
59
+ add_output_args: Optional callback that adds output-format flags.
60
+
61
+ Returns:
62
+ Plugin command parser.
63
+ """
64
+
65
+ def _default_help() -> None:
66
+ print("Usage: opscloud plugin {list,install,uninstall,enable,disable,marketplace}")
67
+
68
+ parent = argparse.ArgumentParser(add_help=False)
69
+ if make_help_action is not None:
70
+ parent.add_argument("-h", "--help", action=make_help_action(_default_help))
71
+ else:
72
+ parent.add_argument("-h", "--help", action="help")
73
+
74
+ parser = subparsers.add_parser(
75
+ "plugin",
76
+ aliases=["plugins"],
77
+ help="Manage plugins",
78
+ add_help=False,
79
+ parents=[parent],
80
+ )
81
+ if add_output_args is not None:
82
+ add_output_args(parser)
83
+ plugin_sub = parser.add_subparsers(dest="plugin_command")
84
+
85
+ list_parser = plugin_sub.add_parser("list", aliases=["ls"], help="List plugins")
86
+ if add_output_args is not None:
87
+ add_output_args(list_parser)
88
+
89
+ search_parser = plugin_sub.add_parser("search", help="Search available plugins")
90
+ search_parser.add_argument(
91
+ "query", help="Search query (matches name, description, category, or keywords)"
92
+ )
93
+ if add_output_args is not None:
94
+ add_output_args(search_parser)
95
+
96
+ install_parser = plugin_sub.add_parser("install", help="Install a plugin")
97
+ install_parser.add_argument("plugin_id")
98
+ install_parser.add_argument(
99
+ "--scope",
100
+ choices=["user", "project", "local"],
101
+ default="user",
102
+ help="Installation scope: user (global), project (shared), or local (gitignored)",
103
+ )
104
+ uninstall_parser = plugin_sub.add_parser("uninstall", help="Uninstall a plugin")
105
+ uninstall_parser.add_argument("plugin_id")
106
+ uninstall_parser.add_argument(
107
+ "--scope",
108
+ choices=["user", "project", "local"],
109
+ default=None,
110
+ help="Scope to uninstall from (omit to remove all scopes)",
111
+ )
112
+
113
+ enable_parser = plugin_sub.add_parser("enable", help="Enable a plugin")
114
+ enable_parser.add_argument("plugin_id")
115
+ enable_parser.add_argument(
116
+ "--scope",
117
+ choices=["user", "project", "local"],
118
+ default="user",
119
+ help="Scope to enable in",
120
+ )
121
+ disable_parser = plugin_sub.add_parser("disable", help="Disable a plugin")
122
+ disable_parser.add_argument("plugin_id")
123
+ disable_parser.add_argument(
124
+ "--scope",
125
+ choices=["user", "project", "local"],
126
+ default="user",
127
+ help="Scope to disable in",
128
+ )
129
+
130
+ refresh_parser = plugin_sub.add_parser(
131
+ "refresh", aliases=["update"], help="Refresh marketplace(s) to fetch latest plugins"
132
+ )
133
+ refresh_parser.add_argument(
134
+ "name",
135
+ nargs="?",
136
+ default=None,
137
+ help="Optional marketplace name to refresh (omit to refresh all)",
138
+ )
139
+ if add_output_args is not None:
140
+ add_output_args(refresh_parser)
141
+
142
+ marketplace_parser = plugin_sub.add_parser(
143
+ "marketplace", help="Manage plugin marketplaces"
144
+ )
145
+ marketplace_sub = marketplace_parser.add_subparsers(dest="marketplace_command")
146
+ marketplace_list = marketplace_sub.add_parser(
147
+ "list", aliases=["ls"], help="List marketplaces"
148
+ )
149
+ if add_output_args is not None:
150
+ add_output_args(marketplace_list)
151
+ marketplace_add = marketplace_sub.add_parser("add", help="Add a marketplace")
152
+ marketplace_add.add_argument("source")
153
+ marketplace_refresh = marketplace_sub.add_parser(
154
+ "refresh", aliases=["update"], help="Refresh marketplace(s) to fetch latest plugins"
155
+ )
156
+ marketplace_refresh.add_argument(
157
+ "name",
158
+ nargs="?",
159
+ default=None,
160
+ help="Optional marketplace name to refresh (omit to refresh all)",
161
+ )
162
+ if add_output_args is not None:
163
+ add_output_args(marketplace_refresh)
164
+ marketplace_remove = marketplace_sub.add_parser(
165
+ "remove", help="Remove a marketplace and uninstall its plugins"
166
+ )
167
+ marketplace_remove.add_argument("name")
168
+ return parser
169
+
170
+
171
+ def _plugin_list_rows(project_root: Path | None = None) -> list[dict[str, object]]:
172
+ return [
173
+ {"id": plugin_id, "description": description, "enabled": enabled}
174
+ for plugin_id, description, enabled in list_available_plugins(project_root=project_root)
175
+ ]
176
+
177
+
178
+ def execute_plugin_command(args: argparse.Namespace) -> str | None:
179
+ """Execute a plugin management command.
180
+
181
+ Args:
182
+ args: Parsed argparse namespace.
183
+
184
+ Returns:
185
+ Text output for slash-command callers, or `None` when output was written.
186
+
187
+ Raises:
188
+ SystemExit: With status 1 when a mutating command fails.
189
+ """
190
+ output_format = getattr(args, "output_format", "text")
191
+ command = getattr(args, "plugin_command", None)
192
+ if command is None:
193
+ text = "Usage: opscloud plugin {list,search,install,uninstall,enable,disable,refresh,marketplace}"
194
+ print(text)
195
+ return text
196
+ if command in {"list", "ls"}:
197
+ project_root = _resolve_project_root("project")
198
+ rows = _plugin_list_rows(project_root=project_root)
199
+ if output_format == "json":
200
+ import json
201
+ text = json.dumps({"plugins": rows}, indent=2)
202
+ print(text)
203
+ return None
204
+ if not rows:
205
+ text = "No plugin marketplaces configured."
206
+ print(text)
207
+ return text
208
+
209
+ from opscloud.plugins.store import load_all_enabled_plugin_ids, load_installed_plugins
210
+
211
+ installed_ids = load_installed_plugins()
212
+ enabled_ids = load_all_enabled_plugin_ids(project_root=project_root)
213
+
214
+ lines: list[str] = []
215
+ lines.append("Installed Plugins:")
216
+ if not installed_ids:
217
+ lines.append(" (none installed)")
218
+ else:
219
+ for pid in sorted(installed_ids):
220
+ status = "enabled" if pid in enabled_ids else "disabled"
221
+ lines.append(f" • {pid} [{status}]")
222
+ lines.append("")
223
+
224
+ records = load_marketplace_records(project_root=project_root)
225
+ found_any_marketplace = False
226
+ for name, record in sorted(records.items()):
227
+ try:
228
+ from opscloud.plugins.marketplace import load_marketplace_location
229
+
230
+ marketplace = load_marketplace_location(Path(record.install_location))
231
+ except Exception:
232
+ continue
233
+
234
+ found_any_marketplace = True
235
+ from opscloud.config.plugins import is_default_marketplace
236
+
237
+ mp_label = f"Available in {name}"
238
+ if getattr(record, "is_default", False) or is_default_marketplace(name):
239
+ mp_label += " (Default Marketplace)"
240
+ lines.append(f"{mp_label}:")
241
+
242
+ # Group plugins dynamically by category
243
+ categories: dict[str, list[Any]] = {}
244
+ for p in marketplace.plugins:
245
+ categories.setdefault(p.category_label, []).append(p)
246
+
247
+ for cat_label, cat_plugins in categories.items():
248
+ lines.append(f" {cat_label}:")
249
+ for p in cat_plugins:
250
+ desc = p.description or ""
251
+ short_desc = desc if len(desc) <= 60 else f"{desc[:57]}..."
252
+ lines.append(f" {p.name:<28} v{p.version or '0.1.0':<7} {short_desc}")
253
+ lines.append("")
254
+
255
+ if not found_any_marketplace:
256
+ simple_lines = []
257
+ for row in rows:
258
+ status = "enabled" if row["enabled"] else "disabled"
259
+ simple_lines.append(f"{status} {row['id']} {row['description']}".rstrip())
260
+ text = "\n".join(simple_lines)
261
+ print(text)
262
+ return text
263
+
264
+ text = "\n".join(lines).rstrip()
265
+ print(text)
266
+ return text
267
+ if command == "search":
268
+ query = (getattr(args, "query", "") or "").strip().lower()
269
+ if not query:
270
+ text = "Please provide a search query: opscloud plugin search <query>"
271
+ print(text)
272
+ return text
273
+
274
+ from opscloud.plugins.store import load_all_enabled_plugin_ids, load_installed_plugins
275
+
276
+ records = load_marketplace_records(project_root=_resolve_project_root("project"))
277
+ installed = load_installed_plugins()
278
+ enabled = load_all_enabled_plugin_ids()
279
+ matches: list[dict[str, Any]] = []
280
+
281
+ for mp_name, record in sorted(records.items()):
282
+ try:
283
+ from opscloud.plugins.marketplace import load_marketplace_location
284
+
285
+ marketplace = load_marketplace_location(Path(record.install_location))
286
+ except Exception:
287
+ continue
288
+ for plugin in marketplace.plugins:
289
+ keywords_str = " ".join(plugin.keywords).lower()
290
+ target_str = (
291
+ f"{plugin.name} {plugin.display_name or ''} {plugin.description or ''} "
292
+ f"{plugin.category or ''} {keywords_str}"
293
+ ).lower()
294
+ if query in target_str:
295
+ plugin_id = f"{plugin.name}@{marketplace.name}"
296
+ matches.append(
297
+ {
298
+ "id": plugin_id,
299
+ "name": plugin.name,
300
+ "display_name": plugin.display_name or plugin.name,
301
+ "marketplace": marketplace.name,
302
+ "category_label": plugin.category_label,
303
+ "category": plugin.category,
304
+ "version": plugin.version or "0.1.0",
305
+ "description": plugin.description or "",
306
+ "installed": plugin_id in installed,
307
+ "enabled": plugin_id in enabled,
308
+ }
309
+ )
310
+
311
+ if output_format == "json":
312
+ import json
313
+
314
+ text = json.dumps({"query": query, "results": matches}, indent=2)
315
+ print(text)
316
+ return None
317
+
318
+ if not matches:
319
+ text = f"No plugins found matching '{query}'."
320
+ print(text)
321
+ return text
322
+
323
+ lines = [f"Found {len(matches)} plugin(s) matching '{query}':\n"]
324
+ for m in matches:
325
+ status = (
326
+ "[installed & enabled]"
327
+ if m["enabled"]
328
+ else ("[installed]" if m["installed"] else "[available]")
329
+ )
330
+ lines.append(f" • {m['display_name']} ({m['id']}, v{m['version']}) {status}")
331
+ lines.append(f" Category: {m['category_label']}")
332
+ if m["description"]:
333
+ lines.append(f" {m['description']}")
334
+ lines.append("")
335
+ text = "\n".join(lines).rstrip()
336
+ print(text)
337
+ return text
338
+ if command == "install":
339
+ raw_scope = getattr(args, "scope", "user") or "user"
340
+ scope = cast(InstallScope, raw_scope)
341
+ project_root = _resolve_project_root(scope)
342
+ try:
343
+ instance = install_plugin(
344
+ args.plugin_id, scope=scope, project_root=project_root,
345
+ )
346
+ except (MarketplaceError, FileNotFoundError, OSError, ValueError) as exc:
347
+ text = f"Failed to install {args.plugin_id}: {exc}"
348
+ print(text)
349
+ raise SystemExit(1) from exc
350
+ details = ""
351
+ if instance.version is not None:
352
+ details = f" (version: {instance.version})"
353
+ scope_label = {"user": "for you", "project": "for all collaborators", "local": "for you in this repo"}
354
+ text = (
355
+ f"Installed plugin {instance.plugin_id}{details} "
356
+ f"({scope_label[scope]}). Run /reload to activate."
357
+ )
358
+ print(text)
359
+ return text
360
+ if command == "uninstall":
361
+ raw_scope = getattr(args, "scope", None)
362
+ scope = cast(InstallScope, raw_scope) if raw_scope else None
363
+ project_root = _resolve_project_root(scope) if scope else None
364
+ uninstall_plugin(args.plugin_id, scope=scope, project_root=project_root)
365
+ text = f"Uninstalled plugin {args.plugin_id}."
366
+ print(text)
367
+ return text
368
+ if command in {"enable", "disable"}:
369
+ enabled = command == "enable"
370
+ raw_scope = getattr(args, "scope", "user") or "user"
371
+ scope = cast(InstallScope, raw_scope)
372
+ project_root = _resolve_project_root(scope)
373
+ try:
374
+ set_installed_plugin_enabled(
375
+ args.plugin_id, enabled=enabled, scope=scope, project_root=project_root,
376
+ )
377
+ except (MarketplaceError, OSError, ValueError) as exc:
378
+ text = f"Failed to {command} {args.plugin_id}: {exc}"
379
+ print(text)
380
+ raise SystemExit(1) from exc
381
+ text = f"{command.title()}d plugin {args.plugin_id}."
382
+ print(text)
383
+ return text
384
+ if command == "marketplace":
385
+ marketplace_command = getattr(args, "marketplace_command", None)
386
+ if marketplace_command in {"list", "ls"}:
387
+ records = load_marketplace_records()
388
+ rows = [
389
+ {
390
+ "name": record.name,
391
+ "source_type": record.source_type,
392
+ "source": redact_marketplace_source(record.source),
393
+ "install_location": (
394
+ record.install_location
395
+ if record.source_type in {"directory", "file"}
396
+ else "<managed cache>"
397
+ ),
398
+ }
399
+ for record in records.values()
400
+ ]
401
+ if output_format == "json":
402
+ import json
403
+ text = json.dumps({"marketplaces": rows}, indent=2)
404
+ print(text)
405
+ return None
406
+ text = (
407
+ "No plugin marketplaces configured."
408
+ if not rows
409
+ else "\n".join(f"{row['name']} {row['source']}" for row in rows)
410
+ )
411
+ print(text)
412
+ return text
413
+ if marketplace_command == "add":
414
+ try:
415
+ marketplace = add_marketplace_source(args.source)
416
+ except (MarketplaceError, FileNotFoundError, OSError, ValueError) as exc:
417
+ source = redact_marketplace_source(args.source)
418
+ text = (
419
+ f"Failed to add marketplace {source}: "
420
+ f"{redact_urls_in_text(str(exc))}"
421
+ )
422
+ print(text)
423
+ raise SystemExit(1) from exc
424
+ text = (
425
+ f"Added marketplace {marketplace.name} "
426
+ f"({len(marketplace.plugins)} plugin(s))."
427
+ )
428
+ print(text)
429
+ return text
430
+ if marketplace_command == "remove":
431
+ removed = remove_marketplace(args.name)
432
+ text = (
433
+ f"Removed marketplace {args.name} and its installed plugins."
434
+ if removed
435
+ else f"Marketplace {args.name} is not configured."
436
+ )
437
+ print(text)
438
+ return text
439
+ if marketplace_command in {"refresh", "update"}:
440
+ target_name = getattr(args, "name", None)
441
+ if target_name:
442
+ try:
443
+ marketplace = refresh_marketplace(target_name)
444
+ except Exception as exc:
445
+ text = f"Failed to refresh marketplace {target_name}: {exc}"
446
+ print(text)
447
+ raise SystemExit(1) from exc
448
+
449
+ if output_format == "json":
450
+ import json
451
+
452
+ text = json.dumps(
453
+ {
454
+ "name": marketplace.name,
455
+ "plugin_count": len(marketplace.plugins),
456
+ "status": "refreshed",
457
+ },
458
+ indent=2,
459
+ )
460
+ print(text)
461
+ return None
462
+
463
+ text = f"Refreshed marketplace '{marketplace.name}' ({len(marketplace.plugins)} plugin(s) available)."
464
+ print(text)
465
+ return text
466
+
467
+ refreshed = refresh_all_marketplaces()
468
+ if output_format == "json":
469
+ import json
470
+
471
+ rows = [
472
+ {
473
+ "name": name,
474
+ "plugin_count": len(mp.plugins),
475
+ "status": "refreshed",
476
+ }
477
+ for name, mp in refreshed.items()
478
+ ]
479
+ text = json.dumps({"marketplaces": rows}, indent=2)
480
+ print(text)
481
+ return None
482
+
483
+ lines = [f"Refreshed {len(refreshed)} marketplace(s):"]
484
+ for name, mp in refreshed.items():
485
+ lines.append(f" • {name}: {len(mp.plugins)} plugin(s) available")
486
+ text = "\n".join(lines)
487
+ print(text)
488
+ return text
489
+ if command in {"refresh", "update"}:
490
+ target_name = getattr(args, "name", None)
491
+ if target_name:
492
+ try:
493
+ marketplace = refresh_marketplace(target_name)
494
+ except Exception as exc:
495
+ text = f"Failed to refresh marketplace {target_name}: {exc}"
496
+ print(text)
497
+ raise SystemExit(1) from exc
498
+
499
+ if output_format == "json":
500
+ import json
501
+
502
+ text = json.dumps(
503
+ {
504
+ "name": marketplace.name,
505
+ "plugin_count": len(marketplace.plugins),
506
+ "status": "refreshed",
507
+ },
508
+ indent=2,
509
+ )
510
+ print(text)
511
+ return None
512
+
513
+ text = f"Refreshed marketplace '{marketplace.name}' ({len(marketplace.plugins)} plugin(s) available)."
514
+ print(text)
515
+ return text
516
+
517
+ refreshed = refresh_all_marketplaces()
518
+ if output_format == "json":
519
+ import json
520
+
521
+ rows = [
522
+ {
523
+ "name": name,
524
+ "plugin_count": len(mp.plugins),
525
+ "status": "refreshed",
526
+ }
527
+ for name, mp in refreshed.items()
528
+ ]
529
+ text = json.dumps({"marketplaces": rows}, indent=2)
530
+ print(text)
531
+ return None
532
+
533
+ lines = [f"Refreshed {len(refreshed)} marketplace(s):"]
534
+ for name, mp in refreshed.items():
535
+ lines.append(f" • {name}: {len(mp.plugins)} plugin(s) available")
536
+ text = "\n".join(lines)
537
+ print(text)
538
+ return text
539
+ text = "Usage: opscloud plugin {list,search,install,uninstall,enable,disable,refresh,marketplace}"
540
+ print(text)
541
+ return text