ricecoder_cli/
router.rs

1// Command routing and dispatch
2// Adapted from automation/src/cli/router.rs
3
4use crate::commands::*;
5use crate::error::{CliError, CliResult};
6use clap::{Parser, Subcommand};
7
8/// RiceCoder - Terminal-first, spec-driven coding assistant
9#[derive(Parser, Debug)]
10#[command(name = "rice")]
11#[command(bin_name = "rice")]
12#[command(about = "Terminal-first, spec-driven coding assistant")]
13#[command(
14    long_about = "RiceCoder: A terminal-first, spec-driven coding assistant.\n\nGenerate code from specifications, refactor existing code, and get AI-powered code reviews.\n\nFor more information, visit: https://ricecoder.dev"
15)]
16#[command(version)]
17#[command(author = "RiceCoder Contributors")]
18#[command(arg_required_else_help = true)]
19#[command(disable_help_subcommand = true)]
20pub struct Cli {
21    #[command(subcommand)]
22    pub command: Commands,
23
24    /// Enable verbose output
25    #[arg(short, long, global = true)]
26    pub verbose: bool,
27
28    /// Minimize output
29    #[arg(short, long, global = true)]
30    pub quiet: bool,
31
32    /// Preview changes without applying them
33    #[arg(long, global = true)]
34    pub dry_run: bool,
35}
36
37#[derive(Subcommand, Debug)]
38pub enum Commands {
39    /// Initialize a new ricecoder project
40    #[command(about = "Initialize a new ricecoder project with default configuration")]
41    Init {
42        /// Project path (default: current directory)
43        #[arg(value_name = "PATH")]
44        path: Option<String>,
45    },
46
47    /// Generate code from a specification
48    #[command(about = "Generate code from a specification file")]
49    Gen {
50        /// Path to specification file
51        #[arg(value_name = "SPEC")]
52        spec: String,
53    },
54
55    /// Interactive chat mode with spec awareness
56    #[command(about = "Enter interactive chat mode for free-form coding assistance")]
57    Chat {
58        /// Initial message to send
59        #[arg(value_name = "MESSAGE")]
60        message: Option<String>,
61
62        /// AI provider to use (openai, anthropic, local)
63        #[arg(short, long)]
64        provider: Option<String>,
65
66        /// Model to use
67        #[arg(short, long)]
68        model: Option<String>,
69    },
70
71    /// Refactor existing code
72    #[command(about = "Refactor existing code using AI assistance")]
73    Refactor {
74        /// File to refactor
75        #[arg(value_name = "FILE")]
76        file: String,
77    },
78
79    /// Review code for improvements
80    #[command(about = "Review code for improvements and best practices")]
81    Review {
82        /// File to review
83        #[arg(value_name = "FILE")]
84        file: String,
85    },
86
87    /// Manage configuration settings
88    #[command(about = "View and manage ricecoder configuration")]
89    Config {
90        #[command(subcommand)]
91        action: Option<ConfigSubcommand>,
92    },
93
94    /// Generate shell completions
95    #[command(about = "Generate shell completion scripts")]
96    Completions {
97        /// Shell to generate completions for (bash, zsh, fish, powershell)
98        #[arg(value_name = "SHELL")]
99        shell: String,
100    },
101
102    /// Manage and execute custom commands
103    #[command(about = "Manage and execute custom commands")]
104    Custom {
105        #[command(subcommand)]
106        action: Option<CustomSubcommand>,
107    },
108
109    /// Launch the terminal user interface
110    #[command(about = "Launch the beautiful terminal user interface")]
111    Tui {
112        /// Theme to use (dark, light, monokai, dracula, nord)
113        #[arg(short, long)]
114        theme: Option<String>,
115
116        /// Enable vim keybindings
117        #[arg(long)]
118        vim_mode: bool,
119
120        /// Custom config file path
121        #[arg(short, long)]
122        config: Option<String>,
123
124        /// AI provider to use (openai, anthropic, local)
125        #[arg(short, long)]
126        provider: Option<String>,
127
128        /// Model to use
129        #[arg(short, long)]
130        model: Option<String>,
131    },
132
133    /// Manage sessions
134    #[command(about = "Manage ricecoder sessions")]
135    Sessions {
136        #[command(subcommand)]
137        action: Option<SessionsSubcommand>,
138    },
139
140    /// Start the Language Server Protocol server
141    #[command(about = "Start the Language Server Protocol server for IDE integration")]
142    Lsp {
143        /// Log level (trace, debug, info, warn, error)
144        #[arg(short, long, default_value = "info")]
145        log_level: Option<String>,
146
147        /// Port for TCP transport (future support)
148        #[arg(short, long)]
149        port: Option<u16>,
150
151        /// Enable debug mode for verbose logging
152        #[arg(long)]
153        debug: bool,
154    },
155
156    /// Manage hooks for event-driven automation
157    #[command(about = "Manage hooks for event-driven automation")]
158    Hooks {
159        #[command(subcommand)]
160        action: Option<HooksSubcommand>,
161    },
162
163    /// Show help and tutorials
164    #[command(about = "Show help, tutorials, and troubleshooting guides")]
165    Help {
166        /// Topic to get help on (command name, 'tutorial', 'troubleshooting')
167        #[arg(value_name = "TOPIC")]
168        topic: Option<String>,
169    },
170}
171
172#[derive(Subcommand, Debug)]
173pub enum HooksSubcommand {
174    /// List all hooks
175    #[command(about = "List all registered hooks")]
176    List {
177        /// Output format (table or json)
178        #[arg(short, long)]
179        format: Option<String>,
180    },
181
182    /// Inspect a specific hook
183    #[command(about = "Inspect a specific hook")]
184    Inspect {
185        /// Hook ID
186        #[arg(value_name = "ID")]
187        id: String,
188
189        /// Output format (table or json)
190        #[arg(short, long)]
191        format: Option<String>,
192    },
193
194    /// Enable a hook
195    #[command(about = "Enable a hook")]
196    Enable {
197        /// Hook ID
198        #[arg(value_name = "ID")]
199        id: String,
200    },
201
202    /// Disable a hook
203    #[command(about = "Disable a hook")]
204    Disable {
205        /// Hook ID
206        #[arg(value_name = "ID")]
207        id: String,
208    },
209
210    /// Delete a hook
211    #[command(about = "Delete a hook")]
212    Delete {
213        /// Hook ID
214        #[arg(value_name = "ID")]
215        id: String,
216    },
217}
218
219#[derive(Subcommand, Debug)]
220pub enum SessionsSubcommand {
221    /// List all sessions
222    #[command(about = "List all sessions")]
223    List,
224
225    /// Create a new session
226    #[command(about = "Create a new session")]
227    Create {
228        /// Session name
229        #[arg(value_name = "NAME")]
230        name: String,
231    },
232
233    /// Delete a session
234    #[command(about = "Delete a session")]
235    Delete {
236        /// Session ID
237        #[arg(value_name = "ID")]
238        id: String,
239    },
240
241    /// Rename a session
242    #[command(about = "Rename a session")]
243    Rename {
244        /// Session ID
245        #[arg(value_name = "ID")]
246        id: String,
247
248        /// New session name
249        #[arg(value_name = "NAME")]
250        name: String,
251    },
252
253    /// Switch to a session
254    #[command(about = "Switch to a session")]
255    Switch {
256        /// Session ID
257        #[arg(value_name = "ID")]
258        id: String,
259    },
260
261    /// Show session info
262    #[command(about = "Show session information")]
263    Info {
264        /// Session ID
265        #[arg(value_name = "ID")]
266        id: String,
267    },
268
269    /// Share a session with a shareable link
270    #[command(about = "Generate a shareable link for the current session")]
271    Share {
272        /// Expiration time in seconds (optional)
273        #[arg(long)]
274        expires_in: Option<u64>,
275
276        /// Exclude conversation history from share
277        #[arg(long)]
278        no_history: bool,
279
280        /// Exclude project context from share
281        #[arg(long)]
282        no_context: bool,
283    },
284
285    /// List all active shares
286    #[command(about = "List all active shares for the current user")]
287    ShareList,
288
289    /// Revoke a share
290    #[command(about = "Revoke a share by ID")]
291    ShareRevoke {
292        /// Share ID to revoke
293        #[arg(value_name = "SHARE_ID")]
294        share_id: String,
295    },
296
297    /// Show share information
298    #[command(about = "Show detailed information about a share")]
299    ShareInfo {
300        /// Share ID
301        #[arg(value_name = "SHARE_ID")]
302        share_id: String,
303    },
304
305    /// View a shared session
306    #[command(about = "View a shared session by share ID")]
307    ShareView {
308        /// Share ID to view
309        #[arg(value_name = "SHARE_ID")]
310        share_id: String,
311    },
312}
313
314#[derive(Subcommand, Debug)]
315pub enum CustomSubcommand {
316    /// List all available custom commands
317    #[command(about = "Display all available custom commands")]
318    List,
319
320    /// Show info for a specific custom command
321    #[command(about = "Show info for a specific custom command")]
322    Info {
323        /// Command name
324        #[arg(value_name = "NAME")]
325        name: String,
326    },
327
328    /// Execute a custom command
329    #[command(about = "Execute a custom command")]
330    Run {
331        /// Command name
332        #[arg(value_name = "NAME")]
333        name: String,
334
335        /// Arguments to pass to the command
336        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
337        args: Vec<String>,
338    },
339
340    /// Load custom commands from a file
341    #[command(about = "Load custom commands from a JSON or Markdown file")]
342    Load {
343        /// Path to command definition file
344        #[arg(value_name = "FILE")]
345        file: String,
346    },
347
348    /// Search for custom commands
349    #[command(about = "Search for custom commands by name or description")]
350    Search {
351        /// Search query
352        #[arg(value_name = "QUERY")]
353        query: String,
354    },
355}
356
357#[derive(Subcommand, Debug)]
358pub enum ConfigSubcommand {
359    /// List all configuration values
360    #[command(about = "Display all configuration settings")]
361    List,
362
363    /// Get a specific configuration value
364    #[command(about = "Get a configuration value by key")]
365    Get {
366        /// Configuration key (e.g., provider.default, storage.mode)
367        #[arg(value_name = "KEY")]
368        key: String,
369    },
370
371    /// Set a configuration value
372    #[command(about = "Set a configuration value")]
373    Set {
374        /// Configuration key
375        #[arg(value_name = "KEY")]
376        key: String,
377
378        /// Configuration value
379        #[arg(value_name = "VALUE")]
380        value: String,
381    },
382}
383
384/// Route and execute commands
385pub struct CommandRouter;
386
387impl CommandRouter {
388    /// Parse CLI arguments and route to appropriate handler
389    pub fn route() -> CliResult<()> {
390        let cli = Cli::parse();
391
392        // Initialize logging based on CLI flags
393        crate::logging::init_logging(cli.verbose, cli.quiet);
394
395        Self::execute(&cli)
396    }
397
398    /// Execute a command
399    pub fn execute(cli: &Cli) -> CliResult<()> {
400        match &cli.command {
401            Commands::Init { path } => {
402                let cmd = InitCommand::new(path.clone());
403                cmd.execute()
404            }
405            Commands::Gen { spec } => {
406                let cmd = GenCommand::new(spec.clone());
407                cmd.execute()
408            }
409            Commands::Chat {
410                message,
411                provider,
412                model,
413            } => {
414                let cmd = ChatCommand::new(message.clone(), provider.clone(), model.clone());
415                cmd.execute()
416            }
417            Commands::Refactor { file } => {
418                let cmd = RefactorCommand::new(file.clone());
419                cmd.execute()
420            }
421            Commands::Review { file } => {
422                let cmd = ReviewCommand::new(file.clone());
423                cmd.execute()
424            }
425            Commands::Config { action } => {
426                let config_action = match action {
427                    Some(ConfigSubcommand::List) | None => config::ConfigAction::List,
428                    Some(ConfigSubcommand::Get { key }) => config::ConfigAction::Get(key.clone()),
429                    Some(ConfigSubcommand::Set { key, value }) => {
430                        config::ConfigAction::Set(key.clone(), value.clone())
431                    }
432                };
433                let cmd = ConfigCommand::new(config_action);
434                cmd.execute()
435            }
436            Commands::Completions { shell } => {
437                crate::completion::generate_completions(shell).map_err(CliError::Internal)
438            }
439            Commands::Custom { action } => {
440                let custom_action = match action {
441                    Some(CustomSubcommand::List) | None => custom::CustomAction::List,
442                    Some(CustomSubcommand::Info { name }) => {
443                        custom::CustomAction::Info(name.clone())
444                    }
445                    Some(CustomSubcommand::Run { name, args }) => {
446                        custom::CustomAction::Run(name.clone(), args.clone())
447                    }
448                    Some(CustomSubcommand::Load { file }) => {
449                        custom::CustomAction::Load(file.clone())
450                    }
451                    Some(CustomSubcommand::Search { query }) => {
452                        custom::CustomAction::Search(query.clone())
453                    }
454                };
455                let cmd = custom::CustomCommandHandler::new(custom_action);
456                cmd.execute()
457            }
458            Commands::Tui {
459                theme,
460                vim_mode,
461                config,
462                provider,
463                model,
464            } => {
465                let config_path = config.as_ref().map(std::path::PathBuf::from);
466                let cmd = TuiCommand::new(
467                    theme.clone(),
468                    *vim_mode,
469                    config_path,
470                    provider.clone(),
471                    model.clone(),
472                );
473                cmd.execute()
474            }
475            Commands::Sessions { action } => {
476                let sessions_action = match action {
477                    Some(SessionsSubcommand::List) | None => sessions::SessionsAction::List,
478                    Some(SessionsSubcommand::Create { name }) => {
479                        sessions::SessionsAction::Create { name: name.clone() }
480                    }
481                    Some(SessionsSubcommand::Delete { id }) => {
482                        sessions::SessionsAction::Delete { id: id.clone() }
483                    }
484                    Some(SessionsSubcommand::Rename { id, name }) => {
485                        sessions::SessionsAction::Rename {
486                            id: id.clone(),
487                            name: name.clone(),
488                        }
489                    }
490                    Some(SessionsSubcommand::Switch { id }) => {
491                        sessions::SessionsAction::Switch { id: id.clone() }
492                    }
493                    Some(SessionsSubcommand::Info { id }) => {
494                        sessions::SessionsAction::Info { id: id.clone() }
495                    }
496                    Some(SessionsSubcommand::Share {
497                        expires_in,
498                        no_history,
499                        no_context,
500                    }) => sessions::SessionsAction::Share {
501                        expires_in: *expires_in,
502                        no_history: *no_history,
503                        no_context: *no_context,
504                    },
505                    Some(SessionsSubcommand::ShareList) => sessions::SessionsAction::ShareList,
506                    Some(SessionsSubcommand::ShareRevoke { share_id }) => {
507                        sessions::SessionsAction::ShareRevoke {
508                            share_id: share_id.clone(),
509                        }
510                    }
511                    Some(SessionsSubcommand::ShareInfo { share_id }) => {
512                        sessions::SessionsAction::ShareInfo {
513                            share_id: share_id.clone(),
514                        }
515                    }
516                    Some(SessionsSubcommand::ShareView { share_id }) => {
517                        sessions::SessionsAction::ShareView {
518                            share_id: share_id.clone(),
519                        }
520                    }
521                };
522                let cmd = SessionsCommand::new(sessions_action);
523                cmd.execute()
524            }
525            Commands::Lsp {
526                log_level,
527                port,
528                debug,
529            } => {
530                let cmd = lsp::LspCommand::new(log_level.clone(), *port, *debug);
531                cmd.execute()
532            }
533            Commands::Hooks { action } => {
534                let hooks_action = match action {
535                    Some(HooksSubcommand::List { format }) => hooks::HooksAction::List {
536                        format: format.clone(),
537                    },
538                    None => hooks::HooksAction::List { format: None },
539                    Some(HooksSubcommand::Inspect { id, format }) => hooks::HooksAction::Inspect {
540                        id: id.clone(),
541                        format: format.clone(),
542                    },
543                    Some(HooksSubcommand::Enable { id }) => {
544                        hooks::HooksAction::Enable { id: id.clone() }
545                    }
546                    Some(HooksSubcommand::Disable { id }) => {
547                        hooks::HooksAction::Disable { id: id.clone() }
548                    }
549                    Some(HooksSubcommand::Delete { id }) => {
550                        hooks::HooksAction::Delete { id: id.clone() }
551                    }
552                };
553                let cmd = hooks::HooksCommand::new(hooks_action);
554                cmd.execute()
555            }
556            Commands::Help { topic } => {
557                let cmd = HelpCommand::new(topic.clone());
558                cmd.execute()
559            }
560        }
561    }
562
563    /// Find similar command for suggestions
564    pub fn find_similar(command: &str) -> Option<String> {
565        let commands = ["init", "gen", "chat", "refactor", "review", "config", "tui"];
566
567        // Simple similarity check: commands that start with same letter
568        commands
569            .iter()
570            .find(|c| c.starts_with(&command[0..1.min(command.len())]))
571            .map(|s| s.to_string())
572    }
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578
579    #[test]
580    fn test_find_similar_command() {
581        assert_eq!(CommandRouter::find_similar("i"), Some("init".to_string()));
582        assert_eq!(CommandRouter::find_similar("g"), Some("gen".to_string()));
583        assert_eq!(CommandRouter::find_similar("c"), Some("chat".to_string()));
584    }
585}