omni::architecture_component!(Bin);
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use omni::code_lint::runner::{LintOptions, run_code_lint};
use omni::command_lint::runner::run_command_lint;
use omni::config::Config;
use omni::diagnostic::{OutputFormat, print_diagnostics};
use omni::rule_catalog::{DiscoveryArgs, explain_footer};
use omni::rule_selection::{ConfigOverrides, load_config};
#[derive(Parser, Debug)]
#[command(
name = "omnicoach",
version,
about = "Static code analysis and command workflow coach"
)]
struct Cli {
#[command(subcommand)]
subcommand: Option<DomainCommand>,
#[arg(long, global = true, value_enum, default_value_t = OutputFormat::Plain)]
format: OutputFormat,
#[arg(long)]
diff: bool,
#[arg(long)]
diff_rev: Option<String>,
#[command(flatten)]
config_overrides: ConfigOverrides,
#[command(flatten)]
discovery: DiscoveryArgs,
}
#[derive(Subcommand, Debug)]
enum DomainCommand {
Code(CodeArgs),
Command(CommandArgs),
}
#[derive(Args, Debug)]
struct CodeArgs {
#[arg(default_value = ".")]
paths: Vec<PathBuf>,
#[arg(long)]
diff: bool,
#[arg(long)]
diff_rev: Option<String>,
}
#[derive(Args, Debug)]
struct CommandArgs {
#[arg(long, required_unless_present_any = ["list_rules", "list_tags", "explain"])]
cmd: Option<String>,
}
fn main() {
if let Err(error) = run() {
eprintln!("Error: {error}");
std::process::exit(2);
}
}
fn run() -> anyhow::Result<()> {
let cli = Cli::parse();
if let Some(output) = cli.discovery.render(&cli.config_overrides) {
print!("{}", output?);
return Ok(());
}
let config = load_config(&cli.config_overrides)
.map_err(|error| anyhow::anyhow!("Failed to load configuration: {error}"))?;
match cli.subcommand {
Some(DomainCommand::Code(args)) => {
let options = LintOptions {
paths: args.paths,
diff: args.diff || cli.diff,
diff_rev: args.diff_rev.or(cli.diff_rev),
};
run_code(&options, &config, cli.format)
}
None => {
let options = LintOptions {
paths: vec![PathBuf::from(".")],
diff: cli.diff,
diff_rev: cli.diff_rev,
};
run_code(&options, &config, cli.format)
}
Some(DomainCommand::Command(args)) => {
let Some(command) = args.cmd else {
unreachable!("clap requires --cmd unless a discovery flag is given");
};
run_command(&command, &config, cli.format)
}
}
}
fn run_code(options: &LintOptions, config: &Config, format: OutputFormat) -> anyhow::Result<()> {
let report = run_code_lint(options, config)?;
if !report.diagnostics.is_empty() {
print_diagnostics(&report.diagnostics, format)?;
if format == OutputFormat::Plain {
println!("{}", explain_footer(env!("CARGO_BIN_NAME")));
}
}
for unreadable in &report.unreadable_files {
eprintln!(
"Error: Failed to read file '{}': {}",
unreadable.path.display(),
unreadable.error
);
}
if !report.unreadable_files.is_empty() {
std::process::exit(2);
}
if !report.diagnostics.is_empty() {
std::process::exit(1);
}
if format == OutputFormat::Plain {
println!("All checks passed!");
}
Ok(())
}
fn run_command(command: &str, config: &Config, format: OutputFormat) -> anyhow::Result<()> {
let diagnostics = run_command_lint(command, config);
if !diagnostics.is_empty() {
print_diagnostics(&diagnostics, format)?;
if format == OutputFormat::Plain {
println!("{}", explain_footer(env!("CARGO_BIN_NAME")));
}
std::process::exit(1);
}
Ok(())
}