use std::io::IsTerminal;
use clap::{Parser, Subcommand, ValueEnum};
use clap_complete::Shell;
const COMPLETION_GENERATE_HELP: &str = r#"EXAMPLES
bash
Add to ~/.bashrc or ~/.bash_profile:
eval "$(aptu completion generate bash)"
zsh
Generate completion file:
mkdir -p ~/.zsh/completions
aptu completion generate zsh > ~/.zsh/completions/_aptu
Add to ~/.zshrc (before compinit):
fpath=(~/.zsh/completions $fpath)
autoload -U compinit && compinit -i
fish
Generate completion file:
aptu completion generate fish > ~/.config/fish/completions/aptu.fish
PowerShell
Add to $PROFILE:
aptu completion generate powershell | Out-String | Invoke-Expression
"#;
const MODELS_LIST_HELP: &str = "EXAMPLES
List models from all providers:
aptu models list
List models from a specific provider:
aptu models list --provider openrouter
Sort by context window size:
aptu models list --provider gemini --sort context
Filter to models with at least 100k context:
aptu models list --provider groq --min-context 100000";
#[derive(Clone, Copy, Default, ValueEnum)]
pub enum OutputFormat {
#[default]
Text,
Json,
Yaml,
Markdown,
Sarif,
}
#[derive(Clone, Copy, Default, ValueEnum)]
pub enum IssueState {
#[default]
Open,
Closed,
All,
}
#[derive(Clone)]
pub struct OutputContext {
pub format: OutputFormat,
pub quiet: bool,
pub verbose: bool,
pub is_tty: bool,
}
impl OutputContext {
pub fn from_cli(format: OutputFormat, verbose: bool) -> Self {
let quiet = matches!(
format,
OutputFormat::Json | OutputFormat::Yaml | OutputFormat::Markdown | OutputFormat::Sarif
);
Self {
format,
quiet,
verbose,
is_tty: std::io::stdout().is_terminal(),
}
}
pub fn is_interactive(&self) -> bool {
self.is_tty && !self.quiet && matches!(self.format, OutputFormat::Text)
}
pub fn is_verbose(&self) -> bool {
self.verbose
}
}
pub fn parse_date_to_rfc3339(date_str: &str) -> anyhow::Result<String> {
if chrono::DateTime::parse_from_rfc3339(date_str).is_ok() {
return Ok(date_str.to_string());
}
if let Ok(date) = chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
let datetime = date
.and_hms_opt(0, 0, 0)
.ok_or_else(|| anyhow::anyhow!("Failed to create datetime from date {date_str}"))?;
let rfc3339 = format!("{}Z", datetime.format("%Y-%m-%dT%H:%M:%S"));
return Ok(rfc3339);
}
anyhow::bail!("Invalid date format. Expected YYYY-MM-DD or RFC3339 format, got: {date_str}")
}
#[derive(Parser)]
#[command(name = "aptu")]
#[command(version, about, long_about = None)]
#[command(arg_required_else_help = true)]
pub struct Cli {
#[arg(long, short = 'o', global = true, default_value = "text", value_enum)]
pub output: OutputFormat,
#[arg(long, short = 'v', global = true)]
pub verbose: bool,
#[arg(long, global = true)]
pub provider: Option<String>,
#[arg(long, global = true)]
pub model: Option<String>,
#[arg(skip)]
pub inferred_repo: Option<String>,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
#[command(subcommand)]
Auth(AuthCommand),
#[command(subcommand)]
Repo(RepoCommand),
#[command(subcommand)]
Issue(IssueCommand),
#[command(subcommand)]
Pr(PrCommand),
History,
#[command(subcommand)]
Models(ModelsCommand),
#[command(subcommand)]
Completion(CompletionCommand),
}
#[derive(Subcommand)]
pub enum AuthCommand {
Login,
Logout,
Status,
}
#[derive(Subcommand)]
pub enum RepoCommand {
List {
#[arg(long)]
curated: bool,
#[arg(long)]
custom: bool,
},
Discover {
#[arg(long)]
language: Option<String>,
#[arg(long, default_value = "10")]
min_stars: u32,
#[arg(long, default_value = "20")]
limit: u32,
},
Add {
repo: String,
},
Remove {
repo: String,
},
}
#[derive(Subcommand)]
pub enum IssueCommand {
List {
repo: Option<String>,
#[arg(long)]
no_cache: bool,
},
Triage {
#[arg(value_name = "REFERENCE")]
references: Vec<String>,
#[arg(long, short = 'r')]
repo: Option<String>,
#[arg(long)]
since: Option<String>,
#[arg(long, short = 's', default_value = "open")]
state: IssueState,
#[arg(long)]
dry_run: bool,
#[arg(long)]
no_apply: bool,
#[arg(long)]
no_comment: bool,
#[arg(short, long)]
force: bool,
},
Create {
repo: String,
#[arg(long)]
title: Option<String>,
#[arg(long)]
body: Option<String>,
#[arg(long)]
from: Option<String>,
#[arg(long)]
dry_run: bool,
},
}
#[derive(Subcommand)]
pub enum CompletionCommand {
#[command(after_long_help = COMPLETION_GENERATE_HELP)]
Generate {
#[arg(value_enum)]
shell: Shell,
},
Install {
#[arg(long, value_enum)]
shell: Option<Shell>,
#[arg(long)]
dry_run: bool,
},
}
#[derive(Subcommand)]
pub enum PrCommand {
Review {
#[arg(value_name = "REFERENCE")]
references: Vec<String>,
#[arg(long, short = 'r')]
repo: Option<String>,
#[arg(long, group = "review_type")]
comment: bool,
#[arg(long, group = "review_type")]
approve: bool,
#[arg(long, group = "review_type")]
request_changes: bool,
#[arg(long)]
dry_run: bool,
#[arg(long)]
no_apply: bool,
#[arg(long)]
no_comment: bool,
#[arg(short, long)]
force: bool,
#[arg(long, value_name = "PATH")]
repo_path: Option<std::path::PathBuf>,
#[arg(long, default_value_t = false)]
deep: bool,
},
Label {
#[arg(value_name = "REFERENCE")]
reference: String,
#[arg(long, short = 'r')]
repo: Option<String>,
#[arg(long)]
dry_run: bool,
},
Create {
#[arg(long)]
repo: Option<String>,
#[arg(long)]
title: String,
#[arg(long)]
body: Option<String>,
#[arg(long)]
branch: Option<String>,
#[arg(long, default_value = "main")]
base: String,
#[arg(long, value_name = "FILE")]
diff: Option<std::path::PathBuf>,
#[arg(long)]
draft: bool,
#[arg(long)]
force: bool,
},
}
#[derive(Clone, Copy, Default, ValueEnum)]
pub enum SortBy {
#[default]
Name,
Context,
}
#[derive(Subcommand)]
pub enum ModelsCommand {
#[command(after_long_help = MODELS_LIST_HELP)]
List {
#[arg(long)]
provider: Option<String>,
#[arg(long, value_enum, default_value = "name")]
sort: SortBy,
#[arg(long)]
min_context: Option<u32>,
#[arg(long)]
filter: Option<String>,
},
}