use clap::Parser;
#[derive(Debug, Parser)]
#[command(name = "capo", version, about = "Capo — a Rust coding agent")]
pub struct Cli {
#[arg(short = 'p', long = "prompt")]
pub prompt: Option<String>,
#[arg(long)]
pub model: Option<String>,
#[arg(long = "provider")]
pub provider: Option<String>,
#[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
pub verbosity: u8,
#[arg(short = 'n', long = "no-context-files")]
pub no_context_files: bool,
#[arg(long = "no-skills")]
pub no_skills: bool,
#[arg(short = 'c', long = "continue")]
pub continue_session: bool,
#[arg(long = "session")]
pub session: Option<String>,
#[arg(short = 'r', long = "resume")]
pub resume: bool,
#[arg(long = "json", requires = "prompt")]
pub json: bool,
#[arg(long = "dangerously-allow-all")]
pub dangerously_allow_all: bool,
#[arg(long = "rpc", conflicts_with = "prompt")]
pub rpc: bool,
}
pub enum Mode {
Print(String),
Json(String),
Rpc,
InteractiveStub,
}
impl Cli {
pub fn mode(&self) -> Mode {
match (&self.prompt, self.json, self.rpc) {
(_, _, true) => Mode::Rpc,
(Some(p), true, false) => Mode::Json(p.clone()),
(Some(p), false, false) => Mode::Print(p.clone()),
(None, _, false) => Mode::InteractiveStub,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn print_mode_flag() {
let cli = Cli::parse_from(["capo", "-p", "hello"]);
assert!(matches!(cli.mode(), Mode::Print(s) if s == "hello"));
}
#[test]
fn model_flag() {
let cli = Cli::parse_from(["capo", "-p", "x", "--model", "claude-opus-4-7"]);
assert_eq!(cli.model.as_deref(), Some("claude-opus-4-7"));
}
#[test]
fn no_context_files_flag() {
let cli = Cli::parse_from(["capo", "-p", "x", "-n"]);
assert!(cli.no_context_files);
let cli = Cli::parse_from(["capo", "-p", "x"]);
assert!(!cli.no_context_files);
}
#[test]
fn no_skills_flag() {
let cli = Cli::parse_from(["capo", "--no-skills"]);
assert!(cli.no_skills);
let cli = Cli::parse_from(["capo"]);
assert!(!cli.no_skills);
}
#[test]
fn provider_flag_parses() {
let cli = Cli::parse_from(["capo", "--provider", "claude-code"]);
assert_eq!(cli.provider.as_deref(), Some("claude-code"));
}
#[test]
fn session_flags_parse() {
let cli = Cli::parse_from(["capo", "-c"]);
assert!(cli.continue_session);
let cli = Cli::parse_from(["capo", "--session", "01ABC"]);
assert_eq!(cli.session.as_deref(), Some("01ABC"));
let cli = Cli::parse_from(["capo", "-r"]);
assert!(cli.resume);
}
#[test]
fn print_mode_with_continue_flag_parses() {
let cli = Cli::parse_from(["capo", "-p", "x", "-c"]);
assert!(cli.continue_session);
assert_eq!(cli.prompt.as_deref(), Some("x"));
}
#[test]
fn json_flag_requires_prompt_and_yields_json_mode() {
let cli = Cli::parse_from(["capo", "-p", "hi", "--json"]);
assert!(matches!(cli.mode(), Mode::Json(p) if p == "hi"));
assert!(!cli.dangerously_allow_all);
}
#[test]
fn json_without_prompt_is_a_usage_error() {
assert!(Cli::try_parse_from(["capo", "--json"]).is_err());
}
#[test]
fn rpc_flag_yields_rpc_mode() {
let cli = Cli::parse_from(["capo", "--rpc"]);
assert!(matches!(cli.mode(), Mode::Rpc));
}
#[test]
fn rpc_conflicts_with_prompt() {
assert!(Cli::try_parse_from(["capo", "--rpc", "-p", "hi"]).is_err());
}
#[test]
fn dangerously_allow_all_parses() {
let cli = Cli::parse_from(["capo", "-p", "hi", "--json", "--dangerously-allow-all"]);
assert!(cli.dangerously_allow_all);
}
#[test]
fn prompt_without_json_is_still_print_mode() {
let cli = Cli::parse_from(["capo", "-p", "hi"]);
assert!(matches!(cli.mode(), Mode::Print(_)));
}
}