use crate::commands::*;
use crate::error::{CliError, CliResult};
use clap::{Parser, Subcommand};
#[derive(Parser, Debug)]
#[command(name = "rice")]
#[command(bin_name = "rice")]
#[command(about = "Terminal-first, spec-driven coding assistant")]
#[command(
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"
)]
#[command(version)]
#[command(author = "RiceCoder Contributors")]
#[command(arg_required_else_help = true)]
#[command(disable_help_subcommand = true)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(short, long, global = true)]
pub verbose: bool,
#[arg(short, long, global = true)]
pub quiet: bool,
#[arg(long, global = true)]
pub dry_run: bool,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
#[command(about = "Initialize a new ricecoder project with default configuration")]
Init {
#[arg(value_name = "PATH")]
path: Option<String>,
},
#[command(about = "Generate code from a specification file")]
Gen {
#[arg(value_name = "SPEC")]
spec: String,
},
#[command(about = "Enter interactive chat mode for free-form coding assistance")]
Chat {
#[arg(value_name = "MESSAGE")]
message: Option<String>,
#[arg(short, long)]
provider: Option<String>,
#[arg(short, long)]
model: Option<String>,
},
#[command(about = "Refactor existing code using AI assistance")]
Refactor {
#[arg(value_name = "FILE")]
file: String,
},
#[command(about = "Review code for improvements and best practices")]
Review {
#[arg(value_name = "FILE")]
file: String,
},
#[command(about = "View and manage ricecoder configuration")]
Config {
#[command(subcommand)]
action: Option<ConfigSubcommand>,
},
#[command(about = "Generate shell completion scripts")]
Completions {
#[arg(value_name = "SHELL")]
shell: String,
},
#[command(about = "Manage and execute custom commands")]
Custom {
#[command(subcommand)]
action: Option<CustomSubcommand>,
},
#[command(about = "Launch the beautiful terminal user interface")]
Tui {
#[arg(short, long)]
theme: Option<String>,
#[arg(long)]
vim_mode: bool,
#[arg(short, long)]
config: Option<String>,
#[arg(short, long)]
provider: Option<String>,
#[arg(short, long)]
model: Option<String>,
},
#[command(about = "Manage ricecoder sessions")]
Sessions {
#[command(subcommand)]
action: Option<SessionsSubcommand>,
},
#[command(about = "Start the Language Server Protocol server for IDE integration")]
Lsp {
#[arg(short, long, default_value = "info")]
log_level: Option<String>,
#[arg(short, long)]
port: Option<u16>,
#[arg(long)]
debug: bool,
},
#[command(about = "Manage hooks for event-driven automation")]
Hooks {
#[command(subcommand)]
action: Option<HooksSubcommand>,
},
#[command(about = "Show help, tutorials, and troubleshooting guides")]
Help {
#[arg(value_name = "TOPIC")]
topic: Option<String>,
},
}
#[derive(Subcommand, Debug)]
pub enum HooksSubcommand {
#[command(about = "List all registered hooks")]
List {
#[arg(short, long)]
format: Option<String>,
},
#[command(about = "Inspect a specific hook")]
Inspect {
#[arg(value_name = "ID")]
id: String,
#[arg(short, long)]
format: Option<String>,
},
#[command(about = "Enable a hook")]
Enable {
#[arg(value_name = "ID")]
id: String,
},
#[command(about = "Disable a hook")]
Disable {
#[arg(value_name = "ID")]
id: String,
},
#[command(about = "Delete a hook")]
Delete {
#[arg(value_name = "ID")]
id: String,
},
}
#[derive(Subcommand, Debug)]
pub enum SessionsSubcommand {
#[command(about = "List all sessions")]
List,
#[command(about = "Create a new session")]
Create {
#[arg(value_name = "NAME")]
name: String,
},
#[command(about = "Delete a session")]
Delete {
#[arg(value_name = "ID")]
id: String,
},
#[command(about = "Rename a session")]
Rename {
#[arg(value_name = "ID")]
id: String,
#[arg(value_name = "NAME")]
name: String,
},
#[command(about = "Switch to a session")]
Switch {
#[arg(value_name = "ID")]
id: String,
},
#[command(about = "Show session information")]
Info {
#[arg(value_name = "ID")]
id: String,
},
#[command(about = "Generate a shareable link for the current session")]
Share {
#[arg(long)]
expires_in: Option<u64>,
#[arg(long)]
no_history: bool,
#[arg(long)]
no_context: bool,
},
#[command(about = "List all active shares for the current user")]
ShareList,
#[command(about = "Revoke a share by ID")]
ShareRevoke {
#[arg(value_name = "SHARE_ID")]
share_id: String,
},
#[command(about = "Show detailed information about a share")]
ShareInfo {
#[arg(value_name = "SHARE_ID")]
share_id: String,
},
#[command(about = "View a shared session by share ID")]
ShareView {
#[arg(value_name = "SHARE_ID")]
share_id: String,
},
}
#[derive(Subcommand, Debug)]
pub enum CustomSubcommand {
#[command(about = "Display all available custom commands")]
List,
#[command(about = "Show info for a specific custom command")]
Info {
#[arg(value_name = "NAME")]
name: String,
},
#[command(about = "Execute a custom command")]
Run {
#[arg(value_name = "NAME")]
name: String,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
#[command(about = "Load custom commands from a JSON or Markdown file")]
Load {
#[arg(value_name = "FILE")]
file: String,
},
#[command(about = "Search for custom commands by name or description")]
Search {
#[arg(value_name = "QUERY")]
query: String,
},
}
#[derive(Subcommand, Debug)]
pub enum ConfigSubcommand {
#[command(about = "Display all configuration settings")]
List,
#[command(about = "Get a configuration value by key")]
Get {
#[arg(value_name = "KEY")]
key: String,
},
#[command(about = "Set a configuration value")]
Set {
#[arg(value_name = "KEY")]
key: String,
#[arg(value_name = "VALUE")]
value: String,
},
}
pub struct CommandRouter;
impl CommandRouter {
pub fn route() -> CliResult<()> {
let cli = Cli::parse();
crate::logging::init_logging(cli.verbose, cli.quiet);
Self::execute(&cli)
}
pub fn execute(cli: &Cli) -> CliResult<()> {
match &cli.command {
Commands::Init { path } => {
let cmd = InitCommand::new(path.clone());
cmd.execute()
}
Commands::Gen { spec } => {
let cmd = GenCommand::new(spec.clone());
cmd.execute()
}
Commands::Chat {
message,
provider,
model,
} => {
let cmd = ChatCommand::new(message.clone(), provider.clone(), model.clone());
cmd.execute()
}
Commands::Refactor { file } => {
let cmd = RefactorCommand::new(file.clone());
cmd.execute()
}
Commands::Review { file } => {
let cmd = ReviewCommand::new(file.clone());
cmd.execute()
}
Commands::Config { action } => {
let config_action = match action {
Some(ConfigSubcommand::List) | None => config::ConfigAction::List,
Some(ConfigSubcommand::Get { key }) => config::ConfigAction::Get(key.clone()),
Some(ConfigSubcommand::Set { key, value }) => {
config::ConfigAction::Set(key.clone(), value.clone())
}
};
let cmd = ConfigCommand::new(config_action);
cmd.execute()
}
Commands::Completions { shell } => {
crate::completion::generate_completions(shell).map_err(CliError::Internal)
}
Commands::Custom { action } => {
let custom_action = match action {
Some(CustomSubcommand::List) | None => custom::CustomAction::List,
Some(CustomSubcommand::Info { name }) => {
custom::CustomAction::Info(name.clone())
}
Some(CustomSubcommand::Run { name, args }) => {
custom::CustomAction::Run(name.clone(), args.clone())
}
Some(CustomSubcommand::Load { file }) => {
custom::CustomAction::Load(file.clone())
}
Some(CustomSubcommand::Search { query }) => {
custom::CustomAction::Search(query.clone())
}
};
let cmd = custom::CustomCommandHandler::new(custom_action);
cmd.execute()
}
Commands::Tui {
theme,
vim_mode,
config,
provider,
model,
} => {
let config_path = config.as_ref().map(std::path::PathBuf::from);
let cmd = TuiCommand::new(
theme.clone(),
*vim_mode,
config_path,
provider.clone(),
model.clone(),
);
cmd.execute()
}
Commands::Sessions { action } => {
let sessions_action = match action {
Some(SessionsSubcommand::List) | None => sessions::SessionsAction::List,
Some(SessionsSubcommand::Create { name }) => {
sessions::SessionsAction::Create { name: name.clone() }
}
Some(SessionsSubcommand::Delete { id }) => {
sessions::SessionsAction::Delete { id: id.clone() }
}
Some(SessionsSubcommand::Rename { id, name }) => {
sessions::SessionsAction::Rename {
id: id.clone(),
name: name.clone(),
}
}
Some(SessionsSubcommand::Switch { id }) => {
sessions::SessionsAction::Switch { id: id.clone() }
}
Some(SessionsSubcommand::Info { id }) => {
sessions::SessionsAction::Info { id: id.clone() }
}
Some(SessionsSubcommand::Share {
expires_in,
no_history,
no_context,
}) => sessions::SessionsAction::Share {
expires_in: *expires_in,
no_history: *no_history,
no_context: *no_context,
},
Some(SessionsSubcommand::ShareList) => sessions::SessionsAction::ShareList,
Some(SessionsSubcommand::ShareRevoke { share_id }) => {
sessions::SessionsAction::ShareRevoke {
share_id: share_id.clone(),
}
}
Some(SessionsSubcommand::ShareInfo { share_id }) => {
sessions::SessionsAction::ShareInfo {
share_id: share_id.clone(),
}
}
Some(SessionsSubcommand::ShareView { share_id }) => {
sessions::SessionsAction::ShareView {
share_id: share_id.clone(),
}
}
};
let cmd = SessionsCommand::new(sessions_action);
cmd.execute()
}
Commands::Lsp {
log_level,
port,
debug,
} => {
let cmd = lsp::LspCommand::new(log_level.clone(), *port, *debug);
cmd.execute()
}
Commands::Hooks { action } => {
let hooks_action = match action {
Some(HooksSubcommand::List { format }) => hooks::HooksAction::List {
format: format.clone(),
},
None => hooks::HooksAction::List { format: None },
Some(HooksSubcommand::Inspect { id, format }) => hooks::HooksAction::Inspect {
id: id.clone(),
format: format.clone(),
},
Some(HooksSubcommand::Enable { id }) => {
hooks::HooksAction::Enable { id: id.clone() }
}
Some(HooksSubcommand::Disable { id }) => {
hooks::HooksAction::Disable { id: id.clone() }
}
Some(HooksSubcommand::Delete { id }) => {
hooks::HooksAction::Delete { id: id.clone() }
}
};
let cmd = hooks::HooksCommand::new(hooks_action);
cmd.execute()
}
Commands::Help { topic } => {
let cmd = HelpCommand::new(topic.clone());
cmd.execute()
}
}
}
pub fn find_similar(command: &str) -> Option<String> {
let commands = ["init", "gen", "chat", "refactor", "review", "config", "tui"];
commands
.iter()
.find(|c| c.starts_with(&command[0..1.min(command.len())]))
.map(|s| s.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_similar_command() {
assert_eq!(CommandRouter::find_similar("i"), Some("init".to_string()));
assert_eq!(CommandRouter::find_similar("g"), Some("gen".to_string()));
assert_eq!(CommandRouter::find_similar("c"), Some("chat".to_string()));
}
}