1use super::*;
12use crate::git::{GitPluginConfig, GitPluginLoader, GitPluginSource};
13use crate::loader::PluginLoader;
14use crate::metadata::{MetadataStore, PluginMetadata};
15use crate::remote::{RemotePluginConfig, RemotePluginLoader};
16use crate::signature::SignatureVerifier;
17use std::path::{Path, PathBuf};
18
19#[derive(Debug, Clone)]
21pub enum PluginSource {
22 Local(PathBuf),
24 Url {
26 url: String,
28 checksum: Option<String>,
30 },
31 Git(GitPluginSource),
33 Registry {
35 name: String,
37 version: Option<String>,
39 },
40}
41
42impl PluginSource {
43 pub fn parse(input: &str) -> LoaderResult<Self> {
51 let input = input.trim();
52
53 if input.starts_with("http://") || input.starts_with("https://") {
55 if input.contains(".git")
57 || input.contains("github.com")
58 || input.contains("gitlab.com")
59 {
60 let source = GitPluginSource::parse(input)?;
61 return Ok(PluginSource::Git(source));
62 }
63 return Ok(PluginSource::Url {
64 url: input.to_string(),
65 checksum: None,
66 });
67 }
68
69 if input.starts_with("git@") {
71 let source = GitPluginSource::parse(input)?;
72 return Ok(PluginSource::Git(source));
73 }
74
75 if input.contains('/') || input.contains('\\') || Path::new(input).exists() {
77 return Ok(PluginSource::Local(PathBuf::from(input)));
78 }
79
80 let (name, version) = if let Some((n, v)) = input.split_once('@') {
82 (n.to_string(), Some(v.to_string()))
83 } else {
84 (input.to_string(), None)
85 };
86
87 Ok(PluginSource::Registry { name, version })
88 }
89}
90
91impl std::fmt::Display for PluginSource {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 match self {
94 PluginSource::Local(path) => write!(f, "local:{}", path.display()),
95 PluginSource::Url { url, .. } => write!(f, "url:{}", url),
96 PluginSource::Git(source) => write!(f, "git:{}", source),
97 PluginSource::Registry { name, version } => {
98 if let Some(v) = version {
99 write!(f, "registry:{}@{}", name, v)
100 } else {
101 write!(f, "registry:{}", name)
102 }
103 }
104 }
105 }
106}
107
108#[derive(Debug, Clone)]
110pub struct InstallOptions {
111 pub force: bool,
113 pub skip_validation: bool,
115 pub verify_signature: bool,
117 pub expected_checksum: Option<String>,
119}
120
121impl Default for InstallOptions {
122 fn default() -> Self {
123 Self {
124 force: false,
125 skip_validation: false,
126 verify_signature: true,
127 expected_checksum: None,
128 }
129 }
130}
131
132pub struct PluginInstaller {
134 loader: PluginLoader,
135 remote_loader: RemotePluginLoader,
136 git_loader: GitPluginLoader,
137 config: PluginLoaderConfig,
138 metadata_store: std::sync::Arc<tokio::sync::RwLock<MetadataStore>>,
139}
140
141impl PluginInstaller {
142 pub fn new(loader_config: PluginLoaderConfig) -> LoaderResult<Self> {
144 let loader = PluginLoader::new(loader_config.clone());
145 let remote_loader = RemotePluginLoader::new(RemotePluginConfig::default())?;
146 let git_loader = GitPluginLoader::new(GitPluginConfig::default())?;
147
148 let metadata_dir = shellexpand::tilde("~/.mockforge/plugin-metadata");
150 let metadata_store = MetadataStore::new(PathBuf::from(metadata_dir.as_ref()));
151
152 Ok(Self {
153 loader,
154 remote_loader,
155 git_loader,
156 config: loader_config,
157 metadata_store: std::sync::Arc::new(tokio::sync::RwLock::new(metadata_store)),
158 })
159 }
160
161 pub async fn init(&self) -> LoaderResult<()> {
163 let mut store = self.metadata_store.write().await;
164 store.load().await
165 }
166
167 pub async fn install(
171 &self,
172 source_str: &str,
173 options: InstallOptions,
174 ) -> LoaderResult<PluginId> {
175 let source = PluginSource::parse(source_str)?;
176 self.install_from_source(&source, options).await
177 }
178
179 pub async fn install_from_source(
181 &self,
182 source: &PluginSource,
183 options: InstallOptions,
184 ) -> LoaderResult<PluginId> {
185 tracing::info!("Installing plugin from source: {}", source);
186
187 let plugin_dir = match source {
189 PluginSource::Local(path) => path.clone(),
190 PluginSource::Url { url, checksum } => {
191 let checksum_ref = checksum.as_deref().or(options.expected_checksum.as_deref());
192 self.remote_loader.download_with_checksum(url, checksum_ref).await?
193 }
194 PluginSource::Git(git_source) => self.git_loader.clone_from_git(git_source).await?,
195 PluginSource::Registry { name, version } => {
196 return Err(PluginLoaderError::load(format!(
197 "Registry plugin installation not yet implemented: {}@{}",
198 name,
199 version.as_deref().unwrap_or("latest")
200 )));
201 }
202 };
203
204 if options.verify_signature && !options.skip_validation {
206 if let Err(e) = self.verify_plugin_signature(&plugin_dir) {
207 tracing::warn!("Plugin signature verification failed: {}", e);
208 }
210 }
211
212 if !options.skip_validation {
214 self.loader.validate_plugin(&plugin_dir).await?;
215 }
216
217 let manifest = self.loader.validate_plugin(&plugin_dir).await?;
219 let plugin_id = manifest.info.id.clone();
220
221 if self.loader.get_plugin(&plugin_id).await.is_some() && !options.force {
223 return Err(PluginLoaderError::already_loaded(plugin_id));
224 }
225
226 self.loader.load_plugin(&plugin_id).await?;
228
229 let version = manifest.info.version.to_string();
231 let metadata = PluginMetadata::new(plugin_id.clone(), source.clone(), version);
232 let mut store = self.metadata_store.write().await;
233 store.save(metadata).await?;
234
235 tracing::info!("Plugin installed successfully: {}", plugin_id);
236 Ok(plugin_id)
237 }
238
239 fn verify_plugin_signature(&self, plugin_dir: &Path) -> LoaderResult<()> {
241 let verifier = SignatureVerifier::new(&self.config);
242 verifier.verify_plugin_signature(plugin_dir)
243 }
244
245 pub async fn uninstall(&self, plugin_id: &PluginId) -> LoaderResult<()> {
247 self.loader.unload_plugin(plugin_id).await?;
248
249 let mut store = self.metadata_store.write().await;
251 store.remove(plugin_id).await?;
252
253 Ok(())
254 }
255
256 pub async fn list_installed(&self) -> Vec<PluginId> {
258 self.loader.list_plugins().await
259 }
260
261 pub async fn update(&self, plugin_id: &PluginId) -> LoaderResult<()> {
263 tracing::info!("Updating plugin: {}", plugin_id);
264
265 let metadata = {
267 let store = self.metadata_store.read().await;
268 store.get(plugin_id).cloned().ok_or_else(|| {
269 PluginLoaderError::load(format!(
270 "No installation metadata found for plugin {}. Cannot update.",
271 plugin_id
272 ))
273 })?
274 };
275
276 tracing::info!("Updating plugin {} from source: {}", plugin_id, metadata.source);
277
278 if self.loader.get_plugin(plugin_id).await.is_some() {
280 self.loader.unload_plugin(plugin_id).await?;
281 }
282
283 let options = InstallOptions {
285 force: true,
286 skip_validation: false,
287 verify_signature: true,
288 expected_checksum: None,
289 };
290
291 let new_plugin_id = self.install_from_source(&metadata.source, options).await?;
292
293 if new_plugin_id != *plugin_id {
295 return Err(PluginLoaderError::load(format!(
296 "Plugin ID mismatch after update: expected {}, got {}",
297 plugin_id, new_plugin_id
298 )));
299 }
300
301 let new_manifest = self
303 .loader
304 .get_plugin(&new_plugin_id)
305 .await
306 .ok_or_else(|| PluginLoaderError::load("Failed to get updated plugin"))?
307 .manifest;
308
309 let mut store = self.metadata_store.write().await;
310 if let Some(meta) = store.get(plugin_id).cloned() {
311 let mut updated_meta = meta;
312 updated_meta.mark_updated(new_manifest.info.version.to_string());
313 store.save(updated_meta).await?;
314 }
315
316 tracing::info!("Plugin {} updated successfully", plugin_id);
317 Ok(())
318 }
319
320 pub async fn update_all(&self) -> LoaderResult<Vec<PluginId>> {
322 tracing::info!("Updating all plugins");
323
324 let plugin_ids = {
326 let store = self.metadata_store.read().await;
327 store.list()
328 };
329
330 if plugin_ids.is_empty() {
331 tracing::info!("No plugins found with metadata to update");
332 return Ok(Vec::new());
333 }
334
335 tracing::info!("Found {} plugins to update", plugin_ids.len());
336
337 let mut updated = Vec::new();
338 let mut failed = Vec::new();
339
340 for plugin_id in plugin_ids {
342 match self.update(&plugin_id).await {
343 Ok(_) => {
344 tracing::info!("Successfully updated plugin: {}", plugin_id);
345 updated.push(plugin_id);
346 }
347 Err(e) => {
348 tracing::warn!("Failed to update plugin {}: {}", plugin_id, e);
349 failed.push((plugin_id, e.to_string()));
350 }
351 }
352 }
353
354 tracing::info!(
355 "Plugin update complete: {} succeeded, {} failed",
356 updated.len(),
357 failed.len()
358 );
359
360 if !failed.is_empty() {
361 let failed_list = failed
362 .iter()
363 .map(|(id, err)| format!("{}: {}", id, err))
364 .collect::<Vec<_>>()
365 .join(", ");
366 tracing::warn!("Failed updates: {}", failed_list);
367 }
368
369 Ok(updated)
370 }
371
372 pub async fn clear_caches(&self) -> LoaderResult<()> {
374 self.remote_loader.clear_cache().await?;
375 self.git_loader.clear_cache().await?;
376 Ok(())
377 }
378
379 pub async fn get_cache_stats(&self) -> LoaderResult<CacheStats> {
381 let download_cache_size = self.remote_loader.get_cache_size()?;
382 let git_cache_size = self.git_loader.get_cache_size()?;
383
384 Ok(CacheStats {
385 download_cache_size,
386 git_cache_size,
387 total_size: download_cache_size + git_cache_size,
388 })
389 }
390
391 pub async fn get_plugin_metadata(&self, plugin_id: &PluginId) -> Option<PluginMetadata> {
393 let store = self.metadata_store.read().await;
394 store.get(plugin_id).cloned()
395 }
396
397 pub async fn list_plugins_with_metadata(&self) -> Vec<(PluginId, PluginMetadata)> {
399 let store = self.metadata_store.read().await;
400 store
401 .list()
402 .into_iter()
403 .filter_map(|id| store.get(&id).map(|meta| (id, meta.clone())))
404 .collect()
405 }
406}
407
408#[derive(Debug, Clone)]
410pub struct CacheStats {
411 pub download_cache_size: u64,
413 pub git_cache_size: u64,
415 pub total_size: u64,
417}
418
419impl CacheStats {
420 pub fn format_size(bytes: u64) -> String {
422 const KB: u64 = 1024;
423 const MB: u64 = KB * 1024;
424 const GB: u64 = MB * 1024;
425
426 if bytes >= GB {
427 format!("{:.2} GB", bytes as f64 / GB as f64)
428 } else if bytes >= MB {
429 format!("{:.2} MB", bytes as f64 / MB as f64)
430 } else if bytes >= KB {
431 format!("{:.2} KB", bytes as f64 / KB as f64)
432 } else {
433 format!("{} bytes", bytes)
434 }
435 }
436
437 pub fn download_cache_formatted(&self) -> String {
439 Self::format_size(self.download_cache_size)
440 }
441
442 pub fn git_cache_formatted(&self) -> String {
444 Self::format_size(self.git_cache_size)
445 }
446
447 pub fn total_formatted(&self) -> String {
449 Self::format_size(self.total_size)
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 #[test]
458 fn test_plugin_source_parse_url() {
459 let source = PluginSource::parse("https://example.com/plugin.zip").unwrap();
460 assert!(matches!(source, PluginSource::Url { .. }));
461 }
462
463 #[test]
464 fn test_plugin_source_parse_git_https() {
465 let source = PluginSource::parse("https://github.com/user/repo").unwrap();
466 assert!(matches!(source, PluginSource::Git(_)));
467 }
468
469 #[test]
470 fn test_plugin_source_parse_git_ssh() {
471 let source = PluginSource::parse("git@github.com:user/repo.git").unwrap();
472 assert!(matches!(source, PluginSource::Git(_)));
473 }
474
475 #[test]
476 fn test_plugin_source_parse_local() {
477 let source = PluginSource::parse("/path/to/plugin").unwrap();
478 assert!(matches!(source, PluginSource::Local(_)));
479
480 let source = PluginSource::parse("./relative/path").unwrap();
481 assert!(matches!(source, PluginSource::Local(_)));
482 }
483
484 #[test]
485 fn test_plugin_source_parse_registry() {
486 let source = PluginSource::parse("auth-jwt").unwrap();
487 assert!(matches!(source, PluginSource::Registry { .. }));
488
489 let source = PluginSource::parse("auth-jwt@1.0.0").unwrap();
490 if let PluginSource::Registry { name, version } = source {
491 assert_eq!(name, "auth-jwt");
492 assert_eq!(version, Some("1.0.0".to_string()));
493 } else {
494 panic!("Expected Registry source");
495 }
496 }
497
498 #[test]
499 fn test_cache_stats_formatting() {
500 assert_eq!(CacheStats::format_size(512), "512 bytes");
501 assert_eq!(CacheStats::format_size(1024), "1.00 KB");
502 assert_eq!(CacheStats::format_size(1024 * 1024), "1.00 MB");
503 assert_eq!(CacheStats::format_size(1024 * 1024 * 1024), "1.00 GB");
504 }
505}