Skip to main content

canic_cli/cli/
help.rs

1//! Module: canic_cli::cli::help
2//!
3//! Responsibility: render the CLI catalog and route help/version requests.
4//! Does not own: command execution, command-specific help text, or global option forwarding.
5//! Boundary: defines the top-level command catalog shared by help and dispatch.
6
7use crate::cli::globals::{DISPATCH_ARGS, environment_arg, icp_arg};
8use clap::{Arg, ColorChoice, Command};
9use std::ffi::OsString;
10
11const TOP_LEVEL_HELP_TEMPLATE: &str = "Canic Operator CLI v{version}\n{about-with-newline}\n{usage-heading} {usage}\n\n{before-help}\x1b[1mOptions:\x1b[0m\n{options}{after-help}\n";
12const COLOR_RESET: &str = "\x1b[0m";
13const COLOR_HEADING: &str = "\x1b[1m";
14const COLOR_COMMAND: &str = "\x1b[38;5;109m";
15const COLOR_TIP: &str = "\x1b[38;5;245m";
16
17/// One top-level command shown in help and accepted by dispatch.
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub(super) struct CommandSpec {
21    pub(super) name: &'static str,
22    about: &'static str,
23}
24
25pub(super) const COMMAND_SPECS: &[CommandSpec] = &[
26    CommandSpec {
27        name: "admission",
28        about: "Plan, apply, and inspect Fleet ingress admission",
29    },
30    CommandSpec {
31        name: "app",
32        about: "Manage Canic source apps and roles",
33    },
34    CommandSpec {
35        name: "auth",
36        about: "Inspect delegated-auth operation state",
37    },
38    CommandSpec {
39        name: "backup",
40        about: "Create, inspect, and verify backups",
41    },
42    CommandSpec {
43        name: "blob-storage",
44        about: "Inspect and manage blob-storage billing",
45    },
46    CommandSpec {
47        name: "build",
48        about: "Build Canic App and infrastructure artifacts",
49    },
50    CommandSpec {
51        name: "component",
52        about: "Review, create and reconcile one top-level Component",
53    },
54    CommandSpec {
55        name: "cycles",
56        about: "Inspect and transfer cycles for current Fleets",
57    },
58    CommandSpec {
59        name: "diagnostic",
60        about: "Look up a diagnostic code or inspect a build lock",
61    },
62    CommandSpec {
63        name: "evidence",
64        about: "Evaluate stable evidence envelopes",
65    },
66    CommandSpec {
67        name: "fleet",
68        about: "Converge one Fleet from current desired state",
69    },
70    CommandSpec {
71        name: "frontend",
72        about: "Export and verify exact frontend environment bindings",
73    },
74    CommandSpec {
75        name: "info",
76        about: "Inspect one terminal current Fleet",
77    },
78    CommandSpec {
79        name: "inspect",
80        about: "Inspect one current Fleet canister runtime",
81    },
82    CommandSpec {
83        name: "medic",
84        about: "Diagnose workspace and current-Fleet readiness",
85    },
86    CommandSpec {
87        name: "network",
88        about: "Enroll canonical network trust identities",
89    },
90    CommandSpec {
91        name: "observatory",
92        about: "Collect bounded Fleet observations and public reports",
93    },
94    CommandSpec {
95        name: "replica",
96        about: "Manage the local ICP replica",
97    },
98    CommandSpec {
99        name: "restore",
100        about: "Plan or run snapshot restores",
101    },
102    CommandSpec {
103        name: "scaffold",
104        about: "Scaffold Canic source roles",
105    },
106    CommandSpec {
107        name: "state",
108        about: "Audit declared Canic state metadata",
109    },
110    CommandSpec {
111        name: "status",
112        about: "Show quick local workspace status",
113    },
114    CommandSpec {
115        name: "token",
116        about: "Wrap ICP token balance and transfer commands",
117    },
118    CommandSpec {
119        name: "toolchain",
120        about: "Install checksum-authoritative release tools",
121    },
122];
123
124fn is_help_arg(arg: &OsString) -> bool {
125    arg.to_str()
126        .is_some_and(|arg| matches!(arg, "--help" | "-h"))
127}
128
129fn is_version_arg(arg: &OsString) -> bool {
130    arg.to_str()
131        .is_some_and(|arg| matches!(arg, "--version" | "-V"))
132}
133
134/// Return whether the first CLI argument requests help.
135pub fn first_arg_is_help(args: &[OsString]) -> bool {
136    args.first().is_some_and(is_help_arg)
137}
138
139fn first_arg_is_version(args: &[OsString]) -> bool {
140    args.first().is_some_and(is_version_arg)
141}
142
143/// Print help or version text when the first CLI argument requests it.
144///
145/// Returns `true` when the caller should stop command execution.
146pub fn print_help_or_version(
147    args: &[OsString],
148    usage: impl FnOnce() -> String,
149    version_text: &str,
150) -> bool {
151    if first_arg_is_help(args) {
152        println!("{}", usage());
153        return true;
154    }
155    if first_arg_is_version(args) {
156        println!("{version_text}");
157        return true;
158    }
159    false
160}
161
162/// Print help for an exact nested command before parsing its required operands.
163pub fn print_nested_help(args: &[OsString], mut command: Command) -> bool {
164    let mut path = command
165        .get_bin_name()
166        .unwrap_or_else(|| command.get_name())
167        .to_string();
168    for arg in args {
169        if is_help_arg(arg) {
170            println!("{}", command.bin_name(path).render_help());
171            return true;
172        }
173        let Some(child) = command
174            .get_subcommands()
175            .find(|child| Some(child.get_name()) == arg.to_str())
176            .cloned()
177        else {
178            return false;
179        };
180        path.push(' ');
181        path.push_str(child.get_name());
182        command = child;
183    }
184    false
185}
186
187#[must_use]
188/// Build the top-level Clap command used for public help rendering.
189pub fn top_level_command() -> Command {
190    let command = Command::new("canic")
191        .version(env!("CARGO_PKG_VERSION"))
192        .about("Operator CLI for current Canic Apps and Fleets")
193        .color(ColorChoice::Always)
194        .subcommand_required(true)
195        .arg(icp_arg())
196        .arg(environment_arg())
197        .subcommand_help_heading("Commands")
198        .help_template(TOP_LEVEL_HELP_TEMPLATE)
199        .before_help(format!(
200            "{}Commands:{}\n{}",
201            COLOR_HEADING,
202            COLOR_RESET,
203            command_section(COMMAND_SPECS).join("\n")
204        ))
205        .after_help(format!(
206            "\n{}Tip:{} Run {} for command-specific help.",
207            COLOR_TIP,
208            COLOR_RESET,
209            color(COLOR_COMMAND, "`canic <command> --help`")
210        ));
211
212    COMMAND_SPECS.iter().fold(command, |command, spec| {
213        command.subcommand(
214            Command::new(spec.name)
215                .about(spec.about)
216                .disable_help_flag(true)
217                .disable_version_flag(true)
218                .arg(
219                    Arg::new(DISPATCH_ARGS)
220                        .num_args(0..)
221                        .allow_hyphen_values(true)
222                        .trailing_var_arg(true)
223                        .value_parser(clap::value_parser!(OsString))
224                        .hide(true),
225                ),
226        )
227    })
228}
229
230/// Render Canic's custom colorized top-level usage text.
231#[cfg(test)]
232pub fn usage() -> String {
233    let help = top_level_command().render_help();
234    help.ansi().to_string()
235}
236
237fn command_section(specs: &[CommandSpec]) -> Vec<String> {
238    specs
239        .iter()
240        .map(|spec| {
241            let command = format!("{:<12}", spec.name);
242            format!("  {} {}", color(COLOR_COMMAND, &command), spec.about)
243        })
244        .collect()
245}
246
247fn color(code: &str, text: &str) -> String {
248    format!("{code}{text}{COLOR_RESET}")
249}
250
251// -----------------------------------------------------------------------------
252// Tests
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    // Ensure top-level usage keeps the intended help colors.
259    #[test]
260    fn usage_contains_help_colors() {
261        let text = usage();
262
263        assert!(text.contains(COLOR_HEADING));
264        assert!(text.contains(COLOR_COMMAND));
265    }
266
267    #[test]
268    fn first_arg_help_and_version_detection_accepts_flags() {
269        assert!(first_arg_is_help(&[OsString::from("--help")]));
270        assert!(first_arg_is_help(&[OsString::from("-h")]));
271        assert!(first_arg_is_version(&[OsString::from("--version")]));
272        assert!(first_arg_is_version(&[OsString::from("-V")]));
273    }
274}