Skip to main content

haz_cli/cli/
mod.rs

1//! Command-line argument parser for the `haz` binary.
2//!
3//! Built on `bpaf` 0.9 using the combinator API; the derive
4//! macro is deliberately not used so the parser type tree stays
5//! decoupled from bpaf's macros. The parser produces a [`Cmd`]
6//! sum: one variant per command shape that the spec names
7//! normatively (`haz run`, `haz check`, `haz cache clear`,
8//! `haz cache clean --soft`).
9//!
10//! `--help` and `--version` are provided by bpaf automatically;
11//! the version string is sourced from `CARGO_PKG_VERSION` at
12//! compile time.
13
14use bpaf::{OptionParser, Parser, construct};
15pub use haz_domain::scope::Scope;
16pub use haz_domain::settings::output::OutputMode;
17pub use haz_domain::task_ref::TaskRef;
18pub use haz_vcs::GitFilter;
19
20pub(super) mod cache;
21pub(super) mod check;
22pub(super) mod init;
23pub(super) mod query;
24pub(super) mod run;
25pub(super) mod why;
26
27use crate::cli::cache::cache_command;
28use crate::cli::check::check_command;
29use crate::cli::init::init_command;
30use crate::cli::query::query_command;
31use crate::cli::run::run_command;
32use crate::cli::why::why_command;
33
34pub use crate::cli::cache::CacheCleanArgs;
35pub use crate::cli::check::CheckArgs;
36pub use crate::cli::init::InitArgs;
37pub use crate::cli::query::{FieldPresence, OutputFormat, QueryArgs};
38pub use crate::cli::run::RunArgs;
39pub use crate::cli::why::WhyArgs;
40
41/// Top-level command parsed from `argv`.
42///
43/// Each variant carries the per-command arguments needed to
44/// dispatch. The parser layer does not load the workspace, run
45/// the scheduler, or touch the filesystem; downstream modules
46/// take a [`Cmd`] value and act on it.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum Cmd {
49    /// `haz run [--all | --workspace] [--dry-run] [--output-mode <mode>]
50    /// [<TASK_REF>...]` -- run (or, with `--dry-run`, enumerate)
51    /// the selected tasks plus their transitive hard predecessors
52    /// in the discovered workspace.
53    Run(RunArgs),
54    /// `haz check [--all | --workspace]` -- validate the discovered
55    /// workspace without running any task (`AUX-009`..`AUX-012`).
56    Check(CheckArgs),
57    /// `haz why <TASK>` -- read-only introspection of one task per
58    /// `AUX-013`..`AUX-016`.
59    Why(WhyArgs),
60    /// `haz cache clear` -- remove every cache entry (`CACHE-021`).
61    CacheClear,
62    /// `haz cache clean [--soft] [--max-age <DURATION>]
63    /// [--max-size <BYTES>] [--dry-run]` -- selective cache
64    /// reclamation (`CACHE-022`, `AUX-022`..`AUX-027`). The
65    /// executor enforces the "at least one mode" rule
66    /// (`AUX-022`) after merging CLI flags with workspace
67    /// `cache.clean.*` defaults (`AUX-025`).
68    CacheClean(CacheCleanArgs),
69    /// `haz cache info` -- read-only cache-state report
70    /// (`AUX-017`..`AUX-021`).
71    CacheInfo,
72    /// `haz query [filter flags]... [--format <fmt>]` -- enumerate
73    /// the tasks in the workspace matching every supplied filter
74    /// (`QRY-001`..`QRY-009`).
75    Query(QueryArgs),
76    /// `haz init [--yes]` -- scaffold a workspace
77    /// (`AUX-001`..`AUX-008`). The bare form runs the interactive
78    /// flow (`AUX-003`..`AUX-005`); `--yes` writes the minimal
79    /// `projects: []` workspace per `AUX-007`.
80    Init(InitArgs),
81}
82
83/// Build the top-level argument parser.
84///
85/// Returns an [`OptionParser`] that callers consume via
86/// `.run()` (for `argv` parsing) or `.run_inner(...)` (for
87/// programmatic / test parsing).
88pub fn cli() -> OptionParser<Cmd> {
89    let run = run_command().map(Cmd::Run).to_options();
90    let check = check_command().map(Cmd::Check).to_options();
91    let why = why_command().map(Cmd::Why).to_options();
92    let cache = cache_command().to_options();
93    let query = query_command().map(Cmd::Query).to_options();
94    let init = init_command().map(Cmd::Init).to_options();
95
96    let run_subcommand = run.command("run").help("Run one or more tasks");
97    let check_subcommand = check
98        .command("check")
99        .help("Validate the workspace without running (AUX-009..AUX-012)");
100    let why_subcommand = why
101        .command("why")
102        .help("Introspect a single task (AUX-013..AUX-016)");
103    let cache_subcommand = cache
104        .command("cache")
105        .help("Cache management commands (CACHE-021, CACHE-022)");
106    let query_subcommand = query
107        .command("query")
108        .help("Enumerate tasks under a filter expression (QRY-001..QRY-009)");
109    let init_subcommand = init
110        .command("init")
111        .help("Scaffold a workspace (AUX-001..AUX-008)");
112
113    construct!([
114        run_subcommand,
115        check_subcommand,
116        why_subcommand,
117        cache_subcommand,
118        query_subcommand,
119        init_subcommand,
120    ])
121    .to_options()
122    .version(env!("CARGO_PKG_VERSION"))
123    .descr("haz: task dependency graph runner")
124}
125
126/// Shared test parser harness: run the full `cli()` parser
127/// against `args` and surface the bpaf stderr string on error.
128/// Lives at module scope so the per-subcommand child test modules
129/// can reach it as `crate::cli::parse`.
130#[cfg(test)]
131fn parse(args: &[&str]) -> Result<Cmd, String> {
132    cli().run_inner(args).map_err(|err| err.unwrap_stderr())
133}
134
135#[cfg(test)]
136mod tests {
137    use super::parse;
138
139    #[test]
140    fn no_subcommand_rejects() {
141        let err = parse(&[]).unwrap_err();
142        assert!(
143            !err.is_empty(),
144            "missing subcommand must produce a diagnostic",
145        );
146    }
147}