ricecoder_cli/commands/
custom.rs

1// Custom commands module - CLI integration for ricecoder-commands
2// Integrates the ricecoder-commands crate with the CLI router and storage
3
4use super::custom_storage::CustomCommandsStorage;
5use super::Command;
6use crate::error::{CliError, CliResult};
7use ricecoder_commands::{CommandManager, CommandRegistry, ConfigManager};
8use std::collections::HashMap;
9use std::path::PathBuf;
10
11/// Action for custom command handler
12#[derive(Debug, Clone)]
13pub enum CustomAction {
14    /// List all available custom commands
15    List,
16    /// Show info for a specific command
17    Info(String),
18    /// Execute a custom command
19    Run(String, Vec<String>),
20    /// Load custom commands from a file
21    Load(String),
22    /// Search for custom commands
23    Search(String),
24}
25
26/// Handler for custom command CLI operations
27pub struct CustomCommandHandler {
28    action: CustomAction,
29    manager: CommandManager,
30    storage: CustomCommandsStorage,
31}
32
33impl CustomCommandHandler {
34    /// Create a new custom command handler
35    pub fn new(action: CustomAction) -> Self {
36        // Load commands from storage
37        let storage = CustomCommandsStorage::default();
38        let registry = storage
39            .load_all()
40            .unwrap_or_else(|_| CommandRegistry::new());
41        let manager = CommandManager::new(registry);
42
43        Self {
44            action,
45            manager,
46            storage,
47        }
48    }
49
50    /// Create a handler with a pre-populated manager
51    pub fn with_manager(action: CustomAction, manager: CommandManager) -> Self {
52        let storage = CustomCommandsStorage::default();
53        Self {
54            action,
55            manager,
56            storage,
57        }
58    }
59}
60
61impl Command for CustomCommandHandler {
62    fn execute(&self) -> CliResult<()> {
63        match &self.action {
64            CustomAction::List => self.handle_list(),
65            CustomAction::Info(name) => self.handle_info(name),
66            CustomAction::Run(name, args) => self.handle_run(name, args),
67            CustomAction::Load(file) => self.handle_load(file),
68            CustomAction::Search(query) => self.handle_search(query),
69        }
70    }
71}
72
73impl CustomCommandHandler {
74    /// Handle list action - display all available commands
75    fn handle_list(&self) -> CliResult<()> {
76        let commands = self.manager.list_commands();
77
78        if commands.is_empty() {
79            println!("No custom commands available.");
80            println!("Use 'rice custom load <file>' to load commands from a file.");
81            return Ok(());
82        }
83
84        println!("Available Commands:");
85        println!("==================\n");
86
87        for cmd in commands {
88            println!("{:<20} {}", cmd.name, cmd.description);
89        }
90
91        Ok(())
92    }
93
94    /// Handle info action - show info for a specific command
95    fn handle_info(&self, name: &str) -> CliResult<()> {
96        let cmd = self
97            .manager
98            .get_command(name)
99            .map_err(|e| CliError::Internal(e.to_string()))?;
100
101        println!("Command: {}", cmd.name);
102        println!("Description: {}", cmd.description);
103        println!("Command: {}", cmd.command);
104
105        if !cmd.arguments.is_empty() {
106            println!("\nArguments:");
107            for arg in &cmd.arguments {
108                let required = if arg.required { "required" } else { "optional" };
109                println!("  {} ({}): {}", arg.name, required, arg.description);
110                if let Some(default) = &arg.default {
111                    println!("    Default: {}", default);
112                }
113            }
114        }
115
116        Ok(())
117    }
118
119    /// Handle run action - execute a custom command
120    fn handle_run(&self, name: &str, args: &[String]) -> CliResult<()> {
121        // Get the command from registry
122        let cmd = self
123            .manager
124            .get_command(name)
125            .map_err(|e| CliError::Internal(e.to_string()))?;
126
127        // Build arguments map from positional arguments
128        let mut arguments = HashMap::new();
129        for (i, arg) in args.iter().enumerate() {
130            if i < cmd.arguments.len() {
131                arguments.insert(cmd.arguments[i].name.clone(), arg.clone());
132            }
133        }
134
135        // Execute the command
136        let cwd = std::env::current_dir()
137            .unwrap_or_else(|_| PathBuf::from("."))
138            .to_string_lossy()
139            .to_string();
140
141        let result = self
142            .manager
143            .execute(name, arguments, cwd)
144            .map_err(|e| CliError::Internal(e.to_string()))?;
145
146        // Display the result
147        if result.success {
148            println!("{}", result.stdout);
149        } else {
150            eprintln!("{}", result.stderr);
151            return Err(CliError::Internal(format!(
152                "Command failed with exit code {}",
153                result.exit_code
154            )));
155        }
156
157        Ok(())
158    }
159
160    /// Handle load action - load commands from a file
161    fn handle_load(&self, file: &str) -> CliResult<()> {
162        let registry =
163            ConfigManager::load_from_file(file).map_err(|e| CliError::Internal(e.to_string()))?;
164
165        let commands = registry.list_all();
166
167        if commands.is_empty() {
168            println!("No commands found in file: {}", file);
169            return Ok(());
170        }
171
172        println!("Loaded {} command(s) from {}", commands.len(), file);
173
174        // Save each command to storage
175        for cmd in commands {
176            println!("  - {}: {}", cmd.name, cmd.description);
177
178            match self.storage.save_command(&cmd) {
179                Ok(path) => {
180                    println!("    Saved to: {}", path.display());
181                }
182                Err(e) => {
183                    eprintln!("    Warning: Failed to save command: {}", e);
184                }
185            }
186        }
187
188        Ok(())
189    }
190
191    /// Handle search action - search for commands
192    fn handle_search(&self, query: &str) -> CliResult<()> {
193        let results = self.manager.search_commands(query);
194
195        if results.is_empty() {
196            println!("No commands found matching '{}'", query);
197            return Ok(());
198        }
199
200        println!("Search results for '{}':", query);
201        println!("========================\n");
202
203        for cmd in results {
204            println!("{:<20} {}", cmd.name, cmd.description);
205        }
206
207        Ok(())
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_custom_handler_list_empty() {
217        let handler = CustomCommandHandler::new(CustomAction::List);
218        // Should not panic and should handle empty registry gracefully
219        let result = handler.execute();
220        assert!(result.is_ok());
221    }
222
223    #[test]
224    fn test_custom_handler_info_not_found() {
225        let handler = CustomCommandHandler::new(CustomAction::Info("nonexistent".to_string()));
226        let result = handler.execute();
227        assert!(result.is_err());
228    }
229
230    #[test]
231    fn test_custom_handler_run_not_found() {
232        let handler =
233            CustomCommandHandler::new(CustomAction::Run("nonexistent".to_string(), vec![]));
234        let result = handler.execute();
235        assert!(result.is_err());
236    }
237
238    #[test]
239    fn test_custom_handler_search_empty() {
240        let handler = CustomCommandHandler::new(CustomAction::Search("test".to_string()));
241        // Should not panic and should handle empty results gracefully
242        let result = handler.execute();
243        assert!(result.is_ok());
244    }
245}