1use 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#[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 one compact Canic diagnostic code",
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
134pub 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
143pub 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
162pub 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]
188pub 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#[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#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[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}