1use crate::output::{render_output, MultiFormatDisplay, OutputFormat};
4use crate::plugin::PluginManager;
5use anyhow::Result;
6use clap::Subcommand;
7use comfy_table::{presets::UTF8_FULL, Cell, Color, ContentArrangement, Table};
8use serde::Serialize;
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12#[derive(Subcommand)]
13pub enum PluginCommands {
14 #[command(visible_aliases = &["ls"])]
16 List,
17
18 #[command(visible_aliases = &["show", "details"])]
20 Info {
21 name: String,
23 },
24
25 #[command(visible_aliases = &["add"])]
27 Install {
28 path: PathBuf,
30 },
31
32 #[command(visible_aliases = &["remove", "rm"])]
34 Uninstall {
35 name: String,
37
38 #[arg(short = 'y', long)]
40 yes: bool,
41 },
42
43 Enable {
45 name: String,
47 },
48
49 Disable {
51 name: String,
53 },
54
55 #[command(visible_aliases = &["run", "exec"])]
57 Execute {
58 plugin: String,
60
61 command: String,
63
64 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
66 args: Vec<String>,
67 },
68
69 Reload,
71
72 #[command(visible_aliases = &["dir"])]
74 Directory,
75}
76
77pub async fn handle_plugin_command(cmd: PluginCommands, format: OutputFormat) -> Result<()> {
78 match cmd {
79 PluginCommands::List => list_plugins(format).await,
80 PluginCommands::Info { name } => show_plugin_info(&name, format).await,
81 PluginCommands::Install { path } => install_plugin(&path, format).await,
82 PluginCommands::Uninstall { name, yes } => uninstall_plugin(&name, yes, format).await,
83 PluginCommands::Enable { name } => enable_plugin(&name, format).await,
84 PluginCommands::Disable { name } => disable_plugin(&name, format).await,
85 PluginCommands::Execute {
86 plugin,
87 command,
88 args,
89 } => execute_plugin_command(&plugin, &command, args, format).await,
90 PluginCommands::Reload => reload_plugins(format).await,
91 PluginCommands::Directory => show_plugin_directory(format).await,
92 }
93}
94
95#[derive(Debug, Serialize)]
96struct PluginListEntry {
97 name: String,
98 version: String,
99 description: String,
100 enabled: String,
101 commands: usize,
102}
103
104impl MultiFormatDisplay for Vec<PluginListEntry> {
105 fn to_table(&self) -> Table {
106 let mut table = Table::new();
107 table
108 .load_preset(UTF8_FULL)
109 .set_content_arrangement(ContentArrangement::Dynamic);
110
111 table.set_header(vec![
112 Cell::new("Name").fg(Color::Cyan),
113 Cell::new("Version").fg(Color::Cyan),
114 Cell::new("Description").fg(Color::Cyan),
115 Cell::new("Enabled").fg(Color::Cyan),
116 Cell::new("Commands").fg(Color::Cyan),
117 ]);
118
119 for entry in self {
120 let enabled_cell = if entry.enabled == "Yes" {
121 Cell::new(&entry.enabled).fg(Color::Green)
122 } else {
123 Cell::new(&entry.enabled).fg(Color::Red)
124 };
125
126 table.add_row(vec![
127 Cell::new(&entry.name),
128 Cell::new(&entry.version),
129 Cell::new(&entry.description),
130 enabled_cell,
131 Cell::new(entry.commands.to_string()),
132 ]);
133 }
134
135 table
136 }
137
138 fn to_quiet(&self) -> String {
139 self.iter()
140 .map(|e| e.name.clone())
141 .collect::<Vec<_>>()
142 .join("\n")
143 }
144}
145
146async fn list_plugins(format: OutputFormat) -> Result<()> {
147 let mut manager = PluginManager::new()?;
148 manager.discover_plugins()?;
149
150 let plugins = manager.list_plugins();
151 let entries: Vec<PluginListEntry> = plugins
152 .iter()
153 .map(|p| PluginListEntry {
154 name: p.metadata.name.clone(),
155 version: p.metadata.version.clone(),
156 description: p.metadata.description.clone(),
157 enabled: if p.enabled { "Yes" } else { "No" }.to_string(),
158 commands: p.metadata.commands.len(),
159 })
160 .collect();
161
162 println!("{}", render_output(&entries, format)?);
163 Ok(())
164}
165
166#[derive(Debug, Serialize)]
167struct PluginInfoDisplay {
168 name: String,
169 version: String,
170 description: String,
171 author: String,
172 license: String,
173 enabled: String,
174 commands: Vec<CommandInfo>,
175 dependencies: Vec<String>,
176 min_version: String,
177}
178
179#[derive(Debug, Serialize)]
180struct CommandInfo {
181 name: String,
182 description: String,
183 aliases: String,
184 arguments: usize,
185}
186
187impl MultiFormatDisplay for PluginInfoDisplay {
188 fn to_table(&self) -> Table {
189 let mut table = Table::new();
190 table
191 .load_preset(UTF8_FULL)
192 .set_content_arrangement(ContentArrangement::Dynamic);
193
194 table.add_row(vec![
195 Cell::new("Name").fg(Color::Cyan),
196 Cell::new(&self.name),
197 ]);
198 table.add_row(vec![
199 Cell::new("Version").fg(Color::Cyan),
200 Cell::new(&self.version),
201 ]);
202 table.add_row(vec![
203 Cell::new("Description").fg(Color::Cyan),
204 Cell::new(&self.description),
205 ]);
206 table.add_row(vec![
207 Cell::new("Author").fg(Color::Cyan),
208 Cell::new(&self.author),
209 ]);
210 table.add_row(vec![
211 Cell::new("License").fg(Color::Cyan),
212 Cell::new(&self.license),
213 ]);
214 table.add_row(vec![
215 Cell::new("Enabled").fg(Color::Cyan),
216 Cell::new(&self.enabled),
217 ]);
218 table.add_row(vec![
219 Cell::new("Commands").fg(Color::Cyan),
220 Cell::new(self.commands.len().to_string()),
221 ]);
222
223 if !self.commands.is_empty() {
224 table.add_row(vec![Cell::new("").fg(Color::Cyan), Cell::new("")]);
225 table.add_row(vec![
226 Cell::new("Available Commands")
227 .fg(Color::Yellow)
228 .add_attribute(comfy_table::Attribute::Bold),
229 Cell::new(""),
230 ]);
231 for cmd in &self.commands {
232 table.add_row(vec![
233 Cell::new(format!(" {}", cmd.name)),
234 Cell::new(&cmd.description),
235 ]);
236 }
237 }
238
239 table
240 }
241
242 fn to_quiet(&self) -> String {
243 format!("{} v{}", self.name, self.version)
244 }
245}
246
247async fn show_plugin_info(name: &str, format: OutputFormat) -> Result<()> {
248 let mut manager = PluginManager::new()?;
249 manager.discover_plugins()?;
250
251 let plugin = manager
252 .get_plugin(name)
253 .ok_or_else(|| anyhow::anyhow!("Plugin not found: {}", name))?;
254
255 let info = PluginInfoDisplay {
256 name: plugin.metadata.name.clone(),
257 version: plugin.metadata.version.clone(),
258 description: plugin.metadata.description.clone(),
259 author: plugin.metadata.author.clone(),
260 license: plugin.metadata.license.clone(),
261 enabled: if plugin.enabled { "Yes" } else { "No" }.to_string(),
262 commands: plugin
263 .metadata
264 .commands
265 .iter()
266 .map(|c| CommandInfo {
267 name: c.name.clone(),
268 description: c.description.clone(),
269 aliases: c.aliases.join(", "),
270 arguments: c.arguments.len(),
271 })
272 .collect(),
273 dependencies: plugin.metadata.dependencies.clone(),
274 min_version: plugin
275 .metadata
276 .min_version
277 .clone()
278 .unwrap_or_else(|| "None".to_string()),
279 };
280
281 println!("{}", render_output(&info, format)?);
282 Ok(())
283}
284
285async fn install_plugin(path: &Path, format: OutputFormat) -> Result<()> {
286 if !path.exists() {
287 anyhow::bail!("Plugin directory not found: {:?}", path);
288 }
289
290 if !path.is_dir() {
291 anyhow::bail!("Path is not a directory: {:?}", path);
292 }
293
294 let mut manager = PluginManager::new()?;
295 manager.install_plugin(path)?;
296
297 if format != OutputFormat::Quiet {
298 println!("✓ Plugin installed successfully");
299 }
300
301 Ok(())
302}
303
304async fn uninstall_plugin(name: &str, yes: bool, format: OutputFormat) -> Result<()> {
305 let mut manager = PluginManager::new()?;
306 manager.discover_plugins()?;
307
308 if manager.get_plugin(name).is_none() {
310 anyhow::bail!("Plugin not found: {}", name);
311 }
312
313 if !yes && format != OutputFormat::Quiet {
315 println!(
316 "Are you sure you want to uninstall plugin '{}'? [y/N]",
317 name
318 );
319 let mut input = String::new();
320 std::io::stdin().read_line(&mut input)?;
321 if !input.trim().eq_ignore_ascii_case("y") {
322 println!("Uninstall cancelled");
323 return Ok(());
324 }
325 }
326
327 manager.uninstall_plugin(name)?;
328
329 if format != OutputFormat::Quiet {
330 println!("✓ Plugin uninstalled successfully");
331 }
332
333 Ok(())
334}
335
336async fn enable_plugin(name: &str, format: OutputFormat) -> Result<()> {
337 let mut manager = PluginManager::new()?;
338 manager.discover_plugins()?;
339
340 manager.enable_plugin(name)?;
341
342 if format != OutputFormat::Quiet {
343 println!("✓ Plugin enabled: {}", name);
344 }
345
346 Ok(())
347}
348
349async fn disable_plugin(name: &str, format: OutputFormat) -> Result<()> {
350 let mut manager = PluginManager::new()?;
351 manager.discover_plugins()?;
352
353 manager.disable_plugin(name)?;
354
355 if format != OutputFormat::Quiet {
356 println!("✓ Plugin disabled: {}", name);
357 }
358
359 Ok(())
360}
361
362async fn execute_plugin_command(
363 plugin: &str,
364 command: &str,
365 args: Vec<String>,
366 format: OutputFormat,
367) -> Result<()> {
368 let mut manager = PluginManager::new()?;
369 manager.discover_plugins()?;
370
371 let mut arguments = HashMap::new();
373 for arg in args {
374 let parts: Vec<&str> = arg.splitn(2, '=').collect();
375 if parts.len() == 2 {
376 arguments.insert(parts[0].to_string(), parts[1].to_string());
377 } else {
378 anyhow::bail!("Invalid argument format: {}. Expected KEY=VALUE", arg);
379 }
380 }
381
382 let result = manager.execute_command(plugin, command, arguments).await?;
383
384 if result.exit_code != 0 {
385 if !result.stderr.is_empty() && format != OutputFormat::Quiet {
386 eprintln!("{}", result.stderr);
387 }
388 anyhow::bail!("Plugin command failed with exit code: {}", result.exit_code);
389 }
390
391 if !result.stdout.is_empty() && format != OutputFormat::Quiet {
392 println!("{}", result.stdout);
393 }
394
395 if let Some(data) = result.data {
396 if format == OutputFormat::Json {
397 println!("{}", serde_json::to_string_pretty(&data)?);
398 } else if format == OutputFormat::Yaml {
399 println!("{}", serde_yaml::to_string(&data)?);
400 }
401 }
402
403 Ok(())
404}
405
406async fn reload_plugins(format: OutputFormat) -> Result<()> {
407 let mut manager = PluginManager::new()?;
408 let count = manager.discover_plugins()?;
409
410 if format != OutputFormat::Quiet {
411 println!("✓ Reloaded {} plugin(s)", count);
412 }
413
414 Ok(())
415}
416
417#[derive(Debug, Serialize)]
418struct PluginDirectoryInfo {
419 path: String,
420 exists: bool,
421}
422
423impl MultiFormatDisplay for PluginDirectoryInfo {
424 fn to_table(&self) -> Table {
425 let mut table = Table::new();
426 table
427 .load_preset(UTF8_FULL)
428 .set_content_arrangement(ContentArrangement::Dynamic);
429
430 table.add_row(vec![
431 Cell::new("Plugin Directory").fg(Color::Cyan),
432 Cell::new(&self.path),
433 ]);
434
435 table.add_row(vec![
436 Cell::new("Exists").fg(Color::Cyan),
437 if self.exists {
438 Cell::new("Yes").fg(Color::Green)
439 } else {
440 Cell::new("No").fg(Color::Red)
441 },
442 ]);
443
444 table
445 }
446
447 fn to_quiet(&self) -> String {
448 self.path.clone()
449 }
450}
451
452async fn show_plugin_directory(format: OutputFormat) -> Result<()> {
453 let plugin_dir = PluginManager::get_plugin_dir()?;
454
455 let info = PluginDirectoryInfo {
456 path: plugin_dir.to_string_lossy().to_string(),
457 exists: plugin_dir.exists(),
458 };
459
460 println!("{}", render_output(&info, format)?);
461 Ok(())
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
469 fn test_plugin_list_entry_serialization() {
470 let entry = PluginListEntry {
471 name: "test-plugin".to_string(),
472 version: "1.0.0".to_string(),
473 description: "Test plugin".to_string(),
474 enabled: "Yes".to_string(),
475 commands: 2,
476 };
477
478 let json = serde_json::to_string(&entry).unwrap();
479 assert!(json.contains("test-plugin"));
480 assert!(json.contains("1.0.0"));
481 }
482
483 #[test]
484 fn test_plugin_info_display() {
485 let info = PluginInfoDisplay {
486 name: "test-plugin".to_string(),
487 version: "1.0.0".to_string(),
488 description: "Test plugin".to_string(),
489 author: "Test Author".to_string(),
490 license: "MIT".to_string(),
491 enabled: "Yes".to_string(),
492 commands: vec![],
493 dependencies: vec![],
494 min_version: "0.1.0".to_string(),
495 };
496
497 let quiet = info.to_quiet();
498 assert_eq!(quiet, "test-plugin v1.0.0");
499 }
500
501 #[test]
502 fn test_plugin_directory_info() {
503 let info = PluginDirectoryInfo {
504 path: "/tmp/plugins".to_string(),
505 exists: false,
506 };
507
508 let quiet = info.to_quiet();
509 assert_eq!(quiet, "/tmp/plugins");
510 }
511}