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 let Some(prefix) = pattern.strip_suffix("*") {
581 return event_name.starts_with(prefix);
582 }
583 pattern == event_name
584 }
585
586 pub async fn get_plugin(&self, plugin_id: &str) -> Option<PluginInstance> {
588 self.plugins.read().await.get(plugin_id).cloned()
589 }
590
591 pub async fn list_plugins(&self) -> Vec<PluginInstance> {
593 self.plugins.read().await.values().cloned().collect()
594 }
595
596 pub async fn get_config(&self, plugin_id: &str) -> Option<serde_json::Value> {
598 self.plugins.read().await
599 .get(plugin_id)
600 .map(|p| p.config.clone())
601 }
602
603 pub async fn set_config(&self, plugin_id: &str, config: serde_json::Value) -> Result<()> {
605 let mut plugins = self.plugins.write().await;
606 let plugin = plugins.get_mut(plugin_id)
607 .ok_or_else(|| anyhow!("Plugin not found: {}", plugin_id))?;
608
609 if let Some(schema) = &plugin.manifest.config_schema {
611 self.validate_config(&config, schema)?;
612 }
613
614 plugin.config = config;
615 Ok(())
616 }
617
618 fn validate_config(&self, _config: &serde_json::Value, _schema: &serde_json::Value) -> Result<()> {
619 Ok(())
621 }
622
623 pub async fn create_context(&self, plugin_id: &str) -> Result<PluginContext> {
625 let plugins = self.plugins.read().await;
626 let plugin = plugins.get(plugin_id)
627 .ok_or_else(|| anyhow!("Plugin not found: {}", plugin_id))?;
628
629 Ok(PluginContext {
630 plugin_id: plugin_id.to_string(),
631 permissions: plugin.manifest.permissions.clone(),
632 data_dir: plugin.path.join("data"),
633 config: plugin.config.clone(),
634 })
635 }
636}
637
638#[derive(Debug, Clone, Serialize, Deserialize)]
644pub struct RegistryEntry {
645 pub id: String,
647 pub version: String,
649 pub name: String,
651 pub description: String,
653 pub author: String,
655 pub download_url: String,
657 pub downloads: u64,
659 pub rating: f32,
661 pub category: PluginCategory,
663 pub keywords: Vec<String>,
665 pub updated_at: DateTime<Utc>,
667}
668
669pub struct PluginRegistry {
671 registry_url: String,
673}
674
675impl PluginRegistry {
676 pub fn new(registry_url: String) -> Self {
677 Self { registry_url }
678 }
679
680 pub async fn search(&self, _query: &str, _category: Option<PluginCategory>) -> Result<Vec<RegistryEntry>> {
682 Ok(Vec::new())
685 }
686
687 pub async fn get_plugin(&self, plugin_id: &str) -> Result<RegistryEntry> {
689 Err(anyhow!("Plugin not found in registry: {}", plugin_id))
690 }
691
692 pub async fn install(&self, plugin_id: &str, manager: &PluginManager) -> Result<()> {
694 let _entry = self.get_plugin(plugin_id).await?;
695
696 let plugin_dir = manager.plugins_dir.join(plugin_id);
698 std::fs::create_dir_all(&plugin_dir)?;
699
700 manager.load_plugin(&plugin_dir).await?;
704 Ok(())
705 }
706}
707
708#[cfg(test)]
709mod tests {
710 use super::*;
711 use tempfile::tempdir;
712
713 #[tokio::test]
714 async fn test_plugin_manager_init() {
715 let temp_dir = tempdir().unwrap();
716 let manager = PluginManager::new(temp_dir.path().to_path_buf());
717
718 assert!(manager.init().await.is_ok());
719 }
720
721 #[test]
722 fn test_event_name() {
723 let manager = PluginManager::new(PathBuf::from("."));
724
725 let event = PluginEvent::SessionCreated { session_id: "test".to_string() };
726 assert_eq!(manager.get_event_name(&event), "session.created");
727 }
728
729 #[test]
730 fn test_pattern_matching() {
731 let manager = PluginManager::new(PathBuf::from("."));
732
733 assert!(manager.matches_pattern("*", "session.created"));
734 assert!(manager.matches_pattern("session.*", "session.created"));
735 assert!(manager.matches_pattern("session.created", "session.created"));
736 assert!(!manager.matches_pattern("session.updated", "session.created"));
737 }
738}