use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use anyhow::{Context, Result, bail};
use clap::{Parser, Subcommand};
use agentlint::output::OutputFormat;
use agentlint::{LintOptions, Severity};
#[derive(Debug, Parser)]
#[command(name = "agentlint", version, about = "ESLint for your AI coding setup", long_about = None)]
#[allow(clippy::struct_excessive_bools)] struct Cli {
#[command(subcommand)]
command: Option<Command>,
#[arg(value_name = "PATH", default_value = ".")]
path: PathBuf,
#[arg(long, value_enum, default_value = "stylish", global = true)]
format: OutputFormat,
#[arg(long, value_enum, value_name = "LEVEL", global = true)]
severity: Option<Severity>,
#[arg(long, global = true)]
no_color: bool,
#[arg(long, global = true)]
quiet: bool,
#[arg(long, global = true)]
verbose: bool,
#[arg(long, global = true)]
fix: bool,
#[arg(long, value_name = "FILE", global = true)]
config: Option<PathBuf>,
#[arg(long, value_name = "GLOB", action = clap::ArgAction::Append, global = true)]
ignore: Vec<String>,
}
#[derive(Debug, Subcommand)]
enum Command {
Check {
#[arg(default_value = ".")]
path: PathBuf,
},
Rules,
Explain { rule: String },
Init {
#[arg(default_value = ".")]
path: PathBuf,
},
}
fn main() -> ExitCode {
match run(Cli::parse()) {
Ok(code) => code,
Err(error) => {
let _ = writeln!(io::stderr(), "agentlint: {error:#}");
ExitCode::from(2)
}
}
}
fn run(cli: Cli) -> Result<ExitCode> {
match &cli.command {
Some(Command::Rules) => {
print_rules();
return Ok(ExitCode::SUCCESS);
}
Some(Command::Explain { rule }) => {
print_explanation(rule)?;
return Ok(ExitCode::SUCCESS);
}
Some(Command::Init { path }) => {
init(path)?;
return Ok(ExitCode::SUCCESS);
}
_ => {}
}
let path = match &cli.command {
Some(Command::Check { path }) => path,
_ => &cli.path,
};
let root = path
.canonicalize()
.with_context(|| format!("cannot access {}", path.display()))?;
if !root.is_dir() {
bail!("scan target must be a directory: {}", root.display());
}
if cli.verbose && cli.format != OutputFormat::Json {
let _ = writeln!(io::stderr(), "Scanning {}", root.display());
}
let options = LintOptions {
config_path: cli.config,
extra_ignores: cli.ignore,
minimum_severity: cli.severity,
};
let mut report = agentlint::lint(&root, &options)?;
if cli.fix {
let config = agentlint::Config::load(&root, options.config_path.as_deref())?;
let files = agentlint::scanner::scan(&root, &config, &options.extra_ignores)?;
let project = agentlint::project::ProjectFacts::inspect(&root)?;
let fixed = agentlint::fix::apply(&files, &project)?;
if !fixed.is_empty() {
report = agentlint::lint(&root, &options)?;
report.fixed_files = fixed;
}
}
let rendered = agentlint::output::render(
&report,
cli.format,
!cli.no_color && console::colors_enabled(),
cli.quiet,
)?;
print!("{rendered}");
Ok(if report.error_count() > 0 {
ExitCode::from(1)
} else {
ExitCode::SUCCESS
})
}
fn print_rules() {
println!("RULE DEFAULT CATEGORY NAME");
for rule in agentlint::rules::definitions() {
println!(
"{:<7} {:<8} {:<12} {}",
rule.id, rule.default_severity, rule.category, rule.name
);
}
}
fn print_explanation(id: &str) -> Result<()> {
let Some(rule) = agentlint::rules::definition(id) else {
bail!("unknown rule `{id}`; run `agentlint rules` to list available rules");
};
println!("{} — {}\n", rule.id, rule.name);
println!("{}\n", rule.description);
println!("Why it matters\n {}\n", rule.why);
println!("Bad\n {}\n", rule.bad_example.replace('\n', "\n "));
println!("Good\n {}\n", rule.good_example.replace('\n', "\n "));
println!("Default severity: {}", rule.default_severity);
println!("Disable with:\n rules:\n {}: off", rule.id);
Ok(())
}
fn init(path: &Path) -> Result<()> {
let directory = path
.canonicalize()
.with_context(|| format!("cannot access {}", path.display()))?;
if !directory.is_dir() {
bail!("init target must be a directory");
}
let target = directory.join(".agentlint.yaml");
if target.exists() {
bail!("{} already exists", target.display());
}
agentlint::fix::create_starter(&target)?;
println!("Created {}", target.display());
Ok(())
}