capo-cli 0.2.0

Capo — a Rust-native coding agent CLI.
use clap::Parser;

#[derive(Debug, Parser)]
#[command(name = "capo", version, about = "Capo — a Rust coding agent")]
pub struct Cli {
    /// Non-interactive print mode: send a single prompt, print the reply.
    #[arg(short = 'p', long = "prompt")]
    pub prompt: Option<String>,

    /// Override the configured model name.
    #[arg(long)]
    pub model: Option<String>,

    /// Override the configured LLM provider for this run.
    /// One of: anthropic, claude-code, codex-cli.
    #[arg(long = "provider")]
    pub provider: Option<String>,

    /// Increase log verbosity (-v = debug, -vv = trace). RUST_LOG always wins.
    #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
    pub verbosity: u8,

    /// Disable AGENTS.md / CLAUDE.md discovery.
    #[arg(short = 'n', long = "no-context-files")]
    pub no_context_files: bool,

    /// Disable skill discovery (`~/.capo/agent/skills/` + `<project>/.capo/skills/`).
    #[arg(long = "no-skills")]
    pub no_skills: bool,

    /// Continue the most recent session in this working directory.
    #[arg(short = 'c', long = "continue")]
    pub continue_session: bool,

    /// Resume a specific session by ULID prefix or absolute/expanded path.
    #[arg(long = "session")]
    pub session: Option<String>,

    /// Open the interactive session picker on startup.
    #[arg(short = 'r', long = "resume")]
    pub resume: bool,
}

pub enum Mode {
    Print(String),
    InteractiveStub,
}

impl Cli {
    pub fn mode(&self) -> Mode {
        if let Some(p) = &self.prompt {
            Mode::Print(p.clone())
        } else {
            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"));
    }
}