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