Skip to main content

haz_cli/
lib.rs

1//! `haz` workspace task runner: library surface for the `haz`
2//! binary.
3//!
4//! Exposes the runtime modules wired together by the `haz`
5//! command-line tool (`src/main.rs`). Integration tests under
6//! `tests/` drive these modules directly against fixture
7//! workspaces; the binary itself is a thin adapter over
8//! [`dispatch`].
9
10pub mod cli;
11pub mod commands;
12pub mod context;
13pub mod output;
14
15mod workspace_load;
16
17use std::path::Path;
18
19use cli::Cmd;
20use context::Context;
21use miette::Result;
22
23/// Exit code reserved for workspace-load failures, target-
24/// resolution failures, and other internal / configuration errors
25/// per `EXEC-021`'s fourth class. Distinct from
26/// `haz_exec::exit_code`'s task-failure code (`1`) and the POSIX
27/// signal codes (`130` / `143`).
28pub const EXIT_INTERNAL_OR_CONFIG_ERROR: i32 = 2;
29
30/// Dispatch a parsed [`Cmd`] against `cwd` and return the per-
31/// command exit code.
32///
33/// `cwd` is a parameter rather than read from
34/// [`std::env::current_dir`] so callers (the binary, integration
35/// tests, embedders) drive the dispatch deterministically. The
36/// binary's `main` reads cwd from the environment once and passes
37/// it here.
38///
39/// `haz init` is the one subcommand that does NOT load a workspace
40/// (it creates one), so the [`Context::open`] preamble is skipped
41/// for it and the cwd flows through directly. Every other
42/// subcommand consumes the freshly-built [`Context`] by value.
43///
44/// # Errors
45///
46/// Returns a [`miette::Report`] on workspace-load, target-
47/// resolution, or scheduler failure. The binary maps any report
48/// to [`EXIT_INTERNAL_OR_CONFIG_ERROR`].
49pub fn dispatch(cmd: &Cmd, cwd: &Path) -> Result<i32> {
50    if let Cmd::Init(args) = cmd {
51        return commands::init::execute(args, cwd);
52    }
53    // `haz check` must surface every load error at once (AUX-010 /
54    // AUX-011); the other subcommands keep the first-error
55    // short-circuit, which AUX-010 permits (it binds only check).
56    let ctx = if matches!(cmd, Cmd::Check(_)) {
57        Context::open_accumulating(cwd)?
58    } else {
59        Context::open(cwd)?
60    };
61    match cmd {
62        Cmd::Run(args) => commands::run::execute(args, &ctx),
63        Cmd::Check(args) => commands::check::execute(args, &ctx),
64        Cmd::Why(args) => commands::why::execute(args, &ctx),
65        Cmd::CacheClear => commands::cache::clear(&ctx),
66        Cmd::CacheClean(args) => commands::cache::clean(&ctx, args),
67        Cmd::CacheInfo => commands::cache::info(&ctx),
68        Cmd::Query(args) => commands::query::execute(args, &ctx),
69        Cmd::Init(_) => unreachable!("Init handled above before Context::open"),
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::dispatch;
76    use crate::cli::{CheckArgs, Cmd};
77    use std::fs;
78    use std::path::Path;
79
80    fn write_file(path: &Path, contents: &str) {
81        if let Some(parent) = path.parent() {
82            fs::create_dir_all(parent).unwrap();
83        }
84        fs::write(path, contents).unwrap();
85    }
86
87    /// End-to-end AUX-010 / AUX-011: dispatching `haz check` against a
88    /// workspace whose projects all fail to parse surfaces every error
89    /// in one run. Exercises the `Cmd::Check` arm routing through the
90    /// accumulating loader.
91    #[test]
92    fn dispatch_check_surfaces_every_parse_error() {
93        let tempdir = tempfile::TempDir::new().unwrap();
94        let root = tempdir.path();
95        write_file(
96            &root.join(".haz/workspace.yml"),
97            "projects:\n  - /alpha\n  - /beta\n",
98        );
99        write_file(
100            &root.join("alpha/haz.yml"),
101            "name: alpha\ntasks:\n  build:\n    command: \"AAA\"\n",
102        );
103        write_file(
104            &root.join("beta/haz.yml"),
105            "name: beta\ntasks:\n  build:\n    command: \"BBB\"\n",
106        );
107
108        let cmd = Cmd::Check(CheckArgs::default());
109        let err = dispatch(&cmd, root).unwrap_err();
110        let rendered = format!("{err:?}");
111        assert!(
112            rendered.contains("AAA") && rendered.contains("BBB"),
113            "haz check must surface every broken file in one run: {rendered}",
114        );
115    }
116}