1use anyhow::{anyhow, Result};
13use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::path::PathBuf;
17use std::sync::Arc;
18use tokio::sync::RwLock;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct PluginManifest {
27 pub id: String,
29 pub name: String,
31 pub version: String,
33 pub description: Option<String>,
35 pub author: Option<PluginAuthor>,
37 pub homepage: Option<String>,
39 pub repository: Option<String>,
41 pub license: Option<String>,
43 pub csm_version: String,
45 pub main: String,
47 pub permissions: Vec<Permission>,
49 pub hooks: Vec<String>,
51 pub config_schema: Option<serde_json::Value>,
53 pub dependencies: Vec<PluginDependency>,
55 pub category: PluginCategory,
57 pub keywords: Vec<String>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct PluginAuthor {
64 pub name: String,
65 pub email: Option<String>,
66 pub url: Option<String>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct PluginDependency {
72 pub id: String,
74 pub version: String,
76 pub optional: bool,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "snake_case")]
83pub enum PluginCategory {
84 Provider,
86 Export,
88 Analysis,
90 Ui,
92 Automation,
94 Storage,
96 Auth,
98 Other,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
104#[serde(rename_all = "snake_case")]
105pub enum Permission {
106 SessionRead,
108 SessionWrite,
110 SessionDelete,
112 ConfigRead,
114 ConfigWrite,
116 Network,
118 FileSystem,
120 Shell,
122 Sensitive,
124 Background,
126 Notifications,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(rename_all = "snake_case")]
137pub enum PluginState {
138 Loaded,
140 Active,
142 Disabled,
144 Error,
146 Updating,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct PluginInstance {
153 pub manifest: PluginManifest,
155 pub state: PluginState,
157 pub path: PathBuf,
159 pub installed_at: DateTime<Utc>,
161 pub last_activated: Option<DateTime<Utc>>,
163 pub config: serde_json::Value,
165 pub error: Option<String>,
167 pub stats: PluginStats,
169}
170
171#[derive(Debug, Clone, Default, Serialize, Deserialize)]
173pub struct PluginStats {
174 pub activation_count: u64,
176 pub total_execution_ms: u64,
178 pub error_count: u64,
180 pub last_error: Option<DateTime<Utc>>,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
190pub enum PluginEvent {
191 SessionCreated { session_id: String },
193 SessionUpdated { session_id: String },
195 SessionDeleted { session_id: String },
197 SessionImported { session_id: String, provider: String },
199 SessionExported { session_id: String, format: String },
201 HarvestCompleted { session_count: usize, provider: String },
203 SyncCompleted { direction: String, changes: usize },
205 UserAction { action: String, context: serde_json::Value },
207 AppStartup,
209 AppShutdown,
211 ConfigChanged { key: String },
213 Custom { name: String, data: serde_json::Value },
215}
216
217#[derive(Debug, Clone)]
219pub struct HookRegistration {
220 pub plugin_id: String,
222 pub event_pattern: String,
224 pub priority: i32,
226 pub handler_id: String,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct HookResult {
233 pub plugin_id: String,
235 pub success: bool,
237 pub data: Option<serde_json::Value>,
239 pub error: Option<String>,
241 pub execution_ms: u64,
243}
244
245pub struct PluginContext {
251 pub plugin_id: String,
253 pub permissions: Vec<Permission>,
255 pub data_dir: PathBuf,
257 pub config: serde_json::Value,
259}
260
261impl PluginContext {
262 pub fn has_permission(&self, permission: &Permission) -> bool {
264 self.permissions.contains(permission)
265 }
266
267 pub fn get_data_path(&self, filename: &str) -> PathBuf {
269 self.data_dir.join(filename)
270 }
271
272 pub fn read_data(&self, filename: &str) -> Result<String> {
274 if !self.has_permission(&Permission::FileSystem) {
275 return Err(anyhow!("Permission denied: FileSystem"));
276 }
277 let path = self.get_data_path(filename);
278 Ok(std::fs::read_to_string(path)?)
279 }
280
281 pub fn write_data(&self, filename: &str, content: &str) -> Result<()> {
283 if !self.has_permission(&Permission::FileSystem) {
284 return Err(anyhow!("Permission denied: FileSystem"));
285 }
286 std::fs::create_dir_all(&self.data_dir)?;
287 let path = self.get_data_path(filename);
288 Ok(std::fs::write(path, content)?)
289 }
290}
291
292pub struct PluginManager {
298 plugins_dir: PathBuf,
300 plugins: Arc<RwLock<HashMap<String, PluginInstance>>>,
302 hooks: Arc<RwLock<Vec<HookRegistration>>>,
304 configs: Arc<RwLock<HashMap<String, serde_json::Value>>>,
306}
307
308impl PluginManager {
309 pub fn new(plugins_dir: PathBuf) -> Self {
311 Self {
312 plugins_dir,
313 plugins: Arc::new(RwLock::new(HashMap::new())),
314 hooks: Arc::new(RwLock::new(Vec::new())),
315 configs: Arc::new(RwLock::new(HashMap::new())),
316 }
317 }
318
319 pub async fn init(&self) -> Result<()> {
321 std::fs::create_dir_all(&self.plugins_dir)?;
322 self.discover_plugins().await?;
323 Ok(())
324 }
325
326 pub async fn discover_plugins(&self) -> Result<Vec<String>> {
328 let mut discovered = Vec::new();
329
330 if !self.plugins_dir.exists() {
331 return Ok(discovered);
332 }
333
334 for entry in std::fs::read_dir(&self.plugins_dir)? {
335 let entry = entry?;
336 let path = entry.path();
337
338 if path.is_dir() {
339 let manifest_path = path.join("plugin.json");
340 if manifest_path.exists() {
341 match self.load_plugin(&path).await {
342 Ok(plugin_id) => discovered.push(plugin_id),
343 Err(e) => {
344 log::warn!("Failed to load plugin at {:?}: {}", path, e);
345 }
346 }
347 }
348 }
349 }
350
351 Ok(discovered)
352 }
353
354 pub async fn load_plugin(&self, plugin_path: &PathBuf) -> Result<String> {
356 let manifest_path = plugin_path.join("plugin.json");
357 let manifest_content = std::fs::read_to_string(&manifest_path)?;
358 let manifest: PluginManifest = serde_json::from_str(&manifest_content)?;
359
360 self.validate_manifest(&manifest)?;
362
363 self.check_dependencies(&manifest).await?;
365
366 let instance = PluginInstance {
367 manifest: manifest.clone(),
368 state: PluginState::Loaded,
369 path: plugin_path.clone(),
370 installed_at: Utc::now(),
371 last_activated: None,
372 config: serde_json::Value::Object(serde_json::Map::new()),
373 error: None,
374 stats: PluginStats::default(),
375 };
376
377 let plugin_id = manifest.id.clone();
378 self.plugins.write().await.insert(plugin_id.clone(), instance);
379
380 log::info!("Loaded plugin: {} v{}", manifest.name, manifest.version);
381 Ok(plugin_id)
382 }
383
384 fn validate_manifest(&self, manifest: &PluginManifest) -> Result<()> {
386 if manifest.id.is_empty() || manifest.id.len() > 64 {
388 return Err(anyhow!("Invalid plugin ID"));
389 }
390
391 if semver::Version::parse(&manifest.version).is_err() {
393 return Err(anyhow!("Invalid version format: {}", manifest.version));
394 }
395
396 let current_version = env!("CARGO_PKG_VERSION");
398 let req = semver::VersionReq::parse(&manifest.csm_version)
399 .map_err(|_| anyhow!("Invalid csm_version: {}", manifest.csm_version))?;
400 let current = semver::Version::parse(current_version)?;
401
402 if !req.matches(¤t) {
403 return Err(anyhow!(
404 "Plugin requires CSM {}, but current version is {}",
405 manifest.csm_version,
406 current_version
407 ));
408 }
409
410 Ok(())
411 }
412
413 async fn check_dependencies(&self, manifest: &PluginManifest) -> Result<()> {
415 let plugins = self.plugins.read().await;
416
417 for dep in &manifest.dependencies {
418 if dep.optional {
419 continue;
420 }
421
422 let plugin = plugins.get(&dep.id);
423 match plugin {
424 None => {
425 return Err(anyhow!("Missing required dependency: {}", dep.id));
426 }
427 Some(p) => {
428 let req = semver::VersionReq::parse(&dep.version)?;
429 let ver = semver::Version::parse(&p.manifest.version)?;
430 if !req.matches(&ver) {
431 return Err(anyhow!(
432 "Dependency {} version {} does not match requirement {}",
433 dep.id, p.manifest.version, dep.version
434 ));
435 }
436 }
437 }
438 }
439
440 Ok(())
441 }
442
443 pub async fn activate(&self, plugin_id: &str) -> Result<()> {
445 let mut plugins = self.plugins.write().await;
446 let plugin = plugins.get_mut(plugin_id)
447 .ok_or_else(|| anyhow!("Plugin not found: {}", plugin_id))?;
448
449 if plugin.state == PluginState::Active {
450 return Ok(());
451 }
452
453 for hook in &plugin.manifest.hooks {
455 self.register_hook(plugin_id, hook, 0).await?;
456 }
457
458 plugin.state = PluginState::Active;
459 plugin.last_activated = Some(Utc::now());
460 plugin.stats.activation_count += 1;
461
462 log::info!("Activated plugin: {}", plugin_id);
463 Ok(())
464 }
465
466 pub async fn deactivate(&self, plugin_id: &str) -> Result<()> {
468 let mut plugins = self.plugins.write().await;
469 let plugin = plugins.get_mut(plugin_id)
470 .ok_or_else(|| anyhow!("Plugin not found: {}", plugin_id))?;
471
472 self.unregister_hooks(plugin_id).await?;
474
475 plugin.state = PluginState::Disabled;
476
477 log::info!("Deactivated plugin: {}", plugin_id);
478 Ok(())
479 }
480
481 pub async fn uninstall(&self, plugin_id: &str) -> Result<()> {
483 self.deactivate(plugin_id).await.ok();
485
486 let mut plugins = self.plugins.write().await;
487 let plugin = plugins.remove(plugin_id)
488 .ok_or_else(|| anyhow!("Plugin not found: {}", plugin_id))?;
489
490 if plugin.path.exists() {
492 std::fs::remove_dir_all(&plugin.path)?;
493 }
494
495 log::info!("Uninstalled plugin: {}", plugin_id);
496 Ok(())
497 }
498
499 async fn register_hook(&self, plugin_id: &str, event_pattern: &str, priority: i32) -> Result<()> {
501 let mut hooks = self.hooks.write().await;
502 hooks.push(HookRegistration {
503 plugin_id: plugin_id.to_string(),
504 event_pattern: event_pattern.to_string(),
505 priority,
506 handler_id: uuid::Uuid::new_v4().to_string(),
507 });
508 Ok(())
509 }
510
511 async fn unregister_hooks(&self, plugin_id: &str) -> Result<()> {
513 let mut hooks = self.hooks.write().await;
514 hooks.retain(|h| h.plugin_id != plugin_id);
515 Ok(())
516 }
517
518 pub async fn emit(&self, event: PluginEvent) -> Vec<HookResult> {
520 let hooks = self.hooks.read().await;
521 let plugins = self.plugins.read().await;
522 let mut results = Vec::new();
523
524 let event_name = self.get_event_name(&event);
525
526 let mut matching_hooks: Vec<_> = hooks.iter()
528 .filter(|h| self.matches_pattern(&h.event_pattern, &event_name))
529 .collect();
530 matching_hooks.sort_by_key(|h| h.priority);
531
532 for hook in matching_hooks {
533 let plugin = match plugins.get(&hook.plugin_id) {
534 Some(p) if p.state == PluginState::Active => p,
535 _ => continue,
536 };
537
538 let start = std::time::Instant::now();
539
540 let result = HookResult {
543 plugin_id: hook.plugin_id.clone(),
544 success: true,
545 data: Some(serde_json::json!({
546 "event": event_name,
547 "handled": true
548 })),
549 error: None,
550 execution_ms: start.elapsed().as_millis() as u64,
551 };
552
553 results.push(result);
554 }
555
556 results
557 }
558
559 fn get_event_name(&self, event: &PluginEvent) -> String {
560 match event {
561 PluginEvent::SessionCreated { .. } => "session.created".to_string(),
562 PluginEvent::SessionUpdated { .. } => "session.updated".to_string(),
563 PluginEvent::SessionDeleted { .. } => "session.deleted".to_string(),
564 PluginEvent::SessionImported { .. } => "session.imported".to_string(),
565 PluginEvent::SessionExported { .. } => "session.exported".to_string(),
566 PluginEvent::HarvestCompleted { .. } => "harvest.completed".to_string(),
567 PluginEvent::SyncCompleted { .. } => "sync.completed".to_string(),
568 PluginEvent::UserAction { action, .. } => format!("user.{}", action),
569 PluginEvent::AppStartup => "app.startup".to_string(),
570 PluginEvent::AppShutdown => "app.shutdown".to_string(),
571 PluginEvent::ConfigChanged { .. } => "config.changed".to_string(),
572 PluginEvent::Custom { name, .. } => format!("custom.{}", name),
573 }
574 }
575
576 fn matches_pattern(&self, pattern: &str, event_name: &str) -> bool {
577 if pattern == "*" {
578 return true;
579 }
580 if pattern.ends_with("*") {
581 let prefix = &pattern[..pattern.len() - 1];
582 return event_name.starts_with(prefix);
583 }
584 pattern == event_name
585 }
586
587 pub async fn get_plugin(&self, plugin_id: &str) -> Option<PluginInstance> {
589 self.plugins.read().await.get(plugin_id).cloned()
590 }
591
592 pub async fn list_plugins(&self) -> Vec<PluginInstance> {
594 self.plugins.read().await.values().cloned().collect()
595 }
596
597 pub async fn get_config(&self, plugin_id: &str) -> Option<serde_json::Value> {
599 self.plugins.read().await
600 .get(plugin_id)
601 .map(|p| p.config.clone())
602 }
603
604 pub async fn set_config(&self, plugin_id: &str, config: serde_json::Value) -> Result<()> {
606 let mut plugins = self.plugins.write().await;
607 let plugin = plugins.get_mut(plugin_id)
608 .ok_or_else(|| anyhow!("Plugin not found: {}", plugin_id))?;
609
610 if let Some(schema) = &plugin.manifest.config_schema {
612 self.validate_config(&config, schema)?;
613 }
614
615 plugin.config = config;
616 Ok(())
617 }
618
619 fn validate_config(&self, _config: &serde_json::Value, _schema: &serde_json::Value) -> Result<()> {
620 Ok(())
622 }
623
624 pub async fn create_context(&self, plugin_id: &str) -> Result<PluginContext> {
626 let plugins = self.plugins.read().await;
627 let plugin = plugins.get(plugin_id)
628 .ok_or_else(|| anyhow!("Plugin not found: {}", plugin_id))?;
629
630 Ok(PluginContext {
631 plugin_id: plugin_id.to_string(),
632 permissions: plugin.manifest.permissions.clone(),
633 data_dir: plugin.path.join("data"),
634 config: plugin.config.clone(),
635 })
636 }
637}
638
639#[derive(Debug, Clone, Serialize, Deserialize)]
645pub struct RegistryEntry {
646 pub id: String,
648 pub version: String,
650 pub name: String,
652 pub description: String,
654 pub author: String,
656 pub download_url: String,
658 pub downloads: u64,
660 pub rating: f32,
662 pub category: PluginCategory,
664 pub keywords: Vec<String>,
666 pub updated_at: DateTime<Utc>,
668}
669
670pub struct PluginRegistry {
672 registry_url: String,
674}
675
676impl PluginRegistry {
677 pub fn new(registry_url: String) -> Self {
678 Self { registry_url }
679 }
680
681 pub async fn search(&self, query: &str, category: Option<PluginCategory>) -> Result<Vec<RegistryEntry>> {
683 Ok(Vec::new())
686 }
687
688 pub async fn get_plugin(&self, plugin_id: &str) -> Result<RegistryEntry> {
690 Err(anyhow!("Plugin not found in registry: {}", plugin_id))
691 }
692
693 pub async fn install(&self, plugin_id: &str, manager: &PluginManager) -> Result<()> {
695 let entry = self.get_plugin(plugin_id).await?;
696
697 let plugin_dir = manager.plugins_dir.join(plugin_id);
699 std::fs::create_dir_all(&plugin_dir)?;
700
701 manager.load_plugin(&plugin_dir).await?;
705 Ok(())
706 }
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712 use tempfile::tempdir;
713
714 #[tokio::test]
715 async fn test_plugin_manager_init() {
716 let temp_dir = tempdir().unwrap();
717 let manager = PluginManager::new(temp_dir.path().to_path_buf());
718
719 assert!(manager.init().await.is_ok());
720 }
721
722 #[test]
723 fn test_event_name() {
724 let manager = PluginManager::new(PathBuf::from("."));
725
726 let event = PluginEvent::SessionCreated { session_id: "test".to_string() };
727 assert_eq!(manager.get_event_name(&event), "session.created");
728 }
729
730 #[test]
731 fn test_pattern_matching() {
732 let manager = PluginManager::new(PathBuf::from("."));
733
734 assert!(manager.matches_pattern("*", "session.created"));
735 assert!(manager.matches_pattern("session.*", "session.created"));
736 assert!(manager.matches_pattern("session.created", "session.created"));
737 assert!(!manager.matches_pattern("session.updated", "session.created"));
738 }
739}