1use serde::{Deserialize, Serialize};
78use std::collections::HashMap;
79use std::path::{Path, PathBuf};
80
81use crate::{DrivenError, Result};
82
83#[cfg(test)]
84mod property_tests;
85
86#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
88pub struct Module {
89 pub id: String,
91 pub name: String,
93 pub version: String,
95 pub description: String,
97 pub author: Option<String>,
99 pub license: Option<String>,
101 pub dependencies: Vec<ModuleDependency>,
103 pub agents: Vec<String>,
105 pub workflows: Vec<String>,
107 pub templates: Vec<String>,
109 pub resources: Vec<String>,
111 #[serde(skip)]
113 pub install_path: Option<PathBuf>,
114}
115
116impl Module {
117 pub fn new(id: impl Into<String>, name: impl Into<String>, version: impl Into<String>) -> Self {
119 Self {
120 id: id.into(),
121 name: name.into(),
122 version: version.into(),
123 description: String::new(),
124 author: None,
125 license: None,
126 dependencies: Vec::new(),
127 agents: Vec::new(),
128 workflows: Vec::new(),
129 templates: Vec::new(),
130 resources: Vec::new(),
131 install_path: None,
132 }
133 }
134
135 pub fn with_description(mut self, description: impl Into<String>) -> Self {
137 self.description = description.into();
138 self
139 }
140
141 pub fn with_dependency(mut self, dep: ModuleDependency) -> Self {
143 self.dependencies.push(dep);
144 self
145 }
146
147 pub fn with_agent(mut self, agent: impl Into<String>) -> Self {
149 self.agents.push(agent.into());
150 self
151 }
152
153 pub fn with_workflow(mut self, workflow: impl Into<String>) -> Self {
155 self.workflows.push(workflow.into());
156 self
157 }
158
159 pub fn with_template(mut self, template: impl Into<String>) -> Self {
161 self.templates.push(template.into());
162 self
163 }
164
165 pub fn with_resource(mut self, resource: impl Into<String>) -> Self {
167 self.resources.push(resource.into());
168 self
169 }
170
171 pub fn namespaced_agent(&self, agent: &str) -> String {
173 format!("{}:{}", self.id, agent)
174 }
175
176 pub fn namespaced_workflow(&self, workflow: &str) -> String {
178 format!("{}:{}", self.id, workflow)
179 }
180
181 pub fn namespaced_template(&self, template: &str) -> String {
183 format!("{}:{}", self.id, template)
184 }
185
186 pub fn satisfies_version(&self, version_req: &str) -> bool {
188 if version_req == "*" {
190 return true;
191 }
192 if version_req.starts_with('^') {
193 let req = version_req.trim_start_matches('^');
195 return self
196 .version
197 .starts_with(&req.split('.').next().unwrap_or("0").to_string());
198 }
199 if version_req.starts_with('~') {
200 let req = version_req.trim_start_matches('~');
202 let parts: Vec<&str> = req.split('.').collect();
203 let self_parts: Vec<&str> = self.version.split('.').collect();
204 return parts.first() == self_parts.first() && parts.get(1) == self_parts.get(1);
205 }
206 self.version == version_req
208 }
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
213pub struct ModuleDependency {
214 pub module_id: String,
216 pub version_req: String,
218 #[serde(default)]
220 pub optional: bool,
221}
222
223impl ModuleDependency {
224 pub fn new(module_id: impl Into<String>, version_req: impl Into<String>) -> Self {
226 Self {
227 module_id: module_id.into(),
228 version_req: version_req.into(),
229 optional: false,
230 }
231 }
232
233 pub fn optional(module_id: impl Into<String>, version_req: impl Into<String>) -> Self {
235 Self {
236 module_id: module_id.into(),
237 version_req: version_req.into(),
238 optional: true,
239 }
240 }
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub enum ModuleStatus {
246 Installed,
248 Installing,
250 UpdateAvailable,
252 DependencyError,
254 Disabled,
256}
257
258pub struct ModuleManager {
260 installed: HashMap<String, Module>,
262 status: HashMap<String, ModuleStatus>,
264 registry_path: PathBuf,
266 isolation_enabled: bool,
268}
269
270impl ModuleManager {
271 pub fn new(registry_path: impl Into<PathBuf>) -> Self {
273 Self {
274 installed: HashMap::new(),
275 status: HashMap::new(),
276 registry_path: registry_path.into(),
277 isolation_enabled: true,
278 }
279 }
280
281 pub fn load(&mut self) -> Result<()> {
283 let modules_dir = self.registry_path.join("installed");
284 if !modules_dir.exists() {
285 return Ok(());
286 }
287
288 for entry in std::fs::read_dir(&modules_dir)? {
289 let entry = entry?;
290 let path = entry.path();
291 if path.is_dir() {
292 let manifest_path = path.join("module.dx");
293 if manifest_path.exists() {
294 match self.load_manifest(&manifest_path) {
295 Ok(mut module) => {
296 module.install_path = Some(path.clone());
297 self.status
298 .insert(module.id.clone(), ModuleStatus::Installed);
299 self.installed.insert(module.id.clone(), module);
300 }
301 Err(e) => {
302 eprintln!("Warning: Failed to load module at {:?}: {}", path, e);
303 }
304 }
305 }
306 }
307 }
308
309 Ok(())
310 }
311
312 pub fn install(&mut self, module_path: &Path) -> Result<()> {
314 let manifest_path = module_path.join("module.dx");
316 if !manifest_path.exists() {
317 return Err(DrivenError::Config(format!(
318 "Module manifest not found at {:?}",
319 manifest_path
320 )));
321 }
322
323 let module = self.load_manifest(&manifest_path)?;
324
325 if self.installed.contains_key(&module.id) {
327 return Err(DrivenError::Config(format!(
328 "Module '{}' is already installed",
329 module.id
330 )));
331 }
332
333 self.resolve_dependencies(&module)?;
335
336 self.status
338 .insert(module.id.clone(), ModuleStatus::Installing);
339
340 let install_dir = self.registry_path.join("installed").join(&module.id);
342 std::fs::create_dir_all(&install_dir)?;
343
344 self.copy_module_files(module_path, &install_dir)?;
346
347 let mut installed_module = module.clone();
349 installed_module.install_path = Some(install_dir);
350
351 self.status
353 .insert(installed_module.id.clone(), ModuleStatus::Installed);
354 self.installed
355 .insert(installed_module.id.clone(), installed_module);
356
357 Ok(())
358 }
359
360 pub fn uninstall(&mut self, module_id: &str) -> Result<()> {
362 let module = self.installed.get(module_id).ok_or_else(|| {
363 DrivenError::Config(format!("Module '{}' is not installed", module_id))
364 })?;
365
366 for (id, other) in &self.installed {
368 if id != module_id {
369 for dep in &other.dependencies {
370 if dep.module_id == module_id && !dep.optional {
371 return Err(DrivenError::Config(format!(
372 "Cannot uninstall '{}': module '{}' depends on it",
373 module_id, id
374 )));
375 }
376 }
377 }
378 }
379
380 if let Some(install_path) = &module.install_path {
382 if install_path.exists() {
383 std::fs::remove_dir_all(install_path)?;
384 }
385 }
386
387 self.installed.remove(module_id);
389 self.status.remove(module_id);
390
391 Ok(())
392 }
393
394 pub fn update(&mut self, module_id: &str, new_module_path: &Path) -> Result<()> {
396 if !self.installed.contains_key(module_id) {
398 return Err(DrivenError::Config(format!(
399 "Module '{}' is not installed",
400 module_id
401 )));
402 }
403
404 let manifest_path = new_module_path.join("module.dx");
406 let new_module = self.load_manifest(&manifest_path)?;
407
408 if new_module.id != module_id {
410 return Err(DrivenError::Config(format!(
411 "Module ID mismatch: expected '{}', got '{}'",
412 module_id, new_module.id
413 )));
414 }
415
416 self.uninstall(module_id)?;
418
419 self.install(new_module_path)?;
421
422 Ok(())
423 }
424
425 pub fn list_installed(&self) -> Vec<&Module> {
427 self.installed.values().collect()
428 }
429
430 pub fn get(&self, module_id: &str) -> Option<&Module> {
432 self.installed.get(module_id)
433 }
434
435 pub fn get_status(&self, module_id: &str) -> Option<ModuleStatus> {
437 self.status.get(module_id).copied()
438 }
439
440 pub fn resolve_dependencies(&self, module: &Module) -> Result<Vec<&Module>> {
442 let mut resolved = Vec::new();
443 let mut to_resolve: Vec<&ModuleDependency> = module.dependencies.iter().collect();
444 let mut visited = std::collections::HashSet::new();
445
446 while let Some(dep) = to_resolve.pop() {
447 if visited.contains(&dep.module_id) {
448 continue;
449 }
450 visited.insert(dep.module_id.clone());
451
452 match self.installed.get(&dep.module_id) {
453 Some(installed) => {
454 if !installed.satisfies_version(&dep.version_req) {
455 return Err(DrivenError::Config(format!(
456 "Dependency '{}' version {} does not satisfy requirement {}",
457 dep.module_id, installed.version, dep.version_req
458 )));
459 }
460 resolved.push(installed);
461 for trans_dep in &installed.dependencies {
463 to_resolve.push(trans_dep);
464 }
465 }
466 None if !dep.optional => {
467 return Err(DrivenError::Config(format!(
468 "Required dependency '{}' ({}) is not installed",
469 dep.module_id, dep.version_req
470 )));
471 }
472 None => {
473 }
475 }
476 }
477
478 Ok(resolved)
479 }
480
481 pub fn is_installed(&self, module_id: &str) -> bool {
483 self.installed.contains_key(module_id)
484 }
485
486 pub fn enable_isolation(&mut self) {
488 self.isolation_enabled = true;
489 }
490
491 pub fn disable_isolation(&mut self) {
493 self.isolation_enabled = false;
494 }
495
496 pub fn is_isolation_enabled(&self) -> bool {
498 self.isolation_enabled
499 }
500
501 pub fn get_all_agents(&self) -> Vec<String> {
503 let mut agents = Vec::new();
504 for module in self.installed.values() {
505 for agent in &module.agents {
506 if self.isolation_enabled {
507 agents.push(module.namespaced_agent(agent));
508 } else {
509 agents.push(agent.clone());
510 }
511 }
512 }
513 agents
514 }
515
516 pub fn get_all_workflows(&self) -> Vec<String> {
518 let mut workflows = Vec::new();
519 for module in self.installed.values() {
520 for workflow in &module.workflows {
521 if self.isolation_enabled {
522 workflows.push(module.namespaced_workflow(workflow));
523 } else {
524 workflows.push(workflow.clone());
525 }
526 }
527 }
528 workflows
529 }
530
531 pub fn get_all_templates(&self) -> Vec<String> {
533 let mut templates = Vec::new();
534 for module in self.installed.values() {
535 for template in &module.templates {
536 if self.isolation_enabled {
537 templates.push(module.namespaced_template(template));
538 } else {
539 templates.push(template.clone());
540 }
541 }
542 }
543 templates
544 }
545
546 fn load_manifest(&self, path: &Path) -> Result<Module> {
549 let content = std::fs::read_to_string(path)?;
550 self.parse_dx_manifest(&content)
551 }
552
553 fn parse_dx_manifest(&self, content: &str) -> Result<Module> {
554 let mut module = Module::new("", "", "");
556
557 for line in content.lines() {
558 let line = line.trim();
559 if line.is_empty() || line.starts_with('#') {
560 continue;
561 }
562
563 if let Some((key, value)) = line.split_once('|') {
564 let key = key.trim();
565 let value = value.trim();
566
567 match key {
568 "id" => module.id = value.to_string(),
569 "nm" | "name" => module.name = value.to_string(),
570 "v" | "version" => module.version = value.to_string(),
571 "desc" | "description" => module.description = value.to_string(),
572 "author" => module.author = Some(value.to_string()),
573 "license" => module.license = Some(value.to_string()),
574 key if key.starts_with("dep.") => {
575 let dep_id = key.strip_prefix("dep.").unwrap();
576 module
577 .dependencies
578 .push(ModuleDependency::new(dep_id, value));
579 }
580 key if key.starts_with("agent.") => {
581 module.agents.push(value.to_string());
582 }
583 key if key.starts_with("workflow.") => {
584 module.workflows.push(value.to_string());
585 }
586 key if key.starts_with("template.") => {
587 module.templates.push(value.to_string());
588 }
589 key if key.starts_with("resource.") => {
590 module.resources.push(value.to_string());
591 }
592 _ => {}
593 }
594 }
595 }
596
597 if module.id.is_empty() {
598 return Err(DrivenError::Config(
599 "Module manifest missing 'id' field".to_string(),
600 ));
601 }
602 if module.name.is_empty() {
603 module.name = module.id.clone();
604 }
605 if module.version.is_empty() {
606 module.version = "0.0.0".to_string();
607 }
608
609 Ok(module)
610 }
611
612 fn copy_module_files(&self, src: &Path, dest: &Path) -> Result<()> {
613 for entry in std::fs::read_dir(src)? {
615 let entry = entry?;
616 let src_path = entry.path();
617 let dest_path = dest.join(entry.file_name());
618
619 if src_path.is_dir() {
620 std::fs::create_dir_all(&dest_path)?;
621 self.copy_module_files(&src_path, &dest_path)?;
622 } else {
623 std::fs::copy(&src_path, &dest_path)?;
624 }
625 }
626 Ok(())
627 }
628}
629
630impl Default for ModuleManager {
631 fn default() -> Self {
632 Self::new(".driven/modules")
633 }
634}
635
636#[cfg(test)]
637mod tests {
638 use super::*;
639
640 #[test]
641 fn test_module_creation() {
642 let module = Module::new("test-module", "Test Module", "1.0.0")
643 .with_description("A test module")
644 .with_agent("test-agent")
645 .with_workflow("test-workflow");
646
647 assert_eq!(module.id, "test-module");
648 assert_eq!(module.name, "Test Module");
649 assert_eq!(module.version, "1.0.0");
650 assert_eq!(module.agents.len(), 1);
651 assert_eq!(module.workflows.len(), 1);
652 }
653
654 #[test]
655 fn test_version_satisfaction() {
656 let module = Module::new("test", "Test", "1.2.3");
657
658 assert!(module.satisfies_version("1.2.3"));
659 assert!(module.satisfies_version("*"));
660 assert!(module.satisfies_version("^1"));
661 assert!(module.satisfies_version("~1.2"));
662 assert!(!module.satisfies_version("2.0.0"));
663 assert!(!module.satisfies_version("^2"));
664 }
665
666 #[test]
667 fn test_namespacing() {
668 let module = Module::new("my-module", "My Module", "1.0.0")
669 .with_agent("agent1")
670 .with_workflow("workflow1");
671
672 assert_eq!(module.namespaced_agent("agent1"), "my-module:agent1");
673 assert_eq!(
674 module.namespaced_workflow("workflow1"),
675 "my-module:workflow1"
676 );
677 }
678
679 #[test]
680 fn test_dependency_creation() {
681 let dep = ModuleDependency::new("other-module", "^1.0.0");
682 assert_eq!(dep.module_id, "other-module");
683 assert_eq!(dep.version_req, "^1.0.0");
684 assert!(!dep.optional);
685
686 let opt_dep = ModuleDependency::optional("optional-module", "*");
687 assert!(opt_dep.optional);
688 }
689
690 #[test]
691 fn test_module_manager_creation() {
692 let manager = ModuleManager::new("/tmp/test-modules");
693 assert!(manager.list_installed().is_empty());
694 assert!(manager.is_isolation_enabled());
695 }
696}