1use 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#[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
114pub 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
123pub 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]
143pub 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
168pub 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#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[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}