use clap::{Parser, Subcommand};
use std::path::PathBuf;
pub use oxi_store::settings::ThinkingLevel;
#[derive(Debug, Clone, Parser)]
#[command(name = "oxi")]
#[command(about = "CLI coding harness for oxi")]
#[command(version)]
pub struct CliArgs {
#[command(subcommand)]
pub command: Option<Commands>,
#[arg(short, long)]
pub provider: Option<String>,
#[arg(short, long)]
pub model: Option<String>,
#[arg(default_value = "")]
pub prompt: Vec<String>,
#[arg(short, long)]
pub interactive: bool,
#[arg(long)]
pub thinking: Option<String>,
#[arg(short = 'e', long = "extension", value_name = "PATH")]
pub extensions: Vec<PathBuf>,
#[arg(long)]
pub mode: Option<String>,
#[arg(long)]
pub tools: Option<String>,
#[arg(long)]
pub append_system_prompt: Option<PathBuf>,
#[arg(long)]
pub print: bool,
#[arg(long)]
pub no_session: bool,
#[arg(long)]
pub timeout: Option<u64>,
#[arg(short, long)]
pub continue_session: bool,
}
#[derive(Debug, Clone, Subcommand)]
pub enum Commands {
Sessions,
Tree {
#[arg(default_value = "")]
session_id: String,
},
Fork {
parent_id: String,
entry_id: String,
},
Delete {
session_id: String,
},
Pkg {
#[command(subcommand)]
action: PkgCommands,
},
Config {
#[command(subcommand)]
action: ConfigCommands,
},
Ext {
#[command(subcommand)]
action: ExtCommands,
},
Models {
#[arg(long)]
provider: Option<String>,
},
Setup {
#[arg(long)]
reset: bool,
},
}
#[derive(Debug, Clone, Subcommand)]
pub enum PkgCommands {
Install {
source: String,
},
List,
Uninstall {
name: String,
},
Update {
name: Option<String>,
},
}
#[derive(Debug, Clone, Subcommand)]
pub enum ExtCommands {
Install {
source: String,
#[arg(long)]
prerelease: bool,
},
List,
Remove {
name: String,
},
Update {
name: Option<String>,
},
Info {
source: String,
},
}
#[derive(Debug, Clone, Subcommand)]
pub enum ConfigCommands {
Show,
List {
resource_type: Option<String>,
},
Enable {
resource_type: String,
name: String,
},
Disable {
resource_type: String,
name: String,
},
Set {
key: String,
value: String,
},
Get {
key: String,
},
AddProvider {
name: String,
base_url: String,
api_key_env: String,
#[arg(default_value = "openai-completions")]
api: String,
},
RemoveProvider {
name: String,
},
Reset {
#[arg(long, short)]
all: bool,
},
}
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)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_basic_prompt() {
let args = parse_args_from(["oxi", "Hello", "world"]).unwrap();
assert_eq!(args.prompt, vec!["Hello", "world"]);
}
#[test]
fn test_parse_with_provider_and_model() {
let args = parse_args_from([
"oxi",
"--provider",
"anthropic",
"--model",
"claude-sonnet-4-20250514",
"Hello",
])
.unwrap();
assert_eq!(args.provider, Some("anthropic".to_string()));
assert_eq!(args.model, Some("claude-sonnet-4-20250514".to_string()));
}
#[test]
fn test_parse_interactive_flag() {
let args = parse_args_from(["oxi", "-i"]).unwrap();
assert!(args.interactive);
}
#[test]
fn test_parse_extension_paths() {
let args =
parse_args_from(["oxi", "-e", "/path/to/ext.so", "-e", "/other/ext.so"]).unwrap();
assert_eq!(args.extensions.len(), 2);
}
#[test]
fn test_parse_sessions_command() {
let args = parse_args_from(["oxi", "sessions"]).unwrap();
assert!(matches!(args.command, Some(Commands::Sessions)));
}
#[test]
fn test_parse_tree_command() {
let args = parse_args_from(["oxi", "tree", "abc-123"]).unwrap();
match args.command {
Some(Commands::Tree { session_id }) => {
assert_eq!(session_id, "abc-123");
}
_ => panic!("Expected Tree command"),
}
}
#[test]
fn test_parse_tree_command_default() {
let args = parse_args_from(["oxi", "tree"]).unwrap();
match args.command {
Some(Commands::Tree { session_id }) => {
assert_eq!(session_id, "");
}
_ => panic!("Expected Tree command"),
}
}
#[test]
fn test_parse_fork_command() {
let args = parse_args_from(["oxi", "fork", "parent-id", "entry-id"]).unwrap();
match args.command {
Some(Commands::Fork {
parent_id,
entry_id,
}) => {
assert_eq!(parent_id, "parent-id");
assert_eq!(entry_id, "entry-id");
}
_ => panic!("Expected Fork command"),
}
}
#[test]
fn test_parse_delete_command() {
let args = parse_args_from(["oxi", "delete", "session-123"]).unwrap();
match args.command {
Some(Commands::Delete { session_id }) => {
assert_eq!(session_id, "session-123");
}
_ => panic!("Expected Delete command"),
}
}
#[test]
fn test_parse_pkg_install() {
let args = parse_args_from(["oxi", "pkg", "install", "npm:@scope/name"]).unwrap();
match args.command {
Some(Commands::Pkg { action }) => match action {
PkgCommands::Install { source } => {
assert_eq!(source, "npm:@scope/name");
}
_ => panic!("Expected Install subcommand"),
},
_ => panic!("Expected Pkg command"),
}
}
#[test]
fn test_parse_pkg_list() {
let args = parse_args_from(["oxi", "pkg", "list"]).unwrap();
match args.command {
Some(Commands::Pkg { action }) => {
assert!(matches!(action, PkgCommands::List));
}
_ => panic!("Expected Pkg command"),
}
}
#[test]
fn test_parse_pkg_update_all() {
let args = parse_args_from(["oxi", "pkg", "update"]).unwrap();
match args.command {
Some(Commands::Pkg { action }) => match action {
PkgCommands::Update { name } => assert!(name.is_none()),
_ => panic!("Expected Update subcommand"),
},
_ => panic!("Expected Pkg command"),
}
}
#[test]
fn test_parse_pkg_update_named() {
let args = parse_args_from(["oxi", "pkg", "update", "my-pkg"]).unwrap();
match args.command {
Some(Commands::Pkg { action }) => match action {
PkgCommands::Update { name } => assert_eq!(name, Some("my-pkg".to_string())),
_ => panic!("Expected Update subcommand"),
},
_ => panic!("Expected Pkg command"),
}
}
#[test]
fn test_parse_config_show() {
let args = parse_args_from(["oxi", "config", "show"]).unwrap();
assert!(matches!(
args.command,
Some(Commands::Config {
action: ConfigCommands::Show
})
));
}
#[test]
fn test_parse_config_set() {
let args = parse_args_from(["oxi", "config", "set", "theme", "dracula"]).unwrap();
match args.command {
Some(Commands::Config { action }) => match action {
ConfigCommands::Set { key, value } => {
assert_eq!(key, "theme");
assert_eq!(value, "dracula");
}
_ => panic!("Expected Set subcommand"),
},
_ => panic!("Expected Config command"),
}
}
#[test]
fn test_parse_config_get() {
let args = parse_args_from(["oxi", "config", "get", "theme"]).unwrap();
match args.command {
Some(Commands::Config { action }) => match action {
ConfigCommands::Get { key } => {
assert_eq!(key, "theme");
}
_ => panic!("Expected Get subcommand"),
},
_ => panic!("Expected Config command"),
}
}
#[test]
fn test_parse_config_enable() {
let args = parse_args_from(["oxi", "config", "enable", "extension", "my-ext"]).unwrap();
match args.command {
Some(Commands::Config { action }) => match action {
ConfigCommands::Enable {
resource_type,
name,
} => {
assert_eq!(resource_type, "extension");
assert_eq!(name, "my-ext");
}
_ => panic!("Expected Enable subcommand"),
},
_ => panic!("Expected Config command"),
}
}
#[test]
fn test_parse_config_disable() {
let args = parse_args_from(["oxi", "config", "disable", "skill", "my-skill"]).unwrap();
match args.command {
Some(Commands::Config { action }) => match action {
ConfigCommands::Disable {
resource_type,
name,
} => {
assert_eq!(resource_type, "skill");
assert_eq!(name, "my-skill");
}
_ => panic!("Expected Disable subcommand"),
},
_ => panic!("Expected Config command"),
}
}
#[test]
fn test_parse_config_list() {
let args = parse_args_from(["oxi", "config", "list"]).unwrap();
match args.command {
Some(Commands::Config { action }) => match action {
ConfigCommands::List { resource_type } => {
assert!(resource_type.is_none());
}
_ => panic!("Expected List subcommand"),
},
_ => panic!("Expected Config command"),
}
}
#[test]
fn test_parse_config_list_filtered() {
let args = parse_args_from(["oxi", "config", "list", "extensions"]).unwrap();
match args.command {
Some(Commands::Config { action }) => match action {
ConfigCommands::List { resource_type } => {
assert_eq!(resource_type, Some("extensions".to_string()));
}
_ => panic!("Expected List subcommand"),
},
_ => panic!("Expected Config command"),
}
}
#[test]
fn test_thinking_level_reexport() {
assert_eq!(format!("{:?}", ThinkingLevel::Medium), "Medium");
}
#[test]
fn test_parse_config_add_provider() {
let args = parse_args_from([
"oxi",
"config",
"add-provider",
"minimax",
"https://api.minimax.chat/v1",
"MINIMAX_API_KEY",
"openai-completions",
])
.unwrap();
match args.command {
Some(Commands::Config { action }) => match action {
ConfigCommands::AddProvider {
name,
base_url,
api_key_env,
api,
} => {
assert_eq!(name, "minimax");
assert_eq!(base_url, "https://api.minimax.chat/v1");
assert_eq!(api_key_env, "MINIMAX_API_KEY");
assert_eq!(api, "openai-completions");
}
_ => panic!("Expected AddProvider subcommand"),
},
_ => panic!("Expected Config command"),
}
}
#[test]
fn test_parse_config_add_provider_default_api() {
let args = parse_args_from([
"oxi",
"config",
"add-provider",
"zai",
"https://api.z.ai/v1",
"ZAI_API_KEY",
])
.unwrap();
match args.command {
Some(Commands::Config { action }) => match action {
ConfigCommands::AddProvider {
name,
base_url,
api_key_env,
api,
} => {
assert_eq!(name, "zai");
assert_eq!(base_url, "https://api.z.ai/v1");
assert_eq!(api_key_env, "ZAI_API_KEY");
assert_eq!(api, "openai-completions"); }
_ => panic!("Expected AddProvider subcommand"),
},
_ => panic!("Expected Config command"),
}
}
#[test]
fn test_parse_config_remove_provider() {
let args = parse_args_from(["oxi", "config", "remove-provider", "minimax"]).unwrap();
match args.command {
Some(Commands::Config { action }) => match action {
ConfigCommands::RemoveProvider { name } => {
assert_eq!(name, "minimax");
}
_ => panic!("Expected RemoveProvider subcommand"),
},
_ => panic!("Expected Config command"),
}
}
#[test]
fn test_parse_models_command() {
let args = parse_args_from(["oxi", "models"]).unwrap();
match args.command {
Some(Commands::Models { provider }) => {
assert!(provider.is_none());
}
_ => panic!("Expected Models command"),
}
}
#[test]
fn test_parse_models_with_provider() {
let args = parse_args_from(["oxi", "models", "--provider", "minimax"]).unwrap();
match args.command {
Some(Commands::Models { provider }) => {
assert_eq!(provider, Some("minimax".to_string()));
}
_ => panic!("Expected Models command"),
}
}
#[test]
fn test_parse_setup_command() {
let args = parse_args_from(["oxi", "setup"]).unwrap();
match args.command {
Some(Commands::Setup { reset }) => {
assert!(!reset);
}
_ => panic!("Expected Setup command"),
}
}
#[test]
fn test_parse_setup_reset() {
let args = parse_args_from(["oxi", "setup", "--reset"]).unwrap();
match args.command {
Some(Commands::Setup { reset }) => {
assert!(reset);
}
_ => panic!("Expected Setup command with reset"),
}
}
}