Skip to main content

canic_cli/cli/
help.rs

1//! Module: canic_cli::cli::help
2//!
3//! Responsibility: render top-level CLI help and detect 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::{environment_arg, icp_arg};
8use clap::{Arg, ArgAction, Command};
9use std::ffi::OsString;
10
11const TOP_LEVEL_HELP_TEMPLATE: &str = "{name} {version}\n{about-with-newline}\n{usage-heading} {usage}\n\n{before-help}Options:\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: "app",
28        about: "Manage Canic source apps and roles",
29    },
30    CommandSpec {
31        name: "auth",
32        about: "Run delegated-auth operator workflows",
33    },
34    CommandSpec {
35        name: "backup",
36        about: "Plan, inspect, and verify backups",
37    },
38    CommandSpec {
39        name: "blob-storage",
40        about: "Inspect and provision blob-storage billing",
41    },
42    CommandSpec {
43        name: "build",
44        about: "Build one Canic canister artifact",
45    },
46    CommandSpec {
47        name: "cycles",
48        about: "Wrap ICP cycles balance and transfer commands",
49    },
50    CommandSpec {
51        name: "deploy",
52        about: "Plan and check deployment truth before mutation",
53    },
54    CommandSpec {
55        name: "evidence",
56        about: "Evaluate stable evidence envelopes",
57    },
58    CommandSpec {
59        name: "info",
60        about: "Query deployed canister information",
61    },
62    CommandSpec {
63        name: "inspect",
64        about: "Inspect runtime-observed status for one deployed canister",
65    },
66    CommandSpec {
67        name: "install",
68        about: "Install and bootstrap a Canic fleet",
69    },
70    CommandSpec {
71        name: "medic",
72        about: "Diagnose workspace and Fleet preflight readiness",
73    },
74    CommandSpec {
75        name: "network",
76        about: "Enroll canonical network trust identities",
77    },
78    CommandSpec {
79        name: "replica",
80        about: "Manage the local ICP replica",
81    },
82    CommandSpec {
83        name: "restore",
84        about: "Plan or run snapshot restores",
85    },
86    CommandSpec {
87        name: "scaffold",
88        about: "Scaffold Canic source files",
89    },
90    CommandSpec {
91        name: "state",
92        about: "Audit declared Canic state metadata",
93    },
94    CommandSpec {
95        name: "status",
96        about: "Show quick local workspace status",
97    },
98    CommandSpec {
99        name: "token",
100        about: "Wrap ICP token balance and transfer commands",
101    },
102];
103
104fn is_help_arg(arg: &OsString) -> bool {
105    arg.to_str()
106        .is_some_and(|arg| matches!(arg, "--help" | "-h"))
107}
108
109fn is_version_arg(arg: &OsString) -> bool {
110    arg.to_str()
111        .is_some_and(|arg| matches!(arg, "--version" | "-V"))
112}
113
114/// Return whether the first CLI argument requests help.
115pub fn first_arg_is_help(args: &[OsString]) -> bool {
116    args.first().is_some_and(is_help_arg)
117}
118
119fn first_arg_is_version(args: &[OsString]) -> bool {
120    args.first().is_some_and(is_version_arg)
121}
122
123/// Print help or version text when the first CLI argument requests it.
124///
125/// Returns `true` when the caller should stop command execution.
126pub fn print_help_or_version(
127    args: &[OsString],
128    usage: impl FnOnce() -> String,
129    version_text: &str,
130) -> bool {
131    if first_arg_is_help(args) {
132        println!("{}", usage());
133        return true;
134    }
135    if first_arg_is_version(args) {
136        println!("{version_text}");
137        return true;
138    }
139    false
140}
141
142#[must_use]
143/// Build the top-level Clap command used for public help rendering.
144pub fn top_level_command() -> Command {
145    let command = Command::new("canic")
146        .version(env!("CARGO_PKG_VERSION"))
147        .about("Operator CLI for Canic Apps, Fleets, backups, and ICP wallet workflows")
148        .disable_version_flag(true)
149        .arg(
150            Arg::new("version")
151                .short('V')
152                .long("version")
153                .action(ArgAction::SetTrue)
154                .help("Print version"),
155        )
156        .arg(icp_arg().global(true))
157        .arg(environment_arg().global(true))
158        .subcommand_help_heading("Commands")
159        .help_template(TOP_LEVEL_HELP_TEMPLATE)
160        .before_help(command_section(COMMAND_SPECS).join("\n"))
161        .after_help("Run `canic <command> --help` for command-specific help.");
162
163    COMMAND_SPECS.iter().fold(command, |command, spec| {
164        command.subcommand(Command::new(spec.name).about(spec.about))
165    })
166}
167
168/// Render Canic's custom colorized top-level usage text.
169pub fn usage() -> String {
170    let mut lines = vec![
171        color(
172            COLOR_HEADING,
173            &format!("Canic Operator CLI v{}", env!("CARGO_PKG_VERSION")),
174        ),
175        String::new(),
176        "Usage: canic [OPTIONS] <COMMAND>".to_string(),
177        String::new(),
178        color(COLOR_HEADING, "Commands:"),
179    ];
180    lines.extend(command_section(COMMAND_SPECS));
181    lines.extend([
182        String::new(),
183        color(COLOR_HEADING, "Options:"),
184        "      --icp <path>      Path to the icp executable for ICP-backed commands".to_string(),
185        "      --environment <name>  ICP environment for ICP-backed commands".to_string(),
186        "  -V, --version  Print version".to_string(),
187        "  -h, --help     Print help".to_string(),
188        String::new(),
189        format!(
190            "{}Tip:{} Run {} for command-specific help.",
191            COLOR_TIP,
192            COLOR_RESET,
193            color(COLOR_COMMAND, "`canic <command> --help`")
194        ),
195    ]);
196    lines.join("\n")
197}
198
199fn command_section(specs: &[CommandSpec]) -> Vec<String> {
200    specs
201        .iter()
202        .map(|spec| {
203            let command = format!("{:<12}", spec.name);
204            format!("  {} {}", color(COLOR_COMMAND, &command), spec.about)
205        })
206        .collect()
207}
208
209fn color(code: &str, text: &str) -> String {
210    format!("{code}{text}{COLOR_RESET}")
211}
212
213// -----------------------------------------------------------------------------
214// Tests
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    // Ensure top-level usage keeps the intended help colors.
221    #[test]
222    fn usage_contains_help_colors() {
223        let text = usage();
224
225        assert!(text.contains(COLOR_HEADING));
226        assert!(text.contains(COLOR_COMMAND));
227    }
228
229    #[test]
230    fn first_arg_help_and_version_detection_accepts_flags() {
231        assert!(first_arg_is_help(&[OsString::from("--help")]));
232        assert!(first_arg_is_help(&[OsString::from("-h")]));
233        assert!(first_arg_is_version(&[OsString::from("--version")]));
234        assert!(first_arg_is_version(&[OsString::from("-V")]));
235    }
236}