use std::path::PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum};
use wickra_copilot_llm::Provider;
#[derive(Parser, Debug)]
#[command(name = "wickra-copilot", version, about)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand, Debug)]
pub enum Command {
Context(FeedInput),
Ask(AskArgs),
}
#[derive(Args, Debug)]
pub struct FeedInput {
#[arg(long)]
pub spec: PathBuf,
#[arg(long, conflicts_with = "stdin")]
pub feeds: Option<PathBuf>,
#[arg(long, conflicts_with = "feeds")]
pub stdin: bool,
#[arg(long, value_enum, default_value_t = Format::Text)]
pub format: Format,
}
#[derive(Args, Debug)]
pub struct AskArgs {
#[command(flatten)]
pub input: FeedInput,
#[arg(long)]
pub question: String,
#[arg(long, value_enum, default_value_t = ProviderArg::Ollama)]
pub provider: ProviderArg,
#[arg(long)]
pub model: Option<String>,
}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub enum Format {
Text,
Json,
}
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub enum ProviderArg {
Ollama,
Openai,
Claude,
Gemini,
Custom,
}
impl From<ProviderArg> for Provider {
fn from(arg: ProviderArg) -> Self {
match arg {
ProviderArg::Ollama => Self::Ollama,
ProviderArg::Openai => Self::Openai,
ProviderArg::Claude => Self::Claude,
ProviderArg::Gemini => Self::Gemini,
ProviderArg::Custom => Self::Custom,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn context_parses_feed_inputs() {
let cli = Cli::try_parse_from([
"wickra-copilot",
"context",
"--spec",
"s.json",
"--feeds",
"feeddir",
"--format",
"json",
])
.unwrap();
let Command::Context(input) = cli.command else {
panic!("expected the context subcommand");
};
assert_eq!(input.spec, PathBuf::from("s.json"));
assert_eq!(input.feeds, Some(PathBuf::from("feeddir")));
assert!(!input.stdin);
assert_eq!(input.format, Format::Json);
}
#[test]
fn ask_defaults_provider_to_ollama_and_format_to_text() {
let cli = Cli::try_parse_from([
"wickra-copilot",
"ask",
"--spec",
"s.json",
"--stdin",
"--question",
"why did BTC dump",
])
.unwrap();
let Command::Ask(ask) = cli.command else {
panic!("expected the ask subcommand");
};
assert!(ask.input.stdin);
assert_eq!(ask.input.format, Format::Text);
assert_eq!(ask.question, "why did BTC dump");
assert_eq!(ask.provider, ProviderArg::Ollama);
assert!(ask.model.is_none());
}
#[test]
fn feeds_and_stdin_conflict() {
let err = Cli::try_parse_from([
"wickra-copilot",
"context",
"--spec",
"s.json",
"--feeds",
"feeddir",
"--stdin",
]);
assert!(err.is_err());
}
#[test]
fn provider_arg_maps_to_the_llm_provider() {
assert_eq!(Provider::from(ProviderArg::Ollama), Provider::Ollama);
assert_eq!(Provider::from(ProviderArg::Openai), Provider::Openai);
assert_eq!(Provider::from(ProviderArg::Claude), Provider::Claude);
assert_eq!(Provider::from(ProviderArg::Gemini), Provider::Gemini);
assert_eq!(Provider::from(ProviderArg::Custom), Provider::Custom);
}
}