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}