Skip to main content

ai_agent/utils/plugins/
marketplace_helpers.rs

1// Source: ~/claudecode/openclaudecode/src/utils/plugins/marketplaceHelpers.ts
2#![allow(dead_code)]
3
4use std::collections::{HashMap, HashSet};
5
6use super::marketplace_manager::get_marketplace;
7use super::schemas::MarketplaceSource;
8use super::types::KnownMarketplace;
9
10/// Format plugin failure details for user display.
11pub fn format_failure_details(failures: &[PluginFailure], include_reasons: bool) -> String {
12    let max_show = 2;
13    let details: Vec<String> = failures
14        .iter()
15        .take(max_show)
16        .map(|f| {
17            let reason = f
18                .reason
19                .as_deref()
20                .or(f.error.as_deref())
21                .unwrap_or("unknown error");
22            if include_reasons {
23                format!("{} ({})", f.name, reason)
24            } else {
25                f.name.clone()
26            }
27        })
28        .collect();
29
30    let remaining = failures.len().saturating_sub(max_show);
31    let more_text = if remaining > 0 {
32        format!(" and {} more", remaining)
33    } else {
34        String::new()
35    };
36
37    let sep = if include_reasons { "; " } else { ", " };
38    format!("{}{}", details.join(sep), more_text)
39}
40
41#[derive(Debug, Clone)]
42pub struct PluginFailure {
43    pub name: String,
44    pub reason: Option<String>,
45    pub error: Option<String>,
46}
47
48/// Extract source display string from marketplace configuration.
49pub fn get_marketplace_source_display(source: &MarketplaceSource) -> String {
50    match source {
51        MarketplaceSource::Github { repo, .. } => repo.clone(),
52        MarketplaceSource::Url { url } => url.clone(),
53        MarketplaceSource::Git { url, .. } => url.clone(),
54        MarketplaceSource::Directory { path } => path.clone(),
55        MarketplaceSource::File { path } => path.clone(),
56        MarketplaceSource::Settings { name, .. } => format!("settings:{}", name),
57        MarketplaceSource::GitSubdir { url, .. } => url.clone(),
58    }
59}
60
61/// Create a plugin ID from plugin name and marketplace name.
62pub fn create_plugin_id(plugin_name: &str, marketplace_name: &str) -> String {
63    format!("{}@{}", plugin_name, marketplace_name)
64}
65
66/// Load marketplaces with graceful degradation for individual failures.
67pub async fn load_marketplaces_with_graceful_degradation(
68    config: &HashMap<String, KnownMarketplace>,
69) -> MarketplaceLoadResult {
70    let mut marketplaces = Vec::new();
71    let mut failures = Vec::new();
72
73    for (name, marketplace_config) in config {
74        if !is_plugin_source_allowed_by_policy(&marketplace_config.source) {
75            continue;
76        }
77
78        let data = match get_marketplace(name).await {
79            Ok(d) => Some(d),
80            Err(err) => {
81                failures.push(MarketplaceFailure {
82                    name: name.clone(),
83                    error: err.to_string(),
84                });
85                log::error!("Failed to load marketplace {}: {}", name, err);
86                None
87            }
88        };
89
90        marketplaces.push(MarketplaceLoadEntry {
91            name: name.clone(),
92            config: marketplace_config.clone(),
93            data,
94        });
95    }
96
97    MarketplaceLoadResult {
98        marketplaces,
99        failures,
100    }
101}
102
103#[derive(Debug)]
104pub struct MarketplaceLoadResult {
105    pub marketplaces: Vec<MarketplaceLoadEntry>,
106    pub failures: Vec<MarketplaceFailure>,
107}
108
109#[derive(Debug)]
110pub struct MarketplaceLoadEntry {
111    pub name: String,
112    pub config: KnownMarketplace,
113    pub data: Option<super::types::PluginMarketplace>,
114}
115
116#[derive(Debug)]
117pub struct MarketplaceFailure {
118    pub name: String,
119    pub error: String,
120}
121
122/// Format marketplace loading failures into appropriate user messages.
123pub fn format_marketplace_loading_errors(
124    failures: &[MarketplaceFailure],
125    success_count: usize,
126) -> Option<MarketplaceLoadingError> {
127    if failures.is_empty() {
128        return None;
129    }
130
131    if success_count > 0 {
132        let message = if failures.len() == 1 {
133            format!(
134                "Warning: Failed to load marketplace '{}': {}",
135                failures[0].name, failures[0].error
136            )
137        } else {
138            let names: Vec<&str> = failures.iter().map(|f| f.name.as_str()).collect();
139            format!(
140                "Warning: Failed to load {} marketplaces: {}",
141                failures.len(),
142                names.join(", ")
143            )
144        };
145        Some(MarketplaceLoadingError {
146            error_type: "warning".to_string(),
147            message,
148        })
149    } else {
150        let errors: Vec<String> = failures
151            .iter()
152            .map(|f| format!("{}: {}", f.name, f.error))
153            .collect();
154        Some(MarketplaceLoadingError {
155            error_type: "error".to_string(),
156            message: format!(
157                "Failed to load all marketplaces. Errors: {}",
158                errors.join("; ")
159            ),
160        })
161    }
162}
163
164#[derive(Debug)]
165pub struct MarketplaceLoadingError {
166    pub error_type: String,
167    pub message: String,
168}
169
170/// Get the strict marketplace source allowlist from policy settings.
171pub fn get_strict_known_marketplaces() -> Option<Vec<MarketplaceSource>> {
172    None
173}
174
175/// Get the marketplace source blocklist from policy settings.
176pub fn get_blocked_marketplaces() -> Option<Vec<MarketplaceSource>> {
177    None
178}
179
180/// Get the custom plugin trust message from policy settings.
181pub fn get_plugin_trust_message() -> Option<String> {
182    None
183}
184
185/// Check if a marketplace source is allowed by enterprise policy.
186pub fn is_source_allowed_by_policy(_source: &MarketplaceSource) -> bool {
187    true
188}
189
190/// Check if a plugin source is allowed by enterprise policy.
191pub fn is_plugin_source_allowed_by_policy(_source: &super::types::PluginSource) -> bool {
192    true
193}
194
195/// Check if a marketplace source is explicitly in the blocklist.
196pub fn is_source_in_blocklist(source: &MarketplaceSource) -> bool {
197    match get_blocked_marketplaces() {
198        None => false,
199        Some(blocklist) => blocklist
200            .iter()
201            .any(|blocked| sources_equal(source, blocked)),
202    }
203}
204
205/// Compare two MarketplaceSource objects for equality.
206fn sources_equal(a: &MarketplaceSource, b: &MarketplaceSource) -> bool {
207    match (a, b) {
208        (MarketplaceSource::Url { url: a_url }, MarketplaceSource::Url { url: b_url }) => {
209            a_url == b_url
210        }
211        (
212            MarketplaceSource::Github {
213                repo: a_repo,
214                ref_: a_ref,
215                path: a_path,
216            },
217            MarketplaceSource::Github {
218                repo: b_repo,
219                ref_: b_ref,
220                path: b_path,
221            },
222        ) => a_repo == b_repo && a_ref == b_ref && a_path == b_path,
223        (
224            MarketplaceSource::Directory { path: a_path },
225            MarketplaceSource::Directory { path: b_path },
226        ) => a_path == b_path,
227        (MarketplaceSource::File { path: a_path }, MarketplaceSource::File { path: b_path }) => {
228            a_path == b_path
229        }
230        _ => false,
231    }
232}
233
234/// Extract the host/domain from a marketplace source.
235pub fn extract_host_from_source(source: &MarketplaceSource) -> Option<String> {
236    match source {
237        MarketplaceSource::Github { .. } => Some("github.com".to_string()),
238        MarketplaceSource::Git { url, .. } | MarketplaceSource::Url { url } => url::Url::parse(url)
239            .ok()
240            .and_then(|u| u.host_str().map(|h| h.to_string())),
241        _ => None,
242    }
243}
244
245/// Format a MarketplaceSource for display in error messages.
246pub fn format_source_for_display(source: &MarketplaceSource) -> String {
247    match source {
248        MarketplaceSource::Github { repo, ref_: r, .. } => {
249            format!(
250                "github:{}{}",
251                repo,
252                r.as_ref().map(|r| format!("@{}", r)).unwrap_or_default()
253            )
254        }
255        MarketplaceSource::Url { url } => url.clone(),
256        MarketplaceSource::Git { url, ref_: r, .. } => {
257            format!(
258                "git:{}{}",
259                url,
260                r.as_ref().map(|r| format!("@{}", r)).unwrap_or_default()
261            )
262        }
263        MarketplaceSource::Directory { path } => format!("dir:{}", path),
264        MarketplaceSource::File { path } => format!("file:{}", path),
265        MarketplaceSource::Settings { name, plugins } => {
266            format!(
267                "settings:{} ({} plugin{})",
268                name,
269                plugins.len(),
270                if plugins.len() == 1 { "" } else { "s" }
271            )
272        }
273        MarketplaceSource::GitSubdir { url, path, .. } => {
274            format!("git-subdir:{}:{}", url, path)
275        }
276    }
277}