use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[clap(rename_all = "lower")]
pub enum ThinkingLevel {
Off,
Minimal,
Low,
Medium,
High,
XHigh,
}
impl std::fmt::Display for ThinkingLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ThinkingLevel::Off => write!(f, "off"),
ThinkingLevel::Minimal => write!(f, "minimal"),
ThinkingLevel::Low => write!(f, "low"),
ThinkingLevel::Medium => write!(f, "medium"),
ThinkingLevel::High => write!(f, "high"),
ThinkingLevel::XHigh => write!(f, "xhigh"),
}
}
}
impl FromStr for ThinkingLevel {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"off" => Ok(ThinkingLevel::Off),
"minimal" => Ok(ThinkingLevel::Minimal),
"low" => Ok(ThinkingLevel::Low),
"medium" => Ok(ThinkingLevel::Medium),
"high" => Ok(ThinkingLevel::High),
"xhigh" | "x-high" => Ok(ThinkingLevel::XHigh),
_ => Err(format!("Invalid thinking level: {}", s)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum OutputMode {
Text,
Json,
Rpc,
}
impl std::fmt::Display for OutputMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OutputMode::Text => write!(f, "text"),
OutputMode::Json => write!(f, "json"),
OutputMode::Rpc => write!(f, "rpc"),
}
}
}
#[derive(Debug, Clone, Parser)]
pub struct InstallArgs {
pub source: String,
#[arg(short = 'l', long)]
pub local: bool,
#[arg(short = 'g', long)]
pub global: bool,
}
#[derive(Debug, Clone, Parser)]
pub struct RemoveArgs {
pub source: String,
#[arg(short = 'l', long)]
pub local: bool,
}
#[derive(Debug, Clone, Parser)]
pub struct UpdateArgs {
pub source: Option<String>,
#[arg(short = 'a', long)]
pub all: bool,
#[arg(short = 'f', long)]
pub force: bool,
}
#[derive(Debug, Clone, Parser)]
pub struct ListArgs {
#[arg(long)]
pub extensions: bool,
#[arg(long)]
pub skills: bool,
#[arg(long)]
pub prompts: bool,
#[arg(long)]
pub themes: bool,
#[arg(long, short = 'a')]
pub include_disabled: bool,
}
#[derive(Debug, Clone, Subcommand)]
pub enum Commands {
Install(InstallArgs),
Remove(RemoveArgs),
Uninstall(RemoveArgs),
Update(UpdateArgs),
List(ListArgs),
Config,
}
#[derive(Debug, Clone, Parser)]
#[command(name = "oxi")]
#[command(about = "AI coding assistant with read, bash, edit, write tools")]
pub struct CliArgs {
#[arg(short, long)]
pub provider: Option<String>,
#[arg(short, long)]
pub model: Option<String>,
#[arg(long)]
pub api_key: Option<String>,
#[arg(long)]
pub system_prompt: Option<String>,
#[arg(long = "append-system-prompt")]
pub append_system_prompt: Vec<String>,
#[arg(long)]
pub thinking: Option<ThinkingLevel>,
#[arg(short = 'c', long)]
pub continue_session: bool,
#[arg(short = 'r', long)]
pub resume: bool,
#[arg(long)]
pub session: Option<String>,
#[arg(long)]
pub fork: Option<String>,
#[arg(long)]
pub session_dir: Option<PathBuf>,
#[arg(long)]
pub no_session: bool,
#[arg(long)]
pub models: Option<String>,
#[arg(long = "no-tools", short = 't')]
pub no_tools: bool,
#[arg(long = "no-builtin-tools")]
pub no_builtin_tools: bool,
#[arg(short = 'o', long)]
pub tools: Option<String>,
#[arg(long)]
pub print: bool,
#[arg(long)]
pub export: Option<PathBuf>,
#[arg(short = 'e', long)]
pub extension: Vec<PathBuf>,
#[arg(long)]
pub no_extensions: bool,
#[arg(long)]
pub skill: Vec<PathBuf>,
#[arg(long = "no-skills")]
pub no_skills: bool,
#[arg(long = "prompt-template")]
pub prompt_template: Vec<PathBuf>,
#[arg(long = "no-prompt-templates")]
pub no_prompt_templates: bool,
#[arg(long)]
pub theme: Vec<PathBuf>,
#[arg(long)]
pub no_themes: bool,
#[arg(long = "no-context-files")]
pub no_context_files: bool,
#[arg(long)]
pub list_models: Option<Option<String>>,
#[arg(long)]
pub verbose: bool,
#[arg(long)]
pub offline: bool,
#[command(subcommand)]
pub command: Option<Commands>,
pub messages: Vec<String>,
#[arg(long = "file", value_delimiter = ' ')]
pub file_args: Vec<PathBuf>,
}
pub fn parse_args() -> CliArgs {
CliArgs::parse()
}
pub fn parse_args_from<I, T>(iter: I) -> Result<CliArgs, clap::Error>
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
CliArgs::try_parse_from(iter)
}
pub fn is_stdin_piped() -> bool {
#[cfg(unix)]
{
use std::io::IsTerminal;
return !std::io::stdin().is_terminal();
}
#[cfg(not(unix))]
{
false
}
}
pub fn detect_print_mode() -> bool {
let args: Vec<String> = std::env::args().collect();
if args.iter().any(|a| a == "-p" || a == "--print") {
return true;
}
is_stdin_piped()
}
pub fn get_version() -> String {
let version = env!("CARGO_PKG_VERSION");
format!("{}", version)
}
pub fn generate_completion(shell: &str) -> String {
format!("# Shell completion for {} is not yet implemented.\n# Install clap_complete to enable this feature.", shell)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_basic_args() {
let args = parse_args_from(["oxi", "Hello", "world"]).unwrap();
assert_eq!(args.messages, vec!["Hello", "world"]);
}
#[test]
fn test_parse_with_provider_and_model() {
let args = parse_args_from([
"oxi",
"--provider",
"anthropic",
"--model",
"claude-sonnet-4-5",
"Hello",
])
.unwrap();
assert_eq!(args.provider, Some("anthropic".to_string()));
assert_eq!(args.model, Some("claude-sonnet-4-5".to_string()));
}
#[test]
fn test_parse_with_thinking_level() {
let args = parse_args_from(["oxi", "--thinking", "high", "Hello"]).unwrap();
assert_eq!(args.thinking, Some(ThinkingLevel::High));
}
#[test]
fn test_parse_with_tools() {
let args = parse_args_from(["oxi", "-o", "read,bash,edit", "Hello"]).unwrap();
assert_eq!(args.tools, Some("read,bash,edit".to_string()));
}
#[test]
fn test_parse_with_multiple_files() {
let args = parse_args_from(["oxi", "--file", "file1.txt", "--file", "file2.txt", "Hello"])
.unwrap();
assert_eq!(args.file_args.len(), 2);
}
#[test]
fn test_parse_print_mode() {
let args = parse_args_from(["oxi", "--print", "Hello"]).unwrap();
assert!(args.print);
}
#[test]
fn test_parse_resume_flag() {
let args = parse_args_from(["oxi", "-r"]).unwrap();
assert!(args.resume);
}
#[test]
fn test_parse_continue_flag() {
let args = parse_args_from(["oxi", "-c"]).unwrap();
assert!(args.continue_session);
}
#[test]
fn test_parse_subcommand() {
let args = parse_args_from(["oxi", "config"]).unwrap();
assert!(matches!(args.command, Some(Commands::Config)));
}
#[test]
fn test_parse_install_command() {
let args =
parse_args_from(["oxi", "install", "git:https://github.com/example/ext"]).unwrap();
match args.command {
Some(Commands::Install(install_args)) => {
assert_eq!(install_args.source, "git:https://github.com/example/ext");
}
_ => panic!("Expected Install command"),
}
}
#[test]
fn test_parse_remove_command() {
let args = parse_args_from(["oxi", "remove", "example-ext"]).unwrap();
match args.command {
Some(Commands::Remove(remove_args)) => {
assert_eq!(remove_args.source, "example-ext");
}
_ => panic!("Expected Remove command"),
}
}
#[test]
fn test_parse_update_command() {
let args = parse_args_from(["oxi", "update", "self"]).unwrap();
match args.command {
Some(Commands::Update(update_args)) => {
assert_eq!(update_args.source, Some("self".to_string()));
}
_ => panic!("Expected Update command"),
}
}
#[test]
fn test_parse_list_command() {
let args = parse_args_from(["oxi", "list", "--extensions"]).unwrap();
match args.command {
Some(Commands::List(list_args)) => {
assert!(list_args.extensions);
}
_ => panic!("Expected List command"),
}
}
#[test]
fn test_thinking_level_from_str() {
assert_eq!(
"high".parse::<ThinkingLevel>().unwrap(),
ThinkingLevel::High
);
assert_eq!("off".parse::<ThinkingLevel>().unwrap(), ThinkingLevel::Off);
assert_eq!(
"xhigh".parse::<ThinkingLevel>().unwrap(),
ThinkingLevel::XHigh
);
assert!("invalid".parse::<ThinkingLevel>().is_err());
}
#[test]
fn test_thinking_level_display() {
assert_eq!(ThinkingLevel::High.to_string(), "high");
assert_eq!(ThinkingLevel::XHigh.to_string(), "xhigh");
}
}