depup-cli 0.2.0

Check dependency versions across Maven and npm ecosystems
//! depup — CLI tool for checking dependency versions across multiple ecosystems.
//!
//! Supports Maven and npm ecosystems with auto-detection based on project files.
//! The pipeline flows: Discovery → Check → Comparison → Output.

mod app;
mod command;
mod constants;
mod dependency;
mod error;
mod filter;
mod json;
mod maven;
mod npm;
mod output;
mod progress;
mod version;

use anyhow::Result;

use crate::error::{DepupError, JsonErrorEnvelope};

#[tokio::main]
async fn main() {
    // Enable dynamic shell completions via clap_complete's `CompleteEnv` protocol.
    clap_complete::CompleteEnv::with_factory(app::build_app).complete();

    // Pre-check for --json flag before parsing to format top-level errors correctly.
    let json = std::env::args().any(|a| a == "--json");
    match run().await {
        Ok(should_fail) => {
            if should_fail {
                std::process::exit(1);
            }
        }
        Err(e) => {
            if json {
                let envelope = JsonErrorEnvelope::from_anyhow(&e);
                match serde_json::to_string(&envelope) {
                    Ok(json) => println!("{json}"),
                    Err(ser) => eprintln!("Error: {e:#}\n(JSON serialization also failed: {ser})"),
                }
            } else {
                eprintln!("Error: {e:#}");
            }
            std::process::exit(1);
        }
    }
}

/// Parses CLI arguments and dispatches to the appropriate subcommand handler.
/// Returns `true` if the process should exit with code 1.
async fn run() -> Result<bool> {
    let matches = app::build_app()
        .try_get_matches()
        .map_err(classify_clap_error)?;

    match matches.subcommand() {
        Some(("check", m)) => command::check::check(m).await,
        Some(("update", m)) => command::update::update(m).await,
        Some(("audit", m)) => command::audit::audit(m).await,
        Some(("completions", m)) => command::completions::completions(m).map(|()| false),
        _ => unreachable!("subcommand_required is set"),
    }
}

/// Converts clap errors into `DepupError` for structured output.
/// Help and version display errors are handled by clap directly (exit 0).
#[allow(clippy::needless_pass_by_value)]
fn classify_clap_error(err: clap::Error) -> anyhow::Error {
    match err.kind() {
        clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
            err.exit();
        }
        _ => DepupError::clap_parse_error(err.to_string().trim()).into(),
    }
}