degenbot_cli/lib.rs
1//! `degenbot-cli` - the degenbot console binary (ADR-051 D2).
2//!
3//! One argv facade, one command model. This crate owns exactly the things
4//! [`degenbot_cli_core`] cannot own by charter:
5//!
6//! - **argv declaration** ([`argv`]): the clap v4 derive tree for the whole
7//! command vocabulary, and the argv -> [`Command`](degenbot_cli_core::Command)
8//! mapping. See `argv`'s module docs for where clap's shape differs from the
9//! retired Python click tree.
10//! - **rendering** ([`render`]): typed [`CommandReport`](degenbot_cli_core::CommandReport)
11//! lines to stdout, typed [`CliError`](degenbot_cli_core::CliError) to stderr
12//! with its `ExitCode`.
13//! - **interaction** ([`prompt`]): the stdin/stdout [`Prompter`](degenbot_cli_core::Prompter)
14//! the arms ask; the prompt *policy* stays declared data in cli-core (D4).
15//! - **sinks** ([`sinks`]): the tracing registry + console fmt layer, booted in
16//! the same order as the Python driver (typed config first, then the
17//! subscriber), and **progress** ([`progress`]): the `indicatif` bar, which
18//! exists only here (D9).
19//! - **SIGINT ownership** ([`signal`], D7): first Ctrl+C feeds cli-core's
20//! [`CancelHandle`](degenbot_cli_core::CancelHandle); a second aborts.
21//!
22//! cli-core stays clap-free and indicatif-free (asserted by
23//! `just check-cli-core-purity`); this crate stays free of every domain-engine
24//! dependency (asserted by `just check-cli-shell-purity`).
25
26pub mod argv;
27pub mod progress;
28pub mod prompt;
29pub mod render;
30pub mod signal;
31pub mod sinks;
32
33pub use argv::{Cli, Commands};
34
35use std::io::Write as _;
36
37use degenbot_cli_core::CancelHandle;
38use degenbot_config::ProcessEnv;
39
40/// The banner `--version` prints: the workspace version (ADR-009 lockstep) plus
41/// the shared build receipt, embedded by `build.rs`.
42///
43/// The fingerprint half is byte-identical to the Python FFI's
44/// `degenbot._ffi.build_fingerprint()`, so the two entry surfaces can be
45/// compared directly rather than trusted.
46pub const VERSION_LINE: &str = concat!(
47 env!("CARGO_PKG_VERSION"),
48 " (build ",
49 env!("DEGENBOT_CLI_BUILD_NUMBER"),
50 " ",
51 env!("DEGENBOT_CLI_BUILD_FINGERPRINT"),
52 ")"
53);
54
55/// Parse argv, boot the sinks, resolve the argv into a
56/// [`Command`](degenbot_cli_core::Command), run it and render the result.
57///
58/// Returns the process exit code: clap owns `--help`/`--version`/usage errors
59/// (its own exit codes apply), a refused typed config is `2` (the same refusal
60/// exit the Python module init uses), and every command outcome maps through
61/// cli-core's single `CliError -> ExitCode` site.
62#[must_use]
63pub fn run() -> i32 {
64 let cli = <Cli as clap::Parser>::parse();
65 execute(&cli)
66}
67
68fn execute(cli: &Cli) -> i32 {
69 let env = ProcessEnv;
70 if cli.command.is_none() {
71 argv::write_missing_subcommand_error();
72 return 2;
73 }
74
75 // Sinks before anything that emits, exactly like the Python module init.
76 let _telemetry = match sinks::boot() {
77 Ok(telemetry) => telemetry,
78 Err(refusal) => {
79 let _ = writeln!(std::io::stderr().lock(), "{refusal}");
80 return 2;
81 }
82 };
83
84 let ctx = argv::context(cli, &env);
85 let command = match argv::resolve_with_env(cli, &env) {
86 Ok(command) => command,
87 Err(error) => return render::error(&error),
88 };
89
90 let prompter = prompt::ConsolePrompter::new();
91 let cancel = CancelHandle::new();
92 let _sigint = signal::install(cancel.clone());
93
94 let outcome = degenbot_cli_core::run_with_cancel(&command, &ctx, &prompter, &cancel);
95 render::outcome(&outcome)
96}