use std::path::PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum};
use crate::io::diff::DiffBackend;
#[derive(Parser, Debug)]
#[command(
name = "llmlint",
version,
about = "LLM-as-judge linter for checks deterministic linters can't express.",
args_conflicts_with_subcommands = true
)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,
#[command(flatten)]
pub lint: LintArgs,
}
#[derive(Subcommand, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum Command {
Lint(LintArgs),
#[command(name = "check-ignores")]
CheckIgnores(CheckIgnoresArgs),
Init(InitArgs),
Config(ConfigArgs),
Where(WhereArgs),
Doctor,
}
#[derive(Args, Debug, Default)]
pub struct LintArgs {
pub files: Vec<PathBuf>,
#[arg(long = "config", short = 'c', value_name = "PATH")]
pub config: Vec<PathBuf>,
#[arg(long = "oneharness-config", value_name = "PATH")]
pub oneharness_config: Vec<PathBuf>,
#[arg(long = "oneharness-bin", value_name = "PATH")]
pub oneharness_bin: Option<String>,
#[arg(long = "prompt-template", value_name = "PATH")]
pub prompt_template: Option<PathBuf>,
#[arg(long = "model", value_name = "NAME")]
pub model: Option<String>,
#[arg(long = "schema-max-retries", value_name = "N")]
pub schema_max_retries: Option<u32>,
#[arg(long = "rationales", overrides_with = "no_rationales", action = clap::ArgAction::SetTrue)]
pub rationales: bool,
#[arg(long = "no-rationales", overrides_with = "rationales", action = clap::ArgAction::SetTrue)]
pub no_rationales: bool,
#[arg(long = "agent", value_name = "NAME")]
pub agent: Option<String>,
#[arg(long = "rule", value_name = "NAME")]
pub rule: Vec<String>,
#[arg(long = "format", value_enum, default_value_t = OutputFormat::Human)]
pub format: OutputFormat,
#[arg(long = "color", value_enum, default_value_t = ColorChoice::Auto)]
pub color: ColorChoice,
#[arg(long = "verbose", short = 'v', action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(long = "max-parallel", value_name = "N")]
pub max_parallel: Option<usize>,
#[arg(long = "timeout", value_name = "SECS")]
pub timeout: Option<u64>,
#[arg(long = "cwd", value_name = "DIR")]
pub cwd: Option<PathBuf>,
#[arg(
long = "diff",
value_name = "BACKEND",
num_args = 0..=1,
default_missing_value = "git",
)]
pub diff: Option<DiffBackend>,
#[arg(long = "diff-base", value_name = "REF", requires = "diff")]
pub diff_base: Option<String>,
}
impl LintArgs {
pub fn rationales(&self) -> Option<bool> {
if self.no_rationales {
Some(false)
} else if self.rationales {
Some(true)
} else {
None
}
}
}
#[derive(ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OutputFormat {
#[default]
Human,
Json,
}
#[derive(ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ColorChoice {
#[default]
Auto,
Always,
Never,
}
impl ColorChoice {
pub fn resolve(self, is_tty: bool, no_color: bool) -> bool {
match self {
ColorChoice::Always => true,
ColorChoice::Never => false,
ColorChoice::Auto => is_tty && !no_color,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn color_always_and_never_ignore_tty_and_no_color() {
for &tty in &[true, false] {
for &no_color in &[true, false] {
assert!(ColorChoice::Always.resolve(tty, no_color));
assert!(!ColorChoice::Never.resolve(tty, no_color));
}
}
}
#[test]
fn color_auto_needs_a_tty_and_an_unset_no_color() {
assert!(ColorChoice::Auto.resolve(true, false));
assert!(!ColorChoice::Auto.resolve(true, true));
assert!(!ColorChoice::Auto.resolve(false, false));
assert!(!ColorChoice::Auto.resolve(false, true));
}
}
#[derive(Args, Debug)]
pub struct InitArgs {
#[arg(long)]
pub global: bool,
#[arg(long = "with-template")]
pub with_template: bool,
#[arg(long)]
pub force: bool,
#[arg(long = "output", short = 'o', value_name = "PATH")]
pub output: Option<PathBuf>,
}
#[derive(Args, Debug, Default)]
pub struct CheckIgnoresArgs {
pub files: Vec<PathBuf>,
#[arg(long = "config", short = 'c', value_name = "PATH")]
pub config: Vec<PathBuf>,
#[arg(long = "cwd", value_name = "DIR")]
pub cwd: Option<PathBuf>,
}
#[derive(Args, Debug, Default)]
pub struct ConfigArgs {
#[arg(long = "config", short = 'c', value_name = "PATH")]
pub config: Vec<PathBuf>,
#[arg(long = "sources")]
pub sources: bool,
#[arg(long = "cwd", value_name = "DIR")]
pub cwd: Option<PathBuf>,
}
#[derive(Args, Debug, Default)]
pub struct WhereArgs {
#[arg(value_name = "PATH")]
pub path: String,
#[arg(long = "config", short = 'c', value_name = "PATH")]
pub config: Vec<PathBuf>,
#[arg(long = "cwd", value_name = "DIR")]
pub cwd: Option<PathBuf>,
}