1use crate::commands::*;
5use crate::error::{CliError, CliResult};
6use clap::{Parser, Subcommand};
7
8#[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 #[arg(short, long, global = true)]
26 pub verbose: bool,
27
28 #[arg(short, long, global = true)]
30 pub quiet: bool,
31
32 #[arg(long, global = true)]
34 pub dry_run: bool,
35}
36
37#[derive(Subcommand, Debug)]
38pub enum Commands {
39 #[command(about = "Initialize a new ricecoder project with default configuration")]
41 Init {
42 #[arg(value_name = "PATH")]
44 path: Option<String>,
45 },
46
47 #[command(about = "Generate code from a specification file")]
49 Gen {
50 #[arg(value_name = "SPEC")]
52 spec: String,
53 },
54
55 #[command(about = "Enter interactive chat mode for free-form coding assistance")]
57 Chat {
58 #[arg(value_name = "MESSAGE")]
60 message: Option<String>,
61
62 #[arg(short, long)]
64 provider: Option<String>,
65
66 #[arg(short, long)]
68 model: Option<String>,
69 },
70
71 #[command(about = "Refactor existing code using AI assistance")]
73 Refactor {
74 #[arg(value_name = "FILE")]
76 file: String,
77 },
78
79 #[command(about = "Review code for improvements and best practices")]
81 Review {
82 #[arg(value_name = "FILE")]
84 file: String,
85 },
86
87 #[command(about = "View and manage ricecoder configuration")]
89 Config {
90 #[command(subcommand)]
91 action: Option<ConfigSubcommand>,
92 },
93
94 #[command(about = "Generate shell completion scripts")]
96 Completions {
97 #[arg(value_name = "SHELL")]
99 shell: String,
100 },
101
102 #[command(about = "Manage and execute custom commands")]
104 Custom {
105 #[command(subcommand)]
106 action: Option<CustomSubcommand>,
107 },
108
109 #[command(about = "Launch the beautiful terminal user interface")]
111 Tui {
112 #[arg(short, long)]
114 theme: Option<String>,
115
116 #[arg(long)]
118 vim_mode: bool,
119
120 #[arg(short, long)]
122 config: Option<String>,
123
124 #[arg(short, long)]
126 provider: Option<String>,
127
128 #[arg(short, long)]
130 model: Option<String>,
131 },
132
133 #[command(about = "Manage ricecoder sessions")]
135 Sessions {
136 #[command(subcommand)]
137 action: Option<SessionsSubcommand>,
138 },
139
140 #[command(about = "Start the Language Server Protocol server for IDE integration")]
142 Lsp {
143 #[arg(short, long, default_value = "info")]
145 log_level: Option<String>,
146
147 #[arg(short, long)]
149 port: Option<u16>,
150
151 #[arg(long)]
153 debug: bool,
154 },
155
156 #[command(about = "Manage hooks for event-driven automation")]
158 Hooks {
159 #[command(subcommand)]
160 action: Option<HooksSubcommand>,
161 },
162
163 #[command(about = "Show help, tutorials, and troubleshooting guides")]
165 Help {
166 #[arg(value_name = "TOPIC")]
168 topic: Option<String>,
169 },
170}
171
172#[derive(Subcommand, Debug)]
173pub enum HooksSubcommand {
174 #[command(about = "List all registered hooks")]
176 List {
177 #[arg(short, long)]
179 format: Option<String>,
180 },
181
182 #[command(about = "Inspect a specific hook")]
184 Inspect {
185 #[arg(value_name = "ID")]
187 id: String,
188
189 #[arg(short, long)]
191 format: Option<String>,
192 },
193
194 #[command(about = "Enable a hook")]
196 Enable {
197 #[arg(value_name = "ID")]
199 id: String,
200 },
201
202 #[command(about = "Disable a hook")]
204 Disable {
205 #[arg(value_name = "ID")]
207 id: String,
208 },
209
210 #[command(about = "Delete a hook")]
212 Delete {
213 #[arg(value_name = "ID")]
215 id: String,
216 },
217}
218
219#[derive(Subcommand, Debug)]
220pub enum SessionsSubcommand {
221 #[command(about = "List all sessions")]
223 List,
224
225 #[command(about = "Create a new session")]
227 Create {
228 #[arg(value_name = "NAME")]
230 name: String,
231 },
232
233 #[command(about = "Delete a session")]
235 Delete {
236 #[arg(value_name = "ID")]
238 id: String,
239 },
240
241 #[command(about = "Rename a session")]
243 Rename {
244 #[arg(value_name = "ID")]
246 id: String,
247
248 #[arg(value_name = "NAME")]
250 name: String,
251 },
252
253 #[command(about = "Switch to a session")]
255 Switch {
256 #[arg(value_name = "ID")]
258 id: String,
259 },
260
261 #[command(about = "Show session information")]
263 Info {
264 #[arg(value_name = "ID")]
266 id: String,
267 },
268
269 #[command(about = "Generate a shareable link for the current session")]
271 Share {
272 #[arg(long)]
274 expires_in: Option<u64>,
275
276 #[arg(long)]
278 no_history: bool,
279
280 #[arg(long)]
282 no_context: bool,
283 },
284
285 #[command(about = "List all active shares for the current user")]
287 ShareList,
288
289 #[command(about = "Revoke a share by ID")]
291 ShareRevoke {
292 #[arg(value_name = "SHARE_ID")]
294 share_id: String,
295 },
296
297 #[command(about = "Show detailed information about a share")]
299 ShareInfo {
300 #[arg(value_name = "SHARE_ID")]
302 share_id: String,
303 },
304
305 #[command(about = "View a shared session by share ID")]
307 ShareView {
308 #[arg(value_name = "SHARE_ID")]
310 share_id: String,
311 },
312}
313
314#[derive(Subcommand, Debug)]
315pub enum CustomSubcommand {
316 #[command(about = "Display all available custom commands")]
318 List,
319
320 #[command(about = "Show info for a specific custom command")]
322 Info {
323 #[arg(value_name = "NAME")]
325 name: String,
326 },
327
328 #[command(about = "Execute a custom command")]
330 Run {
331 #[arg(value_name = "NAME")]
333 name: String,
334
335 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
337 args: Vec<String>,
338 },
339
340 #[command(about = "Load custom commands from a JSON or Markdown file")]
342 Load {
343 #[arg(value_name = "FILE")]
345 file: String,
346 },
347
348 #[command(about = "Search for custom commands by name or description")]
350 Search {
351 #[arg(value_name = "QUERY")]
353 query: String,
354 },
355}
356
357#[derive(Subcommand, Debug)]
358pub enum ConfigSubcommand {
359 #[command(about = "Display all configuration settings")]
361 List,
362
363 #[command(about = "Get a configuration value by key")]
365 Get {
366 #[arg(value_name = "KEY")]
368 key: String,
369 },
370
371 #[command(about = "Set a configuration value")]
373 Set {
374 #[arg(value_name = "KEY")]
376 key: String,
377
378 #[arg(value_name = "VALUE")]
380 value: String,
381 },
382}
383
384pub struct CommandRouter;
386
387impl CommandRouter {
388 pub fn route() -> CliResult<()> {
390 let cli = Cli::parse();
391
392 crate::logging::init_logging(cli.verbose, cli.quiet);
394
395 Self::execute(&cli)
396 }
397
398 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 pub fn find_similar(command: &str) -> Option<String> {
565 let commands = ["init", "gen", "chat", "refactor", "review", "config", "tui"];
566
567 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}