Skip to main content

Grove/Host/
ExtensionManager.rs

1//! Extension Manager Module
2//!
3//! Handles extension discovery, loading, and management.
4//! Provides query and monitoring capabilities for extensions.
5
6use std::{
7	collections::HashMap,
8	path::{Path, PathBuf},
9	sync::Arc,
10};
11
12use anyhow::{Context, Result};
13use serde::{Deserialize, Serialize};
14use tokio::sync::RwLock;
15
16use crate::{Host::HostConfig, WASM::Runtime::WASMRuntime, dev_log};
17
18/// Extension manager for handling extension lifecycle
19pub struct ExtensionManagerImpl {
20	/// WASM runtime for executing extensions
21	#[allow(dead_code)]
22	wasm_runtime:Arc<WASMRuntime>,
23	/// Host configuration
24	config:HostConfig,
25	/// Loaded extensions
26	extensions:Arc<RwLock<HashMap<String, ExtensionInfo>>>,
27	/// Extension statistics
28	stats:Arc<RwLock<ExtensionStats>>,
29}
30
31/// Extension information
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct ExtensionInfo {
34	/// Extension ID (e.g., "publisher.extension-name")
35	pub id:String,
36	/// Extension display name
37	pub display_name:String,
38	/// Extension description
39	pub description:String,
40	/// Extension version
41	pub version:String,
42	/// Publisher name
43	pub publisher:String,
44	/// Path to extension directory
45	pub path:PathBuf,
46	/// Entry point file
47	pub entry_point:PathBuf,
48	/// Activation events
49	pub activation_events:Vec<String>,
50	/// Type of extension (wasm, native, etc.)
51	pub extension_type:ExtensionType,
52	/// Extension state
53	pub state:ExtensionState,
54	/// Extension capabilities
55	pub capabilities:Vec<String>,
56	/// Dependencies
57	pub dependencies:Vec<String>,
58	/// Extension manifest (JSON)
59	pub manifest:serde_json::Value,
60	/// Load timestamp
61	pub loaded_at:u64,
62	/// Activation timestamp
63	pub activated_at:Option<u64>,
64}
65
66/// Extension type
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
68pub enum ExtensionType {
69	/// WebAssembly extension
70	WASM,
71	/// Native Rust extension
72	Native,
73	/// JavaScript/TypeScript extension (via Cocoon compatibility)
74	JavaScript,
75	/// Unknown type
76	Unknown,
77}
78
79/// Extension state
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
81pub enum ExtensionState {
82	/// Extension is loaded but not activated
83	Loaded,
84	/// Extension is activated and running
85	Activated,
86	/// Extension is deactivated
87	Deactivated,
88	/// Extension encountered an error
89	Error,
90}
91
92/// Extension statistics
93#[derive(Debug, Clone, Default, Serialize, Deserialize)]
94pub struct ExtensionStats {
95	/// Total number of extensions loaded
96	pub total_loaded:usize,
97	/// Total number of extensions activated
98	pub total_activated:usize,
99	/// Total number of extensions deactivated
100	pub total_deactivated:usize,
101	/// Total activation time in milliseconds
102	pub total_activation_time_ms:u64,
103	/// Number of errors encountered
104	pub errors:u64,
105}
106
107impl ExtensionManagerImpl {
108	/// Create a new extension manager
109	pub fn new(wasm_runtime:Arc<WASMRuntime>, config:HostConfig) -> Self {
110		Self {
111			wasm_runtime,
112			config,
113			extensions:Arc::new(RwLock::new(HashMap::new())),
114			stats:Arc::new(RwLock::new(ExtensionStats::default())),
115		}
116	}
117
118	/// Load an extension from a path
119	pub async fn load_extension(&self, path:&PathBuf) -> Result<String> {
120		dev_log!("extensions", "Loading extension from: {:?}", path);
121
122		// Validate path
123		if !path.exists() {
124			return Err(anyhow::anyhow!("Extension path does not exist: {:?}", path));
125		}
126
127		// Parse manifest
128		let manifest = self.parse_manifest(path)?;
129		let extension_id = self.extract_extension_id(&manifest)?;
130
131		// Check if extension is already loaded
132		let extensions = self.extensions.read().await;
133		if extensions.contains_key(&extension_id) {
134			dev_log!("extensions", "warn: extension already loaded: {}", extension_id);
135			return Ok(extension_id);
136		}
137		drop(extensions);
138
139		// Determine extension type
140		let extension_type = self.determine_extension_type(path, &manifest)?;
141
142		// Create extension info
143		let extension_info = ExtensionInfo {
144			id:extension_id.clone(),
145			display_name:manifest.get("displayName").and_then(|v| v.as_str()).unwrap_or("").to_string(),
146			description:manifest.get("description").and_then(|v| v.as_str()).unwrap_or("").to_string(),
147			version:manifest.get("version").and_then(|v| v.as_str()).unwrap_or("0.0.0").to_string(),
148			publisher:manifest.get("publisher").and_then(|v| v.as_str()).unwrap_or("").to_string(),
149			path:path.clone(),
150			entry_point:path.join(manifest.get("main").and_then(|v| v.as_str()).unwrap_or("dist/extension.js")),
151			activation_events:self.extract_activation_events(&manifest),
152			extension_type,
153			state:ExtensionState::Loaded,
154			capabilities:self.extract_capabilities(&manifest),
155			dependencies:self.extract_dependencies(&manifest),
156			manifest,
157			loaded_at:std::time::SystemTime::now()
158				.duration_since(std::time::UNIX_EPOCH)
159				.map(|d| d.as_secs())
160				.unwrap_or(0),
161			activated_at:None,
162		};
163
164		// Register extension
165		let mut extensions = self.extensions.write().await;
166		extensions.insert(extension_id.clone(), extension_info);
167
168		// Update statistics
169		let mut stats = self.stats.write().await;
170		stats.total_loaded += 1;
171
172		dev_log!("extensions", "Extension loaded successfully: {}", extension_id);
173
174		Ok(extension_id)
175	}
176
177	/// Unload an extension
178	pub async fn unload_extension(&self, extension_id:&str) -> Result<()> {
179		dev_log!("extensions", "Unloading extension: {}", extension_id);
180
181		let mut extensions = self.extensions.write().await;
182		extensions.remove(extension_id);
183
184		dev_log!("extensions", "Extension unloaded: {}", extension_id);
185
186		Ok(())
187	}
188
189	/// Get an extension by ID
190	pub async fn get_extension(&self, extension_id:&str) -> Option<ExtensionInfo> {
191		self.extensions.read().await.get(extension_id).cloned()
192	}
193
194	/// List all loaded extensions
195	pub async fn list_extensions(&self) -> Vec<String> { self.extensions.read().await.keys().cloned().collect() }
196
197	/// List extensions in a specific state
198	pub async fn list_extensions_by_state(&self, state:ExtensionState) -> Vec<ExtensionInfo> {
199		self.extensions
200			.read()
201			.await
202			.values()
203			.filter(|ext| ext.state == state)
204			.cloned()
205			.collect()
206	}
207
208	/// Update extension state
209	pub async fn update_state(&self, extension_id:&str, state:ExtensionState) -> Result<()> {
210		let mut extensions = self.extensions.write().await;
211		if let Some(info) = extensions.get_mut(extension_id) {
212			info.state = state;
213			if state == ExtensionState::Activated {
214				info.activated_at = Some(
215					std::time::SystemTime::now()
216						.duration_since(std::time::UNIX_EPOCH)
217						.map(|d| d.as_secs())
218						.unwrap_or(0),
219				);
220
221				let mut stats = self.stats.write().await;
222				stats.total_activated += 1;
223			} else if state == ExtensionState::Deactivated {
224				let mut stats = self.stats.write().await;
225				stats.total_deactivated += 1;
226			}
227			Ok(())
228		} else {
229			Err(anyhow::anyhow!("Extension not found: {}", extension_id))
230		}
231	}
232
233	/// Get extension manager statistics
234	pub async fn stats(&self) -> ExtensionStats { self.stats.read().await.clone() }
235
236	/// Discover extensions in configured paths
237	pub async fn discover_extensions(&self) -> Result<Vec<PathBuf>> {
238		dev_log!("extensions", "Discovering extensions in configured paths");
239
240		let mut extensions = Vec::new();
241
242		for discovery_path in &self.config.discovery_paths {
243			match self.discover_in_path(discovery_path).await {
244				Ok(mut found) => extensions.append(&mut found),
245				Err(e) => {
246					dev_log!("extensions", "warn: failed to discover extensions in {}: {}", discovery_path, e);
247				},
248			}
249		}
250
251		dev_log!("extensions", "Discovered {} extensions", extensions.len());
252
253		Ok(extensions)
254	}
255
256	/// Discover extensions in a specific path
257	async fn discover_in_path(&self, path:&str) -> Result<Vec<PathBuf>> {
258		let path = PathBuf::from(shellexpand::tilde(path).as_ref());
259
260		if !path.exists() {
261			return Ok(Vec::new());
262		}
263
264		let mut extensions = Vec::new();
265
266		// Read directory entries
267		let mut entries = tokio::fs::read_dir(&path)
268			.await
269			.context(format!("Failed to read directory: {:?}", path))?;
270
271		while let Some(entry) = entries.next_entry().await? {
272			let entry_path = entry.path();
273
274			// Skip if not a directory
275			if !entry_path.is_dir() {
276				continue;
277			}
278
279			// Check for package.json or manifest.json
280			let manifest_path = entry_path.join("package.json");
281			let alt_manifest_path = entry_path.join("manifest.json");
282
283			if manifest_path.exists() || alt_manifest_path.exists() {
284				extensions.push(entry_path.clone());
285				dev_log!("extensions", "Discovered extension: {:?}", entry_path);
286			}
287		}
288
289		Ok(extensions)
290	}
291
292	/// Parse extension manifest
293	fn parse_manifest(&self, path:&Path) -> Result<serde_json::Value> {
294		let manifest_path = path.join("package.json");
295		let alt_manifest_path = path.join("manifest.json");
296
297		let manifest_content = if manifest_path.exists() {
298			tokio::runtime::Runtime::new()
299				.unwrap()
300				.block_on(tokio::fs::read_to_string(&manifest_path))
301				.context("Failed to read package.json")?
302		} else if alt_manifest_path.exists() {
303			tokio::runtime::Runtime::new()
304				.unwrap()
305				.block_on(tokio::fs::read_to_string(&alt_manifest_path))
306				.context("Failed to read manifest.json")?
307		} else {
308			return Err(anyhow::anyhow!("No manifest found in extension path"));
309		};
310
311		let manifest:serde_json::Value = serde_json::from_str(&manifest_content).context("Failed to parse manifest")?;
312
313		Ok(manifest)
314	}
315
316	/// Extract extension ID from manifest
317	fn extract_extension_id(&self, manifest:&serde_json::Value) -> Result<String> {
318		let publisher = manifest
319			.get("publisher")
320			.and_then(|v| v.as_str())
321			.ok_or_else(|| anyhow::anyhow!("Missing publisher in manifest"))?;
322
323		let name = manifest
324			.get("name")
325			.and_then(|v| v.as_str())
326			.ok_or_else(|| anyhow::anyhow!("Missing name in manifest"))?;
327
328		Ok(format!("{}.{}", publisher, name))
329	}
330
331	/// Determine extension type
332	fn determine_extension_type(&self, path:&Path, manifest:&serde_json::Value) -> Result<ExtensionType> {
333		// Check for WASM file
334		let wasm_path = path.join("extension.wasm");
335		if wasm_path.exists() {
336			return Ok(ExtensionType::WASM);
337		}
338
339		// Check for Rust project
340		let cargo_path = path.join("Cargo.toml");
341		if cargo_path.exists() {
342			return Ok(ExtensionType::Native);
343		}
344
345		// Check for JavaScript/TypeScript
346		let main = manifest.get("main").and_then(|v| v.as_str());
347		if let Some(main) = main {
348			let main_path = path.join(main);
349			if main_path.exists() && (main.ends_with(".js") || main.ends_with(".ts")) {
350				return Ok(ExtensionType::JavaScript);
351			}
352		}
353
354		Ok(ExtensionType::Unknown)
355	}
356
357	/// Extract activation events from manifest
358	fn extract_activation_events(&self, manifest:&serde_json::Value) -> Vec<String> {
359		manifest
360			.get("activationEvents")
361			.and_then(|v| v.as_array())
362			.map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
363			.unwrap_or_default()
364	}
365
366	/// Extract capabilities from manifest
367	fn extract_capabilities(&self, manifest:&serde_json::Value) -> Vec<String> {
368		manifest
369			.get("capabilities")
370			.and_then(|v| v.as_object())
371			.map(|obj| obj.keys().cloned().collect())
372			.unwrap_or_default()
373	}
374
375	/// Extract dependencies from manifest
376	fn extract_dependencies(&self, manifest:&serde_json::Value) -> Vec<String> {
377		manifest
378			.get("extensionDependencies")
379			.and_then(|v| v.as_array())
380			.map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
381			.unwrap_or_default()
382	}
383}
384
385#[cfg(test)]
386mod tests {
387	use super::*;
388
389	#[test]
390	fn test_extension_type() {
391		assert_eq!(ExtensionType::WASM, ExtensionType::WASM);
392		assert_eq!(ExtensionType::Native, ExtensionType::Native);
393		assert_eq!(ExtensionType::JavaScript, ExtensionType::JavaScript);
394	}
395
396	#[test]
397	fn test_extension_state() {
398		assert_eq!(ExtensionState::Loaded, ExtensionState::Loaded);
399		assert_eq!(ExtensionState::Activated, ExtensionState::Activated);
400		assert_eq!(ExtensionState::Deactivated, ExtensionState::Deactivated);
401		assert_eq!(ExtensionState::Error, ExtensionState::Error);
402	}
403
404	#[tokio::test]
405	async fn test_extension_manager_creation() {
406		let wasm_runtime = Arc::new(
407			tokio::runtime::Runtime::new()
408				.unwrap()
409				.block_on(crate::WASM::Runtime::WASMRuntime::new(
410					crate::WASM::Runtime::WASMConfig::default(),
411				))
412				.unwrap(),
413		);
414		let config = HostConfig::default();
415		let manager = ExtensionManagerImpl::new(wasm_runtime, config);
416
417		assert_eq!(manager.list_extensions().await.len(), 0);
418	}
419
420	#[test]
421	fn test_extension_stats_default() {
422		let stats = ExtensionStats::default();
423		assert_eq!(stats.total_loaded, 0);
424		assert_eq!(stats.total_activated, 0);
425	}
426}