Skip to main content

ai_agent/utils/plugins/
loader.rs

1//! Plugin loader - cache-only loading functions
2//!
3//! Ported from ~/claudecode/openclaudecode/src/utils/plugins/marketplaceManager.ts
4//!
5//! This module provides cache-only loading functions for marketplaces and plugins.
6//! These functions are used for startup paths that should never block on network.
7
8use std::fs;
9use std::path::PathBuf;
10
11use super::types::{KnownMarketplacesFile, PluginMarketplace, PluginMarketplaceEntry};
12use crate::utils::config::get_global_config_path;
13
14/// Get the path to the known marketplaces config file
15fn get_known_marketplaces_file() -> PathBuf {
16    get_global_config_path().join("known_marketplaces.json")
17}
18
19/// Read a cached marketplace from disk
20async fn read_cached_marketplace(install_location: &str) -> Option<PluginMarketplace> {
21    let marketplace_path = PathBuf::from(install_location)
22        .join(".ai-plugin")
23        .join("marketplace.json");
24
25    if !marketplace_path.exists() {
26        return None;
27    }
28
29    match fs::read_to_string(&marketplace_path) {
30        Ok(content) => match serde_json::from_str::<PluginMarketplace>(&content) {
31            Ok(marketplace) => Some(marketplace),
32            Err(e) => {
33                eprintln!(
34                    "Failed to parse marketplace at {}: {}",
35                    marketplace_path.display(),
36                    e
37                );
38                None
39            }
40        },
41        Err(e) => {
42            eprintln!(
43                "Failed to read marketplace at {}: {}",
44                marketplace_path.display(),
45                e
46            );
47            None
48        }
49    }
50}
51
52/// Load known marketplaces config from cache
53async fn load_known_marketplaces_config() -> Option<KnownMarketplacesFile> {
54    let config_file = get_known_marketplaces_file();
55
56    if !config_file.exists() {
57        return None;
58    }
59
60    match fs::read_to_string(&config_file) {
61        Ok(content) => match serde_json::from_str::<KnownMarketplacesFile>(&content) {
62            Ok(config) => Some(config),
63            Err(e) => {
64                eprintln!("Failed to parse known marketplaces: {}", e);
65                None
66            }
67        },
68        Err(e) => {
69            eprintln!("Failed to read known marketplaces file: {}", e);
70            None
71        }
72    }
73}
74
75/// Parse plugin identifier into name and marketplace
76///
77/// # Arguments
78/// * `plugin_id` - Plugin ID in format "name@marketplace"
79///
80/// # Returns
81/// Tuple of (name, marketplace) or (None, None) if invalid
82pub fn parse_plugin_identifier(plugin_id: &str) -> (Option<String>, Option<String>) {
83    if let Some(at_pos) = plugin_id.rfind('@') {
84        let name = plugin_id[..at_pos].to_string();
85        let marketplace = plugin_id[at_pos + 1..].to_string();
86        if !name.is_empty() && !marketplace.is_empty() {
87            return (Some(name), Some(marketplace));
88        }
89    }
90    (None, None)
91}
92
93/// Get a marketplace by name from cache only (no network)
94///
95/// Use this for startup paths that should never block on network.
96///
97/// # Arguments
98/// * `name` - Marketplace name
99///
100/// # Returns
101/// The marketplace or null if not found/cache missing
102pub async fn get_marketplace_cache_only(name: &str) -> Option<PluginMarketplace> {
103    let config_file = get_known_marketplaces_file();
104
105    if !config_file.exists() {
106        return None;
107    }
108
109    match fs::read_to_string(&config_file) {
110        Ok(content) => {
111            match serde_json::from_str::<KnownMarketplacesFile>(&content) {
112                Ok(config) => {
113                    if let Some(entry) = config.get(name) {
114                        // Try to read the marketplace from the install location
115                        if let Some(marketplace) =
116                            read_cached_marketplace(&entry.install_location).await
117                        {
118                            return Some(marketplace);
119                        }
120                    }
121                    None
122                }
123                Err(e) => {
124                    eprintln!("Failed to parse known marketplaces config: {}", e);
125                    None
126                }
127            }
128        }
129        Err(_) => None,
130    }
131}
132
133/// Get a plugin by ID from cache only (no network)
134///
135/// # Arguments
136/// * `plugin_id` - Plugin ID in format "name@marketplace"
137///
138/// # Returns
139/// The plugin entry and marketplace install location, or null if not found/cache missing
140pub async fn get_plugin_by_id_cache_only(
141    plugin_id: &str,
142) -> Option<(PluginMarketplaceEntry, String)> {
143    let (plugin_name, marketplace_name) = parse_plugin_identifier(plugin_id);
144    let plugin_name = plugin_name?;
145    let marketplace_name = marketplace_name?;
146
147    let config_file = get_known_marketplaces_file();
148
149    if !config_file.exists() {
150        return None;
151    }
152
153    match fs::read_to_string(&config_file) {
154        Ok(content) => {
155            match serde_json::from_str::<KnownMarketplacesFile>(&content) {
156                Ok(config) => {
157                    // Get marketplace config
158                    let marketplace_config = config.get(&marketplace_name)?;
159
160                    // Get the marketplace itself
161                    let marketplace = get_marketplace_cache_only(&marketplace_name).await?;
162
163                    // Find the plugin in the marketplace
164                    marketplace
165                        .plugins
166                        .into_iter()
167                        .find(|p| p.name == plugin_name)
168                        .map(|entry| (entry, marketplace_config.install_location.clone()))
169                }
170                Err(_) => None,
171            }
172        }
173        Err(_) => None,
174    }
175}
176
177/// Get all known marketplace names
178pub async fn get_known_marketplace_names() -> Vec<String> {
179    match load_known_marketplaces_config().await {
180        Some(config) => config.keys().cloned().collect(),
181        None => vec![],
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_parse_plugin_identifier_basic() {
191        let (name, marketplace) = parse_plugin_identifier("my-plugin@my-marketplace");
192        assert_eq!(name, Some("my-plugin".to_string()));
193        assert_eq!(marketplace, Some("my-marketplace".to_string()));
194    }
195
196    #[test]
197    fn test_parse_plugin_identifier_invalid() {
198        let (name, marketplace) = parse_plugin_identifier("invalid");
199        assert_eq!(name, None);
200        assert_eq!(marketplace, None);
201    }
202
203    #[test]
204    fn test_parse_plugin_identifier_empty_marketplace() {
205        let (name, marketplace) = parse_plugin_identifier("my-plugin@");
206        assert_eq!(name, None);
207        assert_eq!(marketplace, None);
208    }
209}