haz-cli 0.2.0

Command-line task runner: tracks task DAGs across a workspace of projects, executes them with caching.
Documentation
//! `haz` command-line entry point.
//!
//! Thin adapter over [`haz_cli`]: parses argv, reads cwd once
//! from the environment, calls [`haz_cli::dispatch`], and
//! propagates the resulting exit code.
//!
//! Two sources of failure share the
//! [`haz_cli::EXIT_INTERNAL_OR_CONFIG_ERROR`] exit class
//! per `EXEC-021`'s fourth class:
//!
//! - A [`miette::Report`] returned by [`haz_cli::dispatch`]
//!   (workspace-load, target-resolution, scheduler failure).
//! - A [`bpaf::ParseFailure::Stderr`] returned by the CLI parser
//!   (unknown flag, missing positional, malformed value).
//!
//! `bpaf`'s default `.run()` adapter would short-circuit the
//! second case with `std::process::exit(1)`, colliding with the
//! task-failure class. We drive `.run_inner(...)` ourselves and
//! map the parse failure onto the same configuration-error class
//! the dispatch arm uses, so the four EXEC-021 classes stay
//! distinguishable per the auxiliary subcommand rules (`AUX-008`,
//! `AUX-012`, `AUX-016`, `AUX-021`, `AUX-027`, `QRY-009`).

use bpaf::{Args, ParseFailure};
use haz_cli::{EXIT_INTERNAL_OR_CONFIG_ERROR, cli, dispatch};
use miette::{IntoDiagnostic, WrapErr};

/// Width used when rendering bpaf's diagnostic messages.
/// Matches the implicit default of bpaf's `.run()` helper.
const PARSE_FAILURE_RENDER_WIDTH: usize = 100;

fn main() {
    let cmd = match cli::cli().run_inner(Args::current_args()) {
        Ok(cmd) => cmd,
        Err(failure) => {
            failure.print_message(PARSE_FAILURE_RENDER_WIDTH);
            let exit_code = match failure {
                // `--help`, `--version`, and completion are
                // information-request paths: print to stdout and
                // exit with the success class.
                ParseFailure::Stdout(..) | ParseFailure::Completion(..) => 0,
                // A genuine argument-parsing error belongs to the
                // configuration-error class per EXEC-021's fourth
                // bullet. Returning `1` here would collide with
                // the task-failure class and violate
                // `AUX-008` / `AUX-012` / `AUX-016` / `AUX-021` /
                // `AUX-027` / `QRY-009`.
                ParseFailure::Stderr(..) => EXIT_INTERNAL_OR_CONFIG_ERROR,
            };
            std::process::exit(exit_code);
        }
    };
    let exit_code = match run(&cmd) {
        Ok(code) => code,
        Err(report) => {
            eprintln!("{report:?}");
            EXIT_INTERNAL_OR_CONFIG_ERROR
        }
    };
    std::process::exit(exit_code);
}

fn run(cmd: &cli::Cmd) -> miette::Result<i32> {
    let cwd = std::env::current_dir()
        .into_diagnostic()
        .wrap_err("cannot read current working directory")?;
    dispatch(cmd, &cwd)
}