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.
- opscloud/__init__.py +5 -0
- opscloud/__main__.py +6 -0
- opscloud/_constants.py +53 -0
- opscloud/_debug.py +145 -0
- opscloud/_debug_buffer.py +250 -0
- opscloud/_textual_patches.py +367 -0
- opscloud/_version.py +15 -0
- opscloud/agent/__init__.py +27 -0
- opscloud/agent/config.py +168 -0
- opscloud/agent/factory.py +1103 -0
- opscloud/approval_mode.py +44 -0
- opscloud/backend/__init__.py +49 -0
- opscloud/backend/composite.py +89 -0
- opscloud/backend/local.py +183 -0
- opscloud/backend/registry.py +79 -0
- opscloud/backend/sandbox_config.py +107 -0
- opscloud/backend/sandbox_factory.py +432 -0
- opscloud/built_in_skills/remember/SKILL.md +124 -0
- opscloud/built_in_skills/skill-creator/SKILL.md +348 -0
- opscloud/built_in_skills/skill-creator/scripts/init_skill.py +371 -0
- opscloud/built_in_skills/skill-creator/scripts/quick_validate.py +165 -0
- opscloud/cli/__init__.py +1 -0
- opscloud/cli/commands/__init__.py +15 -0
- opscloud/cli/commands/auth.py +45 -0
- opscloud/cli/commands/doctor.py +100 -0
- opscloud/cli/commands/mcp.py +137 -0
- opscloud/cli/commands/threads.py +51 -0
- opscloud/cli/interactive.py +154 -0
- opscloud/cli/main.py +903 -0
- opscloud/cli/non_interactive.py +425 -0
- opscloud/cli/server_manager.py +58 -0
- opscloud/commands/__init__.py +19 -0
- opscloud/commands/_base.py +110 -0
- opscloud/commands/_guards.py +52 -0
- opscloud/commands/_router.py +146 -0
- opscloud/commands/_types.py +36 -0
- opscloud/commands/core/__init__.py +47 -0
- opscloud/commands/core/auth.py +94 -0
- opscloud/commands/core/bug.py +46 -0
- opscloud/commands/core/clear.py +106 -0
- opscloud/commands/core/cloud.py +185 -0
- opscloud/commands/core/compact.py +128 -0
- opscloud/commands/core/config_cmd.py +120 -0
- opscloud/commands/core/context.py +220 -0
- opscloud/commands/core/cost.py +44 -0
- opscloud/commands/core/doctor.py +535 -0
- opscloud/commands/core/effort.py +108 -0
- opscloud/commands/core/exit_cmd.py +41 -0
- opscloud/commands/core/fast.py +148 -0
- opscloud/commands/core/help_cmd.py +94 -0
- opscloud/commands/core/mcp.py +134 -0
- opscloud/commands/core/model.py +109 -0
- opscloud/commands/core/permissions.py +196 -0
- opscloud/commands/core/plugins.py +92 -0
- opscloud/commands/core/pool.py +108 -0
- opscloud/commands/core/resume.py +97 -0
- opscloud/commands/core/skills.py +178 -0
- opscloud/commands/power/__init__.py +51 -0
- opscloud/commands/power/agents.py +98 -0
- opscloud/commands/power/btw.py +85 -0
- opscloud/commands/power/copy.py +97 -0
- opscloud/commands/power/goal.py +548 -0
- opscloud/commands/power/loop.py +295 -0
- opscloud/commands/power/memory.py +287 -0
- opscloud/commands/power/review.py +152 -0
- opscloud/commands/power/rubric.py +287 -0
- opscloud/commands/power/runtime.py +321 -0
- opscloud/commands/power/skill_creator.py +70 -0
- opscloud/commands/power/skill_invoke.py +165 -0
- opscloud/commands/power/tasks.py +209 -0
- opscloud/commands/power/trace.py +117 -0
- opscloud/commands/power/ui_toggles.py +135 -0
- opscloud/commands/power/version.py +52 -0
- opscloud/commands/registry.py +176 -0
- opscloud/config/__init__.py +114 -0
- opscloud/config/adapters/__init__.py +5 -0
- opscloud/config/adapters/postgres.py +776 -0
- opscloud/config/adapters/sqlite.py +769 -0
- opscloud/config/aws.py +248 -0
- opscloud/config/cloud_profiles.py +437 -0
- opscloud/config/env_vars.py +81 -0
- opscloud/config/langsmith.py +319 -0
- opscloud/config/manifest.py +817 -0
- opscloud/config/metadata.py +205 -0
- opscloud/config/paths.py +604 -0
- opscloud/config/plugins.py +130 -0
- opscloud/config/settings.py +834 -0
- opscloud/config/store.py +453 -0
- opscloud/config/store_factory.py +97 -0
- opscloud/config/toml_config.py +562 -0
- opscloud/editor.py +173 -0
- opscloud/exceptions.py +65 -0
- opscloud/file_ops.py +217 -0
- opscloud/hooks/__init__.py +139 -0
- opscloud/hooks/env.py +44 -0
- opscloud/hooks/interrupt.py +121 -0
- opscloud/hooks/models/__init__.py +109 -0
- opscloud/hooks/models/adapters.py +19 -0
- opscloud/hooks/models/config.py +79 -0
- opscloud/hooks/models/domain.py +434 -0
- opscloud/hooks/models/transport.py +37 -0
- opscloud/hooks/tools.py +231 -0
- opscloud/input.py +48 -0
- opscloud/integrations/__init__.py +80 -0
- opscloud/integrations/base.py +153 -0
- opscloud/integrations/content_filter.py +119 -0
- opscloud/integrations/event_bus.py +480 -0
- opscloud/integrations/hooks.py +238 -0
- opscloud/integrations/identity.py +181 -0
- opscloud/integrations/notifications.py +134 -0
- opscloud/integrations/stream_bridge.py +397 -0
- opscloud/integrations/thread_store.py +81 -0
- opscloud/json_types.py +20 -0
- opscloud/mcp/__init__.py +57 -0
- opscloud/mcp/config.py +203 -0
- opscloud/mcp/discovery.py +229 -0
- opscloud/mcp/mcp_info.py +109 -0
- opscloud/mcp/middleware.py +102 -0
- opscloud/mcp/preload.py +505 -0
- opscloud/mcp/raw_config.py +149 -0
- opscloud/mcp/semantic_profiler.py +305 -0
- opscloud/mcp/session_manager.py +727 -0
- opscloud/mcp/trust.py +105 -0
- opscloud/memory/__init__.py +27 -0
- opscloud/memory/branch.py +61 -0
- opscloud/memory/guard.py +19 -0
- opscloud/memory/onboarding.py +120 -0
- opscloud/memory/registry.py +134 -0
- opscloud/memory/store.py +223 -0
- opscloud/middleware/__init__.py +106 -0
- opscloud/middleware/_repository_bounds.py +459 -0
- opscloud/middleware/ask_user.py +152 -0
- opscloud/middleware/auto_mode.py +60 -0
- opscloud/middleware/auto_mode_hitl.py +2434 -0
- opscloud/middleware/compaction.py +566 -0
- opscloud/middleware/configurable_model.py +233 -0
- opscloud/middleware/cost_tracking.py +1021 -0
- opscloud/middleware/glm_stall_recovery.py +109 -0
- opscloud/middleware/goal_criteria.py +1234 -0
- opscloud/middleware/goal_state_notice.py +712 -0
- opscloud/middleware/goal_tools.py +407 -0
- opscloud/middleware/headless_mcp_guard.py +88 -0
- opscloud/middleware/jev_model_router.py +1018 -0
- opscloud/middleware/local_context.py +460 -0
- opscloud/middleware/mcp_context.py +151 -0
- opscloud/middleware/mcp_middleware.py +146 -0
- opscloud/middleware/memory_guard.py +393 -0
- opscloud/middleware/model_retry.py +205 -0
- opscloud/middleware/registry.py +137 -0
- opscloud/middleware/reliable_rubric.py +709 -0
- opscloud/middleware/resume_state.py +91 -0
- opscloud/middleware/server_hooks.py +1189 -0
- opscloud/middleware/shell_allow_list.py +90 -0
- opscloud/middleware/skills.py +382 -0
- opscloud/middleware/subagent_artifacts.py +84 -0
- opscloud/middleware/subagent_telemetry.py +261 -0
- opscloud/middleware/subagents.py +243 -0
- opscloud/middleware/tool_filter.py +384 -0
- opscloud/middleware/unified_system_message.py +62 -0
- opscloud/model/__init__.py +37 -0
- opscloud/model/config.py +2992 -0
- opscloud/model/factory.py +477 -0
- opscloud/model/pool.py +459 -0
- opscloud/model/reasoning.py +282 -0
- opscloud/offload.py +173 -0
- opscloud/output.py +64 -0
- opscloud/plugins/__init__.py +189 -0
- opscloud/plugins/_json.py +22 -0
- opscloud/plugins/adapters/__init__.py +13 -0
- opscloud/plugins/adapters/agents.py +112 -0
- opscloud/plugins/adapters/commands.py +190 -0
- opscloud/plugins/adapters/mcp.py +363 -0
- opscloud/plugins/adapters/skills.py +132 -0
- opscloud/plugins/commands_cli.py +541 -0
- opscloud/plugins/discovery.py +512 -0
- opscloud/plugins/manifest.py +335 -0
- opscloud/plugins/marketplace.py +893 -0
- opscloud/plugins/models.py +334 -0
- opscloud/plugins/project_plugins.py +317 -0
- opscloud/plugins/protocol.py +31 -0
- opscloud/plugins/store.py +905 -0
- opscloud/plugins/substitution.py +122 -0
- opscloud/project_utils.py +145 -0
- opscloud/prompts/__init__.py +37 -0
- opscloud/prompts/system.py +483 -0
- opscloud/prompts/templates/system_prompt.md +166 -0
- opscloud/prompts/types.py +54 -0
- opscloud/rubrics/__init__.py +42 -0
- opscloud/rubrics/evaluator.py +404 -0
- opscloud/rubrics/evidence_extractor.py +229 -0
- opscloud/rubrics/generator.py +239 -0
- opscloud/rubrics/jev_compiler.py +175 -0
- opscloud/rubrics/jev_grader.py +297 -0
- opscloud/rubrics/middleware.py +8 -0
- opscloud/schema/__init__.py +107 -0
- opscloud/schema/interrupts.py +981 -0
- opscloud/security/__init__.py +91 -0
- opscloud/security/approval_mode.py +528 -0
- opscloud/security/approval_mode_source.py +230 -0
- opscloud/security/cli_ast_evaluator.py +552 -0
- opscloud/security/jev_classifier.py +651 -0
- opscloud/security/shell_safety.py +340 -0
- opscloud/security/unicode_security.py +439 -0
- opscloud/security/url_validation.py +160 -0
- opscloud/server/__init__.py +13 -0
- opscloud/server/_server_config.py +158 -0
- opscloud/server/server.py +242 -0
- opscloud/server/server_graph.py +93 -0
- opscloud/skills/__init__.py +91 -0
- opscloud/skills/commands.py +1099 -0
- opscloud/skills/invocation.py +200 -0
- opscloud/skills/load.py +388 -0
- opscloud/skills/loader.py +23 -0
- opscloud/skills/merge.py +65 -0
- opscloud/skills/registry.py +263 -0
- opscloud/skills/sources.py +174 -0
- opscloud/skills/trust.py +417 -0
- opscloud/state/__init__.py +142 -0
- opscloud/state/base.py +154 -0
- opscloud/state/cloud_context.py +262 -0
- opscloud/state/goal_channels.py +124 -0
- opscloud/state/goal_state_limits.py +175 -0
- opscloud/state/resume_state.py +58 -0
- opscloud/state/service_context.py +281 -0
- opscloud/state/session.py +957 -0
- opscloud/state/state_migration.py +86 -0
- opscloud/subagents/__init__.py +30 -0
- opscloud/subagents/loader.py +331 -0
- opscloud/subagents/subagents_parser.py +117 -0
- opscloud/subagents/types.py +25 -0
- opscloud/tools/__init__.py +36 -0
- opscloud/tools/catalog.py +37 -0
- opscloud/tools/display.py +24 -0
- opscloud/tools/fetch_url.py +176 -0
- opscloud/tools/goal_tools.py +423 -0
- opscloud/tools/registry.py +105 -0
- opscloud/tools/thread.py +32 -0
- opscloud/tools/web_search.py +215 -0
- opscloud/ui/__init__.py +119 -0
- opscloud/ui/app.py +5400 -0
- opscloud/ui/app.tcss +479 -0
- opscloud/ui/clipboard.py +147 -0
- opscloud/ui/command_registry.py +482 -0
- opscloud/ui/message_store.py +152 -0
- opscloud/ui/permission_store.py +360 -0
- opscloud/ui/preferences.py +113 -0
- opscloud/ui/prompt_manager.py +96 -0
- opscloud/ui/remote_client.py +505 -0
- opscloud/ui/textual_adapter.py +1269 -0
- opscloud/ui/theme.py +728 -0
- opscloud/ui/ui_help.py +415 -0
- opscloud/ui/widgets/__init__.py +383 -0
- opscloud/ui/widgets/_ask_user_types.py +81 -0
- opscloud/ui/widgets/_copy_spans.py +52 -0
- opscloud/ui/widgets/_inline_prompt.py +290 -0
- opscloud/ui/widgets/_js_eval_display.py +139 -0
- opscloud/ui/widgets/_links.py +175 -0
- opscloud/ui/widgets/_tool_stream.py +308 -0
- opscloud/ui/widgets/agent_selector.py +297 -0
- opscloud/ui/widgets/approval.py +741 -0
- opscloud/ui/widgets/ask_user.py +539 -0
- opscloud/ui/widgets/auth.py +695 -0
- opscloud/ui/widgets/auth_manager.py +214 -0
- opscloud/ui/widgets/auto_mode_notice.py +146 -0
- opscloud/ui/widgets/autocomplete.py +709 -0
- opscloud/ui/widgets/chat_input.py +805 -0
- opscloud/ui/widgets/cloud_selector.py +528 -0
- opscloud/ui/widgets/config_manager.py +652 -0
- opscloud/ui/widgets/debug_console.py +1269 -0
- opscloud/ui/widgets/devops_renderers.py +184 -0
- opscloud/ui/widgets/diff.py +237 -0
- opscloud/ui/widgets/effort_selector.py +166 -0
- opscloud/ui/widgets/goal_review.py +612 -0
- opscloud/ui/widgets/goal_status.py +57 -0
- opscloud/ui/widgets/infra_panel.py +141 -0
- opscloud/ui/widgets/install_confirm.py +114 -0
- opscloud/ui/widgets/loading.py +170 -0
- opscloud/ui/widgets/mcp_viewer.py +1204 -0
- opscloud/ui/widgets/messages.py +4461 -0
- opscloud/ui/widgets/model_selector.py +863 -0
- opscloud/ui/widgets/notification_center.py +108 -0
- opscloud/ui/widgets/notification_settings.py +119 -0
- opscloud/ui/widgets/operation_card.py +106 -0
- opscloud/ui/widgets/permissions_manager.py +667 -0
- opscloud/ui/widgets/plugin_manager.py +1212 -0
- opscloud/ui/widgets/pool_selector.py +376 -0
- opscloud/ui/widgets/preamble.py +53 -0
- opscloud/ui/widgets/skills_viewer.py +432 -0
- opscloud/ui/widgets/status.py +1037 -0
- opscloud/ui/widgets/subagent_panel.py +927 -0
- opscloud/ui/widgets/theme_selector.py +232 -0
- opscloud/ui/widgets/thread_selector.py +358 -0
- opscloud/ui/widgets/toast.py +31 -0
- opscloud/ui/widgets/tool_display.py +325 -0
- opscloud/ui/widgets/tool_renderers.py +234 -0
- opscloud/ui/widgets/tool_widgets.py +278 -0
- opscloud/ui/widgets/welcome.py +421 -0
- opscloud/ui/widgets/welcome_popup.py +102 -0
- opscloud/utils/__init__.py +24 -0
- opscloud/utils/cost_estimation.py +169 -0
- opscloud/utils/git.py +390 -0
- opscloud/utils/logger.py +571 -0
- opscloud/utils/session_stats.py +115 -0
- opscloud/utils/startup_error.py +37 -0
- talkops_opscloud-0.3.0b1.dist-info/METADATA +449 -0
- talkops_opscloud-0.3.0b1.dist-info/RECORD +309 -0
- talkops_opscloud-0.3.0b1.dist-info/WHEEL +4 -0
- talkops_opscloud-0.3.0b1.dist-info/entry_points.txt +2 -0
- talkops_opscloud-0.3.0b1.dist-info/licenses/LICENSE +175 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
"""Plugin discovery, install, and enablement helpers for OpsCloud."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from functools import partial
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from opscloud.utils.logger import get_logger
|
|
11
|
+
|
|
12
|
+
from opscloud.config.plugins import (
|
|
13
|
+
get_default_marketplace_names,
|
|
14
|
+
get_primary_default_marketplace_name,
|
|
15
|
+
)
|
|
16
|
+
from opscloud.plugins.manifest import (
|
|
17
|
+
PluginManifestError,
|
|
18
|
+
build_inventory,
|
|
19
|
+
load_manifest,
|
|
20
|
+
)
|
|
21
|
+
from opscloud.plugins.marketplace import (
|
|
22
|
+
MarketplaceError,
|
|
23
|
+
load_marketplace,
|
|
24
|
+
load_marketplace_location,
|
|
25
|
+
materialize_marketplace_source,
|
|
26
|
+
materialize_plugin_source,
|
|
27
|
+
parse_marketplace_source,
|
|
28
|
+
redact_urls_in_text,
|
|
29
|
+
refresh_marketplace_catalog,
|
|
30
|
+
)
|
|
31
|
+
from opscloud.plugins.models import (
|
|
32
|
+
InstallScope,
|
|
33
|
+
MarketplacePluginEntry,
|
|
34
|
+
MarketplaceRecord,
|
|
35
|
+
PluginDiscoveryResult,
|
|
36
|
+
PluginInstance,
|
|
37
|
+
PluginMarketplace,
|
|
38
|
+
RepositoryMarketplaceSource,
|
|
39
|
+
split_plugin_id,
|
|
40
|
+
)
|
|
41
|
+
from opscloud.plugins.store import (
|
|
42
|
+
cache_and_register_plugin,
|
|
43
|
+
ensure_marketplace_cache_dir,
|
|
44
|
+
ensure_plugin_data_dir,
|
|
45
|
+
get_primary_install_entry,
|
|
46
|
+
load_all_enabled_plugin_ids,
|
|
47
|
+
load_enabled_plugin_ids,
|
|
48
|
+
load_local_enabled_plugin_ids,
|
|
49
|
+
load_project_enabled_plugin_ids,
|
|
50
|
+
load_installed_plugins,
|
|
51
|
+
load_marketplace_records,
|
|
52
|
+
plugin_data_dir,
|
|
53
|
+
plugin_mutation_lock,
|
|
54
|
+
remove_marketplace_record,
|
|
55
|
+
save_marketplace_record,
|
|
56
|
+
set_plugin_enabled_for_scope,
|
|
57
|
+
uninstall_plugin as uninstall_plugin_record,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
logger = get_logger(__name__)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@plugin_mutation_lock()
|
|
64
|
+
def add_local_marketplace(path: str | Path) -> PluginMarketplace:
|
|
65
|
+
"""Add a local marketplace to OpsCloud state."""
|
|
66
|
+
marketplace = load_marketplace(Path(path))
|
|
67
|
+
save_marketplace_record(
|
|
68
|
+
MarketplaceRecord(
|
|
69
|
+
name=marketplace.name,
|
|
70
|
+
source_type="directory",
|
|
71
|
+
source=str(marketplace.root),
|
|
72
|
+
install_location=str(marketplace.root),
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
return marketplace
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@plugin_mutation_lock()
|
|
79
|
+
def add_marketplace_source(raw: str) -> PluginMarketplace:
|
|
80
|
+
"""Add a marketplace from a pasted source string."""
|
|
81
|
+
source = parse_marketplace_source(raw)
|
|
82
|
+
marketplace, location = materialize_marketplace_source(source)
|
|
83
|
+
save_marketplace_record(
|
|
84
|
+
MarketplaceRecord(
|
|
85
|
+
name=marketplace.name,
|
|
86
|
+
source_type=source.source_type,
|
|
87
|
+
source=source.value,
|
|
88
|
+
install_location=str(location),
|
|
89
|
+
ref=source.ref if isinstance(source, RepositoryMarketplaceSource) else None,
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
return marketplace
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@plugin_mutation_lock()
|
|
96
|
+
def remove_marketplace(name: str) -> bool:
|
|
97
|
+
"""Remove a marketplace and every plugin installed from it."""
|
|
98
|
+
record = load_marketplace_records().get(name)
|
|
99
|
+
if record is None:
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
installed = load_installed_plugins(strict=True)
|
|
103
|
+
enabled = load_enabled_plugin_ids(strict=True)
|
|
104
|
+
plugin_ids = set(installed) | set(enabled)
|
|
105
|
+
for plugin_id in plugin_ids:
|
|
106
|
+
try:
|
|
107
|
+
_plugin_name, marketplace_name = split_plugin_id(plugin_id)
|
|
108
|
+
except ValueError:
|
|
109
|
+
continue
|
|
110
|
+
if marketplace_name == name:
|
|
111
|
+
uninstall_plugin(plugin_id)
|
|
112
|
+
|
|
113
|
+
removed = remove_marketplace_record(name)
|
|
114
|
+
location = Path(record.install_location)
|
|
115
|
+
try:
|
|
116
|
+
resolved = location.resolve()
|
|
117
|
+
cache_root = ensure_marketplace_cache_dir().resolve()
|
|
118
|
+
except OSError:
|
|
119
|
+
return removed
|
|
120
|
+
if record.source_type in {"github", "git", "url"} and resolved.is_relative_to(
|
|
121
|
+
cache_root
|
|
122
|
+
):
|
|
123
|
+
if resolved.is_dir():
|
|
124
|
+
shutil.rmtree(resolved, ignore_errors=True)
|
|
125
|
+
elif resolved.is_file():
|
|
126
|
+
resolved.unlink(missing_ok=True)
|
|
127
|
+
return removed
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@plugin_mutation_lock()
|
|
131
|
+
def refresh_marketplace(
|
|
132
|
+
name: str, *, project_root: Path | None = None
|
|
133
|
+
) -> PluginMarketplace:
|
|
134
|
+
"""Refresh a marketplace from its remote source and update local cache and records."""
|
|
135
|
+
records = load_marketplace_records(project_root=project_root)
|
|
136
|
+
record = records.get(name)
|
|
137
|
+
if record is None:
|
|
138
|
+
msg = f"Marketplace {name!r} is not configured"
|
|
139
|
+
raise MarketplaceError(msg)
|
|
140
|
+
|
|
141
|
+
marketplace = refresh_marketplace_catalog(record)
|
|
142
|
+
save_marketplace_record(
|
|
143
|
+
MarketplaceRecord(
|
|
144
|
+
name=marketplace.name,
|
|
145
|
+
source_type=record.source_type,
|
|
146
|
+
source=record.source,
|
|
147
|
+
install_location=record.install_location,
|
|
148
|
+
ref=record.ref,
|
|
149
|
+
plugin_count=len(marketplace.plugins),
|
|
150
|
+
is_default=record.is_default,
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
return marketplace
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@plugin_mutation_lock()
|
|
157
|
+
def refresh_all_marketplaces(
|
|
158
|
+
*, project_root: Path | None = None
|
|
159
|
+
) -> dict[str, PluginMarketplace]:
|
|
160
|
+
"""Refresh all configured marketplaces."""
|
|
161
|
+
records = load_marketplace_records(project_root=project_root)
|
|
162
|
+
results: dict[str, PluginMarketplace] = {}
|
|
163
|
+
for name in records:
|
|
164
|
+
try:
|
|
165
|
+
results[name] = refresh_marketplace(name, project_root=project_root)
|
|
166
|
+
except Exception as exc:
|
|
167
|
+
logger.warning("Failed to refresh marketplace %s: %s", name, exc)
|
|
168
|
+
return results
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _normalize_plugin_id(plugin_id: str) -> str:
|
|
172
|
+
"""Normalize plugin id, resolving across available marketplaces or defaulting to the primary default marketplace."""
|
|
173
|
+
if "@" in plugin_id:
|
|
174
|
+
return plugin_id
|
|
175
|
+
|
|
176
|
+
records = load_marketplace_records()
|
|
177
|
+
for name, rec in records.items():
|
|
178
|
+
try:
|
|
179
|
+
mp = load_marketplace_location(Path(rec.install_location))
|
|
180
|
+
if any(p.name == plugin_id for p in mp.plugins):
|
|
181
|
+
return f"{plugin_id}@{name}"
|
|
182
|
+
except Exception:
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
return f"{plugin_id}@{get_primary_default_marketplace_name()}"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _resolve_installed_plugin_id(plugin_id: str) -> str:
|
|
189
|
+
"""Resolve plugin id against currently installed plugins."""
|
|
190
|
+
if "@" in plugin_id:
|
|
191
|
+
return plugin_id
|
|
192
|
+
|
|
193
|
+
installed = load_installed_plugins()
|
|
194
|
+
prefix = f"{plugin_id}@"
|
|
195
|
+
matches = [pid for pid in installed if pid.startswith(prefix)]
|
|
196
|
+
if len(matches) == 1:
|
|
197
|
+
return matches[0]
|
|
198
|
+
if matches:
|
|
199
|
+
default_names = get_default_marketplace_names()
|
|
200
|
+
for m in matches:
|
|
201
|
+
mp_part = m.split("@", 1)[1]
|
|
202
|
+
if mp_part in default_names:
|
|
203
|
+
return m
|
|
204
|
+
return matches[0]
|
|
205
|
+
|
|
206
|
+
return f"{plugin_id}@{get_primary_default_marketplace_name()}"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _require_installed_plugin(plugin_id: str) -> None:
|
|
210
|
+
canonical_id = _resolve_installed_plugin_id(plugin_id)
|
|
211
|
+
if canonical_id not in load_installed_plugins(strict=True):
|
|
212
|
+
msg = f"Plugin {plugin_id!r} is not installed"
|
|
213
|
+
raise MarketplaceError(msg)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@plugin_mutation_lock()
|
|
217
|
+
def set_installed_plugin_enabled(
|
|
218
|
+
plugin_id: str,
|
|
219
|
+
enabled: bool = True,
|
|
220
|
+
*,
|
|
221
|
+
scope: InstallScope = "user",
|
|
222
|
+
project_root: Path | None = None,
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Set the enabled state of an installed plugin at the given scope."""
|
|
225
|
+
canonical_id = _resolve_installed_plugin_id(plugin_id)
|
|
226
|
+
if scope != "project":
|
|
227
|
+
try:
|
|
228
|
+
_require_installed_plugin(canonical_id)
|
|
229
|
+
except MarketplaceError:
|
|
230
|
+
pass
|
|
231
|
+
set_plugin_enabled_for_scope(
|
|
232
|
+
canonical_id, enabled, scope=scope, project_root=project_root
|
|
233
|
+
)
|
|
234
|
+
if enabled:
|
|
235
|
+
ensure_plugin_data_dir(canonical_id)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@plugin_mutation_lock()
|
|
239
|
+
def uninstall_plugin(
|
|
240
|
+
plugin_id: str,
|
|
241
|
+
*,
|
|
242
|
+
scope: InstallScope | None = None,
|
|
243
|
+
project_root: Path | None = None,
|
|
244
|
+
) -> None:
|
|
245
|
+
"""Uninstall a plugin (disable, clear records, delete orphaned cache)."""
|
|
246
|
+
canonical_id = _resolve_installed_plugin_id(plugin_id)
|
|
247
|
+
effective_scope = scope or ("project" if project_root else "user")
|
|
248
|
+
set_plugin_enabled_for_scope(
|
|
249
|
+
canonical_id, False, scope=effective_scope, project_root=project_root
|
|
250
|
+
)
|
|
251
|
+
uninstall_plugin_record(
|
|
252
|
+
canonical_id, scope=scope, project_root=project_root
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _resolve_marketplace_and_entry(
|
|
257
|
+
plugin_id: str,
|
|
258
|
+
) -> tuple[PluginMarketplace, MarketplacePluginEntry]:
|
|
259
|
+
canonical_id = _normalize_plugin_id(plugin_id)
|
|
260
|
+
try:
|
|
261
|
+
plugin_name, marketplace_name = split_plugin_id(canonical_id)
|
|
262
|
+
except ValueError as exc:
|
|
263
|
+
raise MarketplaceError(str(exc)) from exc
|
|
264
|
+
records = load_marketplace_records()
|
|
265
|
+
record = records.get(marketplace_name)
|
|
266
|
+
if record is None:
|
|
267
|
+
msg = f"Marketplace {marketplace_name!r} is not configured"
|
|
268
|
+
raise MarketplaceError(msg)
|
|
269
|
+
marketplace = load_marketplace_location(Path(record.install_location))
|
|
270
|
+
entry = next(
|
|
271
|
+
(plugin for plugin in marketplace.plugins if plugin.name == plugin_name),
|
|
272
|
+
None,
|
|
273
|
+
)
|
|
274
|
+
if entry is None:
|
|
275
|
+
msg = f"Plugin {plugin_id!r} not found in marketplace {marketplace_name}"
|
|
276
|
+
raise MarketplaceError(msg)
|
|
277
|
+
return marketplace, entry
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@plugin_mutation_lock()
|
|
281
|
+
def install_plugin(
|
|
282
|
+
plugin_id: str,
|
|
283
|
+
*,
|
|
284
|
+
scope: InstallScope = "user",
|
|
285
|
+
project_root: Path | None = None,
|
|
286
|
+
) -> PluginInstance:
|
|
287
|
+
"""Install a marketplace plugin into the versioned cache and enable it.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
plugin_id: Plugin id in ``name@marketplace`` form or shorthand ``name``.
|
|
291
|
+
scope: Installation scope — ``"user"``, ``"project"``, or ``"local"``.
|
|
292
|
+
project_root: Required when ``scope`` is ``"project"`` or ``"local"``.
|
|
293
|
+
"""
|
|
294
|
+
load_installed_plugins(strict=True)
|
|
295
|
+
canonical_id = _normalize_plugin_id(plugin_id)
|
|
296
|
+
marketplace, entry = _resolve_marketplace_and_entry(canonical_id)
|
|
297
|
+
source_root = materialize_plugin_source(marketplace, entry)
|
|
298
|
+
if source_root is None:
|
|
299
|
+
msg = (
|
|
300
|
+
f"Plugin {canonical_id} has unsupported source "
|
|
301
|
+
f"{redact_urls_in_text(repr(entry.source))}; "
|
|
302
|
+
"use a local path, GitHub repository, or Git repository source"
|
|
303
|
+
)
|
|
304
|
+
raise MarketplaceError(msg)
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
manifest, _manifest_path, manifest_warnings = load_manifest(
|
|
308
|
+
source_root, fallback_name=entry.name
|
|
309
|
+
)
|
|
310
|
+
except PluginManifestError as exc:
|
|
311
|
+
msg = f"Cannot install {canonical_id}: {exc}"
|
|
312
|
+
raise MarketplaceError(msg) from exc
|
|
313
|
+
|
|
314
|
+
for warning in manifest_warnings:
|
|
315
|
+
logger.debug("Plugin install warning for %s: %s", canonical_id, warning)
|
|
316
|
+
|
|
317
|
+
version = manifest.version if manifest is not None else None
|
|
318
|
+
cache_path = cache_and_register_plugin(
|
|
319
|
+
canonical_id,
|
|
320
|
+
source_root,
|
|
321
|
+
version=version,
|
|
322
|
+
scope=scope,
|
|
323
|
+
project_root=project_root,
|
|
324
|
+
validate=partial(
|
|
325
|
+
_validate_plugin_copy,
|
|
326
|
+
plugin_id=canonical_id,
|
|
327
|
+
fallback_name=entry.name,
|
|
328
|
+
),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
set_plugin_enabled_for_scope(
|
|
332
|
+
canonical_id, True, scope=scope, project_root=project_root
|
|
333
|
+
)
|
|
334
|
+
ensure_plugin_data_dir(canonical_id)
|
|
335
|
+
|
|
336
|
+
instance, warnings = _plugin_from_install_path(
|
|
337
|
+
plugin_id=canonical_id,
|
|
338
|
+
root=cache_path,
|
|
339
|
+
marketplace_name=marketplace.name,
|
|
340
|
+
fallback_name=entry.name,
|
|
341
|
+
)
|
|
342
|
+
if instance is None:
|
|
343
|
+
detail = "; ".join(warnings)
|
|
344
|
+
uninstall_plugin_record(
|
|
345
|
+
canonical_id, scope=scope, project_root=project_root
|
|
346
|
+
)
|
|
347
|
+
msg = f"Installed {canonical_id} but failed to load from cache: {detail}"
|
|
348
|
+
raise MarketplaceError(msg)
|
|
349
|
+
return instance
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _validate_plugin_copy(
|
|
353
|
+
root: Path,
|
|
354
|
+
*,
|
|
355
|
+
plugin_id: str,
|
|
356
|
+
fallback_name: str,
|
|
357
|
+
) -> None:
|
|
358
|
+
try:
|
|
359
|
+
manifest, _manifest_path, warnings = load_manifest(
|
|
360
|
+
root, fallback_name=fallback_name
|
|
361
|
+
)
|
|
362
|
+
except PluginManifestError as exc:
|
|
363
|
+
msg = f"Cannot install {plugin_id}: {exc}"
|
|
364
|
+
raise MarketplaceError(msg) from exc
|
|
365
|
+
build_inventory(root, manifest, warnings)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _plugin_from_install_path(
|
|
369
|
+
*,
|
|
370
|
+
plugin_id: str,
|
|
371
|
+
root: Path,
|
|
372
|
+
marketplace_name: str,
|
|
373
|
+
fallback_name: str,
|
|
374
|
+
) -> tuple[PluginInstance | None, tuple[str, ...]]:
|
|
375
|
+
warnings: list[str] = []
|
|
376
|
+
try:
|
|
377
|
+
manifest, _manifest_path, manifest_warnings = load_manifest(
|
|
378
|
+
root, fallback_name=fallback_name
|
|
379
|
+
)
|
|
380
|
+
except PluginManifestError as exc:
|
|
381
|
+
return None, (f"Skipping plugin {plugin_id}: {exc}",)
|
|
382
|
+
warnings.extend(manifest_warnings)
|
|
383
|
+
name = manifest.name if manifest and manifest.name else fallback_name
|
|
384
|
+
inventory = build_inventory(root, manifest, tuple(warnings))
|
|
385
|
+
try:
|
|
386
|
+
instance = PluginInstance(
|
|
387
|
+
plugin_id=plugin_id,
|
|
388
|
+
name=name,
|
|
389
|
+
marketplace=marketplace_name,
|
|
390
|
+
version=manifest.version if manifest is not None else None,
|
|
391
|
+
root=root,
|
|
392
|
+
data_dir=plugin_data_dir(plugin_id),
|
|
393
|
+
manifest=manifest,
|
|
394
|
+
inventory=inventory,
|
|
395
|
+
)
|
|
396
|
+
except ValueError as exc:
|
|
397
|
+
return None, (f"Skipping plugin {plugin_id}: {exc}",)
|
|
398
|
+
return instance, inventory.warnings
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def discover_plugins(
|
|
402
|
+
project_root: Path | None = None,
|
|
403
|
+
store: Any = None,
|
|
404
|
+
) -> PluginDiscoveryResult:
|
|
405
|
+
"""Discover enabled marketplace plugins from their install cache paths.
|
|
406
|
+
|
|
407
|
+
Uses merged enablement across user + project + local scopes.
|
|
408
|
+
"""
|
|
409
|
+
enabled = load_all_enabled_plugin_ids(project_root=project_root)
|
|
410
|
+
plugins: list[PluginInstance] = []
|
|
411
|
+
warnings: list[str] = []
|
|
412
|
+
seen_plugin_ids: set[str] = set()
|
|
413
|
+
|
|
414
|
+
# 1. Local filesystem discovery (e.g. project plugins directory)
|
|
415
|
+
if project_root:
|
|
416
|
+
proj_plugins = Path(project_root) / "plugins"
|
|
417
|
+
if proj_plugins.is_dir():
|
|
418
|
+
for p in sorted(proj_plugins.iterdir()):
|
|
419
|
+
if p.is_dir() and not p.name.startswith("."):
|
|
420
|
+
p_id = f"{p.name}@project"
|
|
421
|
+
inst, p_warns = _plugin_from_install_path(
|
|
422
|
+
plugin_id=p_id,
|
|
423
|
+
root=p,
|
|
424
|
+
marketplace_name="project",
|
|
425
|
+
fallback_name=p.name,
|
|
426
|
+
)
|
|
427
|
+
warnings.extend(p_warns)
|
|
428
|
+
if inst:
|
|
429
|
+
plugins.append(inst)
|
|
430
|
+
seen_plugin_ids.add(p_id)
|
|
431
|
+
|
|
432
|
+
enabled = load_all_enabled_plugin_ids(project_root=project_root)
|
|
433
|
+
|
|
434
|
+
for plugin_id in sorted(enabled):
|
|
435
|
+
if plugin_id in seen_plugin_ids:
|
|
436
|
+
continue
|
|
437
|
+
try:
|
|
438
|
+
plugin_name, marketplace_name = split_plugin_id(plugin_id)
|
|
439
|
+
except ValueError:
|
|
440
|
+
warnings.append(f"Ignoring invalid plugin id {plugin_id!r}")
|
|
441
|
+
continue
|
|
442
|
+
entry = get_primary_install_entry(plugin_id)
|
|
443
|
+
needs_install = entry is None
|
|
444
|
+
if entry is not None:
|
|
445
|
+
root = Path(entry.install_path)
|
|
446
|
+
try:
|
|
447
|
+
if not root.is_dir():
|
|
448
|
+
needs_install = True
|
|
449
|
+
except (OSError, RuntimeError):
|
|
450
|
+
needs_install = True
|
|
451
|
+
|
|
452
|
+
if needs_install:
|
|
453
|
+
try:
|
|
454
|
+
scope: InstallScope = "user"
|
|
455
|
+
if project_root:
|
|
456
|
+
if plugin_id in load_local_enabled_plugin_ids(project_root):
|
|
457
|
+
scope = "local"
|
|
458
|
+
elif plugin_id in load_project_enabled_plugin_ids(project_root):
|
|
459
|
+
scope = "project"
|
|
460
|
+
instance = install_plugin(
|
|
461
|
+
plugin_id, scope=scope, project_root=project_root
|
|
462
|
+
)
|
|
463
|
+
if instance is not None:
|
|
464
|
+
plugins.append(instance)
|
|
465
|
+
continue
|
|
466
|
+
except Exception as exc:
|
|
467
|
+
warnings.append(
|
|
468
|
+
f"Plugin {plugin_id} is enabled but auto-install failed: {exc}"
|
|
469
|
+
)
|
|
470
|
+
continue
|
|
471
|
+
|
|
472
|
+
assert entry is not None
|
|
473
|
+
root = Path(entry.install_path)
|
|
474
|
+
try:
|
|
475
|
+
plugin, plugin_warnings = _plugin_from_install_path(
|
|
476
|
+
plugin_id=plugin_id,
|
|
477
|
+
root=root,
|
|
478
|
+
marketplace_name=marketplace_name,
|
|
479
|
+
fallback_name=plugin_name,
|
|
480
|
+
)
|
|
481
|
+
except (OSError, RuntimeError) as exc:
|
|
482
|
+
warnings.append(f"Skipping plugin {plugin_id}: {exc}")
|
|
483
|
+
continue
|
|
484
|
+
warnings.extend(plugin_warnings)
|
|
485
|
+
if plugin is not None:
|
|
486
|
+
plugins.append(plugin)
|
|
487
|
+
|
|
488
|
+
return PluginDiscoveryResult(plugins=tuple(plugins), warnings=tuple(warnings))
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def list_available_plugins(
|
|
492
|
+
project_root: Path | None = None,
|
|
493
|
+
) -> tuple[tuple[str, str, bool], ...]:
|
|
494
|
+
"""List plugins from configured marketplaces."""
|
|
495
|
+
records = load_marketplace_records(project_root=project_root)
|
|
496
|
+
enabled = load_all_enabled_plugin_ids(project_root=project_root)
|
|
497
|
+
rows: list[tuple[str, str, bool]] = []
|
|
498
|
+
for name, record in sorted(records.items()):
|
|
499
|
+
try:
|
|
500
|
+
marketplace = load_marketplace_location(Path(record.install_location))
|
|
501
|
+
except MarketplaceError as exc:
|
|
502
|
+
rows.append((f"<marketplace:{name}>", str(exc), False))
|
|
503
|
+
continue
|
|
504
|
+
for plugin in marketplace.plugins:
|
|
505
|
+
plugin_id = f"{plugin.name}@{marketplace.name}"
|
|
506
|
+
rows.append((plugin_id, plugin.description or "", plugin_id in enabled))
|
|
507
|
+
return tuple(rows)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def list_installed_plugin_ids() -> frozenset[str]:
|
|
511
|
+
"""Return plugin ids that have install records."""
|
|
512
|
+
return frozenset(load_installed_plugins())
|