use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "claux")]
#[command(about = "claux — an open, hackable terminal AI coding assistant in Rust")]
pub struct Cli {
#[command(subcommand)]
pub command: Option<CliCommand>,
#[arg(short = 'p', long = "print")]
pub prompt: Option<String>,
#[arg(long, value_name = "FILE", requires = "prompt")]
pub image: Vec<PathBuf>,
#[arg(long, value_enum, requires = "prompt")]
pub output_format: Option<OutputFormat>,
#[arg(long, value_name = "FILE", requires = "prompt")]
pub transcript: Option<PathBuf>,
#[arg(long)]
pub model: Option<String>,
#[arg(long)]
pub resume: Option<String>,
#[arg(long)]
pub permission_mode: Option<String>,
#[arg(long)]
pub trust_project: bool,
#[arg(short, long)]
pub verbose: bool,
#[arg(long)]
pub debug: bool,
#[arg(long)]
pub tui: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
pub enum OutputFormat {
#[default]
Text,
Json,
}
#[derive(Subcommand)]
pub enum CliCommand {
Auth {
#[command(subcommand)]
command: AuthCommand,
},
Doctor {
#[arg(long)]
offline: bool,
},
#[command(name = "tokenizer-fingerprint")]
TokenizerFingerprint {
#[arg(required = true, num_args = 2..)]
models: Vec<String>,
#[arg(long, value_enum, conflicts_with = "json")]
format: Option<TokenizerOutputFormat>,
#[arg(long, conflicts_with = "format")]
json: bool,
},
Config {
#[command(subcommand)]
command: ConfigCommand,
},
#[command(name = "__sandbox-exec", hide = true)]
SandboxExec {
#[arg(long)]
workspace: PathBuf,
#[arg(long)]
command: String,
},
#[command(name = "__sandbox-probe", hide = true)]
SandboxProbe,
}
#[derive(Subcommand)]
pub enum AuthCommand {
Login {
#[arg(value_enum)]
provider: AuthProvider,
#[arg(long)]
headless: bool,
#[arg(long)]
no_browser: bool,
},
Status {
#[arg(value_enum)]
provider: AuthProvider,
},
Logout {
#[arg(value_enum)]
provider: AuthProvider,
},
#[command(hide = true)]
Token {
#[arg(value_enum)]
provider: AuthProvider,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum AuthProvider {
#[value(name = "openrouter")]
OpenRouter,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
pub enum TokenizerOutputFormat {
#[default]
Text,
Json,
Markdown,
}
#[derive(Subcommand)]
pub enum ConfigCommand {
Init {
#[arg(long, value_enum, default_value_t = ConfigProvider::Anthropic)]
provider: ConfigProvider,
#[arg(long)]
model: Option<String>,
#[arg(long)]
force: bool,
},
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum ConfigProvider {
Anthropic,
Openai,
#[value(name = "openrouter")]
OpenRouter,
Ollama,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_doctor_offline() {
let cli = Cli::try_parse_from(["claux", "doctor", "--offline"]).unwrap();
assert!(matches!(
cli.command,
Some(CliCommand::Doctor { offline: true })
));
}
#[test]
fn parses_tokenizer_fingerprint() {
let cli = Cli::try_parse_from([
"claux",
"tokenizer-fingerprint",
"stealth/ox-alpha",
"z-ai/glm-5.3",
"--json",
])
.unwrap();
assert!(matches!(
cli.command,
Some(CliCommand::TokenizerFingerprint {
models,
format: None,
json: true
})
if models == ["stealth/ox-alpha", "z-ai/glm-5.3"]
));
}
#[test]
fn parses_markdown_tokenizer_fingerprint() {
let cli = Cli::try_parse_from([
"claux",
"tokenizer-fingerprint",
"stealth/ox-alpha",
"z-ai/glm-5.3",
"--format",
"markdown",
])
.unwrap();
assert!(matches!(
cli.command,
Some(CliCommand::TokenizerFingerprint {
format: Some(TokenizerOutputFormat::Markdown),
json: false,
..
})
));
}
#[test]
fn parses_headless_openrouter_login() {
let cli = Cli::try_parse_from([
"claux",
"auth",
"login",
"openrouter",
"--headless",
"--no-browser",
])
.unwrap();
assert!(matches!(
cli.command,
Some(CliCommand::Auth {
command: AuthCommand::Login {
provider: AuthProvider::OpenRouter,
headless: true,
no_browser: true,
},
})
));
}
#[test]
fn parses_json_output_for_one_shot_mode() {
let cli =
Cli::try_parse_from(["claux", "--print", "hello", "--output-format", "json"]).unwrap();
assert_eq!(cli.output_format, Some(OutputFormat::Json));
}
#[test]
fn parses_transcript_for_one_shot_mode() {
let cli = Cli::try_parse_from([
"claux",
"--print",
"hello",
"--transcript",
"/tmp/claux-transcript.json",
])
.unwrap();
assert_eq!(
cli.transcript,
Some(PathBuf::from("/tmp/claux-transcript.json"))
);
}
#[test]
fn parses_repeated_images_for_one_shot_mode() {
let cli = Cli::try_parse_from([
"claux",
"--print",
"describe these",
"--image",
"one.png",
"--image",
"two.jpg",
])
.unwrap();
assert_eq!(
cli.image,
vec![PathBuf::from("one.png"), PathBuf::from("two.jpg")]
);
}
#[test]
fn image_requires_one_shot_mode() {
let error = match Cli::try_parse_from(["claux", "--image", "one.png"]) {
Ok(_) => panic!("image should require one-shot mode"),
Err(error) => error,
};
assert_eq!(
error.kind(),
clap::error::ErrorKind::MissingRequiredArgument
);
}
#[test]
fn transcript_requires_one_shot_mode() {
let error =
match Cli::try_parse_from(["claux", "--transcript", "/tmp/claux-transcript.json"]) {
Ok(_) => panic!("transcript should require one-shot mode"),
Err(error) => error,
};
assert_eq!(
error.kind(),
clap::error::ErrorKind::MissingRequiredArgument
);
}
#[test]
fn output_format_requires_one_shot_mode() {
let error = match Cli::try_parse_from(["claux", "--output-format", "json"]) {
Ok(_) => panic!("output format should require one-shot mode"),
Err(error) => error,
};
assert_eq!(
error.kind(),
clap::error::ErrorKind::MissingRequiredArgument
);
}
#[test]
fn parses_config_init_provider() {
let cli = Cli::try_parse_from([
"claux",
"config",
"init",
"--provider",
"ollama",
"--model",
"local-coder",
])
.unwrap();
assert!(matches!(
cli.command,
Some(CliCommand::Config {
command: ConfigCommand::Init {
provider: ConfigProvider::Ollama,
model: Some(ref model),
force: false,
},
}) if model == "local-coder"
));
}
#[test]
fn parses_openrouter_config_init() {
let cli = Cli::try_parse_from([
"claux",
"config",
"init",
"--provider",
"openrouter",
"--model",
"anthropic/claude-sonnet-5",
])
.unwrap();
assert!(matches!(
cli.command,
Some(CliCommand::Config {
command: ConfigCommand::Init {
provider: ConfigProvider::OpenRouter,
model: Some(ref model),
force: false,
},
}) if model == "anthropic/claude-sonnet-5"
));
}
}