ai_agent/utils/plugins/
loader.rs1use std::fs;
9use std::path::PathBuf;
10
11use super::types::{KnownMarketplacesFile, PluginMarketplace, PluginMarketplaceEntry};
12use crate::plugin::types::{LoadedPlugin, PluginError, PluginLoadResult};
13use crate::utils::config::get_global_config_path;
14
15fn get_known_marketplaces_file() -> PathBuf {
17 get_global_config_path().join("known_marketplaces.json")
18}
19
20async fn read_cached_marketplace(install_location: &str) -> Option<PluginMarketplace> {
22 let marketplace_path = PathBuf::from(install_location)
23 .join(".ai-plugin")
24 .join("marketplace.json");
25
26 if !marketplace_path.exists() {
27 return None;
28 }
29
30 match fs::read_to_string(&marketplace_path) {
31 Ok(content) => match serde_json::from_str::<PluginMarketplace>(&content) {
32 Ok(marketplace) => Some(marketplace),
33 Err(e) => {
34 eprintln!(
35 "Failed to parse marketplace at {}: {}",
36 marketplace_path.display(),
37 e
38 );
39 None
40 }
41 },
42 Err(e) => {
43 eprintln!(
44 "Failed to read marketplace at {}: {}",
45 marketplace_path.display(),
46 e
47 );
48 None
49 }
50 }
51}
52
53async fn load_known_marketplaces_config() -> Option<KnownMarketplacesFile> {
55 let config_file = get_known_marketplaces_file();
56
57 if !config_file.exists() {
58 return None;
59 }
60
61 match fs::read_to_string(&config_file) {
62 Ok(content) => match serde_json::from_str::<KnownMarketplacesFile>(&content) {
63 Ok(config) => Some(config),
64 Err(e) => {
65 eprintln!("Failed to parse known marketplaces: {}", e);
66 None
67 }
68 },
69 Err(e) => {
70 eprintln!("Failed to read known marketplaces file: {}", e);
71 None
72 }
73 }
74}
75
76pub fn parse_plugin_identifier(plugin_id: &str) -> (Option<String>, Option<String>) {
84 if let Some(at_pos) = plugin_id.rfind('@') {
85 let name = plugin_id[..at_pos].to_string();
86 let marketplace = plugin_id[at_pos + 1..].to_string();
87 if !name.is_empty() && !marketplace.is_empty() {
88 return (Some(name), Some(marketplace));
89 }
90 }
91 (None, None)
92}
93
94pub async fn get_marketplace_cache_only(name: &str) -> Option<PluginMarketplace> {
104 let config_file = get_known_marketplaces_file();
105
106 if !config_file.exists() {
107 return None;
108 }
109
110 match fs::read_to_string(&config_file) {
111 Ok(content) => {
112 match serde_json::from_str::<KnownMarketplacesFile>(&content) {
113 Ok(config) => {
114 if let Some(entry) = config.get(name) {
115 if let Some(marketplace) =
117 read_cached_marketplace(&entry.install_location).await
118 {
119 return Some(marketplace);
120 }
121 }
122 None
123 }
124 Err(e) => {
125 eprintln!("Failed to parse known marketplaces config: {}", e);
126 None
127 }
128 }
129 }
130 Err(_) => None,
131 }
132}
133
134pub async fn get_plugin_by_id_cache_only(
142 plugin_id: &str,
143) -> Option<(PluginMarketplaceEntry, String)> {
144 let (plugin_name, marketplace_name) = parse_plugin_identifier(plugin_id);
145 let plugin_name = plugin_name?;
146 let marketplace_name = marketplace_name?;
147
148 let config_file = get_known_marketplaces_file();
149
150 if !config_file.exists() {
151 return None;
152 }
153
154 match fs::read_to_string(&config_file) {
155 Ok(content) => {
156 match serde_json::from_str::<KnownMarketplacesFile>(&content) {
157 Ok(config) => {
158 let marketplace_config = config.get(&marketplace_name)?;
160
161 let marketplace = get_marketplace_cache_only(&marketplace_name).await?;
163
164 marketplace
166 .plugins
167 .into_iter()
168 .find(|p| p.name == plugin_name)
169 .map(|entry| (entry, marketplace_config.install_location.clone()))
170 }
171 Err(_) => None,
172 }
173 }
174 Err(_) => None,
175 }
176}
177
178pub async fn get_known_marketplace_names() -> Vec<String> {
180 match load_known_marketplaces_config().await {
181 Some(config) => config.keys().cloned().collect(),
182 None => vec![],
183 }
184}
185
186pub async fn load_all_plugins() -> Result<PluginLoadResult, Box<dyn std::error::Error + Send + Sync>>
191{
192 Ok(PluginLoadResult {
195 enabled: vec![],
196 disabled: vec![],
197 errors: vec![],
198 })
199}
200
201pub async fn load_all_plugins_cache_only()
205-> Result<PluginLoadResult, Box<dyn std::error::Error + Send + Sync>> {
206 load_all_plugins().await
207}
208
209pub fn get_plugin_cache_path() -> String {
211 get_global_config_path()
212 .join("plugins")
213 .to_string_lossy()
214 .to_string()
215}
216
217pub fn get_versioned_cache_path(plugin_id: &str, version: &str) -> String {
219 let (name, marketplace) = parse_plugin_identifier(plugin_id);
220 let marketplace = marketplace.unwrap_or_else(|| "unknown".to_string());
221 let name = name.unwrap_or_else(|| plugin_id.to_string());
222 get_global_config_path()
223 .join("plugins")
224 .join(&marketplace)
225 .join(&name)
226 .join(version)
227 .to_string_lossy()
228 .to_string()
229}
230
231pub fn get_versioned_zip_cache_path(plugin_id: &str, version: &str) -> String {
233 format!("{}.zip", get_versioned_cache_path(plugin_id, version))
234}
235
236pub async fn cache_plugin(
238 _source: &super::schemas::PluginSource,
239 _entry: &PluginMarketplaceEntry,
240) -> Result<CachePluginResult, Box<dyn std::error::Error + Send + Sync>> {
241 Err("cache_plugin not fully implemented - stub".into())
243}
244
245pub struct CachePluginResult {
247 pub path: String,
248 pub manifest: serde_json::Value,
249 pub git_commit_sha: Option<String>,
250}
251
252pub fn clear_plugin_cache(_marketplace: Option<&str>) {
254 }
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn test_parse_plugin_identifier_basic() {
263 let (name, marketplace) = parse_plugin_identifier("my-plugin@my-marketplace");
264 assert_eq!(name, Some("my-plugin".to_string()));
265 assert_eq!(marketplace, Some("my-marketplace".to_string()));
266 }
267
268 #[test]
269 fn test_parse_plugin_identifier_invalid() {
270 let (name, marketplace) = parse_plugin_identifier("invalid");
271 assert_eq!(name, None);
272 assert_eq!(marketplace, None);
273 }
274
275 #[test]
276 fn test_parse_plugin_identifier_empty_marketplace() {
277 let (name, marketplace) = parse_plugin_identifier("my-plugin@");
278 assert_eq!(name, None);
279 assert_eq!(marketplace, None);
280 }
281}