Skip to main content

driven/cli/
module.rs

1//! Module CLI Command
2//!
3//! Commands for managing Driven modules: install, uninstall, list, update.
4
5use crate::modules::{Module, ModuleManager, ModuleStatus};
6use crate::{DrivenError, Result};
7use console::style;
8use std::path::{Path, PathBuf};
9
10/// Module command handler
11pub struct ModuleCommand {
12    /// Module manager instance
13    manager: ModuleManager,
14    /// Project root path
15    #[allow(dead_code)]
16    project_root: PathBuf,
17}
18
19impl ModuleCommand {
20    /// Create a new module command handler
21    pub fn new(project_root: impl Into<PathBuf>) -> Self {
22        let project_root = project_root.into();
23        let registry_path = project_root.join(".driven/modules");
24        Self {
25            manager: ModuleManager::new(registry_path),
26            project_root,
27        }
28    }
29
30    /// Initialize and load existing modules
31    pub fn init(&mut self) -> Result<()> {
32        self.manager.load()
33    }
34
35    /// Install a module from a path
36    pub fn install(&mut self, module_path: &Path) -> Result<InstallResult> {
37        // Ensure the path exists
38        if !module_path.exists() {
39            return Err(DrivenError::Config(format!(
40                "Module path does not exist: {:?}",
41                module_path
42            )));
43        }
44
45        // Install the module
46        self.manager.install(module_path)?;
47
48        // Get the installed module info
49        let manifest_path = module_path.join("module.dx");
50        let content = std::fs::read_to_string(&manifest_path)?;
51        let module = self.parse_manifest_for_info(&content)?;
52
53        Ok(InstallResult {
54            module_id: module.id,
55            module_name: module.name,
56            version: module.version,
57            agents_count: module.agents.len(),
58            workflows_count: module.workflows.len(),
59            templates_count: module.templates.len(),
60        })
61    }
62
63    /// Uninstall a module
64    pub fn uninstall(&mut self, module_id: &str) -> Result<UninstallResult> {
65        let module = self.manager.get(module_id).ok_or_else(|| {
66            DrivenError::Config(format!("Module '{}' is not installed", module_id))
67        })?;
68
69        let name = module.name.clone();
70        let version = module.version.clone();
71
72        self.manager.uninstall(module_id)?;
73
74        Ok(UninstallResult {
75            module_id: module_id.to_string(),
76            module_name: name,
77            version,
78        })
79    }
80
81    /// List all installed modules
82    pub fn list(&self) -> Vec<ModuleInfo> {
83        self.manager
84            .list_installed()
85            .iter()
86            .map(|m| ModuleInfo {
87                id: m.id.clone(),
88                name: m.name.clone(),
89                version: m.version.clone(),
90                description: m.description.clone(),
91                status: self
92                    .manager
93                    .get_status(&m.id)
94                    .unwrap_or(ModuleStatus::Installed),
95                agents_count: m.agents.len(),
96                workflows_count: m.workflows.len(),
97                templates_count: m.templates.len(),
98                dependencies_count: m.dependencies.len(),
99            })
100            .collect()
101    }
102
103    /// Update a module
104    pub fn update(&mut self, module_id: &str, new_path: &Path) -> Result<UpdateResult> {
105        let old_module = self.manager.get(module_id).ok_or_else(|| {
106            DrivenError::Config(format!("Module '{}' is not installed", module_id))
107        })?;
108
109        let old_version = old_module.version.clone();
110
111        self.manager.update(module_id, new_path)?;
112
113        let new_module = self
114            .manager
115            .get(module_id)
116            .ok_or_else(|| DrivenError::Config("Module not found after update".to_string()))?;
117
118        Ok(UpdateResult {
119            module_id: module_id.to_string(),
120            module_name: new_module.name.clone(),
121            old_version,
122            new_version: new_module.version.clone(),
123        })
124    }
125
126    /// Get detailed info about a specific module
127    pub fn info(&self, module_id: &str) -> Result<ModuleDetails> {
128        let module = self.manager.get(module_id).ok_or_else(|| {
129            DrivenError::Config(format!("Module '{}' is not installed", module_id))
130        })?;
131
132        Ok(ModuleDetails {
133            id: module.id.clone(),
134            name: module.name.clone(),
135            version: module.version.clone(),
136            description: module.description.clone(),
137            author: module.author.clone(),
138            license: module.license.clone(),
139            status: self
140                .manager
141                .get_status(module_id)
142                .unwrap_or(ModuleStatus::Installed),
143            agents: module.agents.clone(),
144            workflows: module.workflows.clone(),
145            templates: module.templates.clone(),
146            resources: module.resources.clone(),
147            dependencies: module
148                .dependencies
149                .iter()
150                .map(|d| DependencyInfo {
151                    module_id: d.module_id.clone(),
152                    version_req: d.version_req.clone(),
153                    optional: d.optional,
154                    installed: self.manager.is_installed(&d.module_id),
155                })
156                .collect(),
157            install_path: module.install_path.clone(),
158        })
159    }
160
161    /// Search for modules (placeholder for future registry support)
162    pub fn search(&self, _query: &str) -> Vec<ModuleInfo> {
163        // TODO: Implement module registry search
164        Vec::new()
165    }
166
167    fn parse_manifest_for_info(&self, content: &str) -> Result<Module> {
168        let mut module = Module::new("", "", "");
169
170        for line in content.lines() {
171            let line = line.trim();
172            if line.is_empty() || line.starts_with('#') {
173                continue;
174            }
175
176            if let Some((key, value)) = line.split_once('|') {
177                match key.trim() {
178                    "id" => module.id = value.trim().to_string(),
179                    "nm" | "name" => module.name = value.trim().to_string(),
180                    "v" | "version" => module.version = value.trim().to_string(),
181                    key if key.starts_with("agent.") => {
182                        module.agents.push(value.trim().to_string());
183                    }
184                    key if key.starts_with("workflow.") => {
185                        module.workflows.push(value.trim().to_string());
186                    }
187                    key if key.starts_with("template.") => {
188                        module.templates.push(value.trim().to_string());
189                    }
190                    _ => {}
191                }
192            }
193        }
194
195        Ok(module)
196    }
197}
198
199/// Result of module installation
200#[derive(Debug, Clone)]
201pub struct InstallResult {
202    pub module_id: String,
203    pub module_name: String,
204    pub version: String,
205    pub agents_count: usize,
206    pub workflows_count: usize,
207    pub templates_count: usize,
208}
209
210/// Result of module uninstallation
211#[derive(Debug, Clone)]
212pub struct UninstallResult {
213    pub module_id: String,
214    pub module_name: String,
215    pub version: String,
216}
217
218/// Result of module update
219#[derive(Debug, Clone)]
220pub struct UpdateResult {
221    pub module_id: String,
222    pub module_name: String,
223    pub old_version: String,
224    pub new_version: String,
225}
226
227/// Summary info about a module
228#[derive(Debug, Clone)]
229pub struct ModuleInfo {
230    pub id: String,
231    pub name: String,
232    pub version: String,
233    pub description: String,
234    pub status: ModuleStatus,
235    pub agents_count: usize,
236    pub workflows_count: usize,
237    pub templates_count: usize,
238    pub dependencies_count: usize,
239}
240
241/// Detailed info about a module
242#[derive(Debug, Clone)]
243pub struct ModuleDetails {
244    pub id: String,
245    pub name: String,
246    pub version: String,
247    pub description: String,
248    pub author: Option<String>,
249    pub license: Option<String>,
250    pub status: ModuleStatus,
251    pub agents: Vec<String>,
252    pub workflows: Vec<String>,
253    pub templates: Vec<String>,
254    pub resources: Vec<String>,
255    pub dependencies: Vec<DependencyInfo>,
256    pub install_path: Option<PathBuf>,
257}
258
259/// Info about a dependency
260#[derive(Debug, Clone)]
261pub struct DependencyInfo {
262    pub module_id: String,
263    pub version_req: String,
264    pub optional: bool,
265    pub installed: bool,
266}
267
268/// Print modules in a table format
269pub fn print_modules_table(modules: &[ModuleInfo]) {
270    if modules.is_empty() {
271        println!("{}", style("No modules installed").dim());
272        return;
273    }
274
275    println!(
276        "{:<20} {:<25} {:<10} {:<10} {:<8} {:<8} {:<8}",
277        style("ID").bold().underlined(),
278        style("Name").bold().underlined(),
279        style("Version").bold().underlined(),
280        style("Status").bold().underlined(),
281        style("Agents").bold().underlined(),
282        style("Flows").bold().underlined(),
283        style("Tmpls").bold().underlined(),
284    );
285
286    for module in modules {
287        let status_str = match module.status {
288            ModuleStatus::Installed => style("OK").green(),
289            ModuleStatus::Installing => style("...").yellow(),
290            ModuleStatus::UpdateAvailable => style("UPD").cyan(),
291            ModuleStatus::DependencyError => style("ERR").red(),
292            ModuleStatus::Disabled => style("OFF").dim(),
293        };
294
295        println!(
296            "{:<20} {:<25} {:<10} {:<10} {:<8} {:<8} {:<8}",
297            module.id,
298            truncate(&module.name, 24),
299            module.version,
300            status_str,
301            module.agents_count,
302            module.workflows_count,
303            module.templates_count,
304        );
305    }
306}
307
308/// Print detailed module info
309pub fn print_module_details(details: &ModuleDetails) {
310    println!("{}", style(&details.name).bold().cyan());
311    println!("{}", style("─".repeat(50)).dim());
312
313    println!("  {} {}", style("ID:").bold(), details.id);
314    println!("  {} {}", style("Version:").bold(), details.version);
315
316    if !details.description.is_empty() {
317        println!("  {} {}", style("Description:").bold(), details.description);
318    }
319
320    if let Some(author) = &details.author {
321        println!("  {} {}", style("Author:").bold(), author);
322    }
323
324    if let Some(license) = &details.license {
325        println!("  {} {}", style("License:").bold(), license);
326    }
327
328    let status_str = match details.status {
329        ModuleStatus::Installed => style("Installed").green(),
330        ModuleStatus::Installing => style("Installing").yellow(),
331        ModuleStatus::UpdateAvailable => style("Update Available").cyan(),
332        ModuleStatus::DependencyError => style("Dependency Error").red(),
333        ModuleStatus::Disabled => style("Disabled").dim(),
334    };
335    println!("  {} {}", style("Status:").bold(), status_str);
336
337    if let Some(path) = &details.install_path {
338        println!("  {} {:?}", style("Path:").bold(), path);
339    }
340
341    if !details.agents.is_empty() {
342        println!(
343            "\n  {} ({})",
344            style("Agents").bold().underlined(),
345            details.agents.len()
346        );
347        for agent in &details.agents {
348            println!("    • {}", agent);
349        }
350    }
351
352    if !details.workflows.is_empty() {
353        println!(
354            "\n  {} ({})",
355            style("Workflows").bold().underlined(),
356            details.workflows.len()
357        );
358        for workflow in &details.workflows {
359            println!("    • {}", workflow);
360        }
361    }
362
363    if !details.templates.is_empty() {
364        println!(
365            "\n  {} ({})",
366            style("Templates").bold().underlined(),
367            details.templates.len()
368        );
369        for template in &details.templates {
370            println!("    • {}", template);
371        }
372    }
373
374    if !details.resources.is_empty() {
375        println!(
376            "\n  {} ({})",
377            style("Resources").bold().underlined(),
378            details.resources.len()
379        );
380        for resource in &details.resources {
381            println!("    • {}", resource);
382        }
383    }
384
385    if !details.dependencies.is_empty() {
386        println!(
387            "\n  {} ({})",
388            style("Dependencies").bold().underlined(),
389            details.dependencies.len()
390        );
391        for dep in &details.dependencies {
392            let status = if dep.installed {
393                style("✓").green()
394            } else if dep.optional {
395                style("○").dim()
396            } else {
397                style("✗").red()
398            };
399            let optional_str = if dep.optional { " (optional)" } else { "" };
400            println!(
401                "    {} {} {}{}",
402                status, dep.module_id, dep.version_req, optional_str
403            );
404        }
405    }
406}
407
408fn truncate(s: &str, max_len: usize) -> String {
409    if s.len() <= max_len {
410        s.to_string()
411    } else {
412        format!("{}…", &s[..max_len - 1])
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419    use tempfile::TempDir;
420
421    #[test]
422    fn test_module_command_creation() {
423        let temp_dir = TempDir::new().unwrap();
424        let cmd = ModuleCommand::new(temp_dir.path());
425        assert!(cmd.list().is_empty());
426    }
427
428    #[test]
429    fn test_module_info_display() {
430        let info = ModuleInfo {
431            id: "test-module".to_string(),
432            name: "Test Module".to_string(),
433            version: "1.0.0".to_string(),
434            description: "A test module".to_string(),
435            status: ModuleStatus::Installed,
436            agents_count: 2,
437            workflows_count: 3,
438            templates_count: 1,
439            dependencies_count: 0,
440        };
441
442        // Just verify it doesn't panic
443        print_modules_table(&[info]);
444    }
445}