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