1use crate::modules::{Module, ModuleManager, ModuleStatus};
6use crate::{DrivenError, Result};
7use console::style;
8use std::path::{Path, PathBuf};
9
10pub struct ModuleCommand {
12 manager: ModuleManager,
14 #[allow(dead_code)]
16 project_root: PathBuf,
17}
18
19impl ModuleCommand {
20 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 pub fn init(&mut self) -> Result<()> {
32 self.manager.load()
33 }
34
35 pub fn install(&mut self, module_path: &Path) -> Result<InstallResult> {
37 if !module_path.exists() {
39 return Err(DrivenError::Config(format!(
40 "Module path does not exist: {:?}",
41 module_path
42 )));
43 }
44
45 self.manager.install(module_path)?;
47
48 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 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 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 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 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 pub fn search(&self, _query: &str) -> Vec<ModuleInfo> {
163 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#[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#[derive(Debug, Clone)]
212pub struct UninstallResult {
213 pub module_id: String,
214 pub module_name: String,
215 pub version: String,
216}
217
218#[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#[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#[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#[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
268pub 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
308pub 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 print_modules_table(&[info]);
444 }
445}