1mod admission;
2mod apps;
3mod auth;
4mod backup;
5mod build;
6mod cli;
7mod component;
8mod cycles;
9mod diagnostic;
10mod endpoints;
11mod evidence;
12mod evidence_support;
13mod fleet;
14mod frontend;
15mod info;
16mod info_env;
17mod info_subnets;
18mod inspect;
19mod list;
20mod medic;
21mod metrics;
22mod network;
23mod observatory;
24mod output;
25mod replica;
26mod restore;
27mod scaffold;
28mod state;
29mod status;
30mod support;
31#[cfg(test)]
32mod test_support;
33mod token;
34mod toolchain;
35
36use crate::cli::{
37 argv::trace_if_enabled,
38 clap::parse_matches,
39 globals::{
40 DISPATCH_ARGS, apply_global_environment, apply_global_icp, global_environment_conflict,
41 misplaced_global_option,
42 },
43};
44use clap::error::ErrorKind;
45pub use cli::top_level_command;
46pub use frontend::FrontendCommandError;
47use std::ffi::OsString;
48use thiserror::Error as ThisError;
49
50const VERSION_TEXT: &str = concat!("canic ", env!("CARGO_PKG_VERSION"));
51
52#[derive(Debug, ThisError)]
57pub enum CliError {
58 #[error("admission: {0}")]
59 Admission(#[source] Box<admission::AdmissionCommandError>),
60
61 #[error(transparent)]
62 Clap(#[from] clap::Error),
63
64 #[error("build: {0}")]
65 Build(#[from] build::BuildCommandError),
66
67 #[error("component: {0}")]
68 Component(#[source] Box<component::ComponentCommandError>),
69
70 #[error("cycles: {0}")]
71 Cycles(#[source] Box<cycles::CyclesCommandError>),
72
73 #[error("backup: {0}")]
74 Backup(#[source] Box<backup::BackupCommandError>),
75
76 #[error("auth: {0}")]
77 Auth(#[source] Box<auth::AuthCommandError>),
78
79 #[error("diagnostic: {0}")]
80 Diagnostic(#[from] diagnostic::DiagnosticCommandError),
81
82 #[error("evidence: {0}")]
83 Evidence(#[from] evidence::EvidenceCommandError),
84
85 #[error("fleet: {0}")]
86 Fleet(#[source] Box<fleet::FleetCommandError>),
87
88 #[error("frontend: {0}")]
89 Frontend(#[source] Box<frontend::FrontendCommandError>),
90
91 #[error("info: {0}")]
92 Info(#[from] info::InfoCommandError),
93
94 #[error("inspect: {0}")]
95 Inspect(#[source] Box<inspect::InspectCommandError>),
96
97 #[error("medic: {0}")]
98 Medic(#[source] Box<medic::MedicCommandError>),
99
100 #[error("network: {0}")]
101 Network(#[from] network::NetworkCommandError),
102
103 #[error("observatory: {0}")]
104 Observatory(#[from] observatory::ObservatoryCommandError),
105
106 #[error("app: {0}")]
107 Apps(#[source] Box<apps::AppCommandError>),
108
109 #[error("state: {0}")]
110 State(#[from] state::StateCommandError),
111
112 #[error("status: {0}")]
113 Status(#[from] status::StatusCommandError),
114
115 #[error("token: {0}")]
116 Token(#[source] Box<token::TokenCommandError>),
117
118 #[error("toolchain: {0}")]
119 Toolchain(#[from] toolchain::ToolchainCommandError),
120
121 #[error("scaffold: {0}")]
122 Scaffold(#[from] scaffold::ScaffoldCommandError),
123
124 #[error("replica: {0}")]
125 Replica(#[from] replica::ReplicaCommandError),
126
127 #[error("restore: {0}")]
128 Restore(#[from] restore::RestoreCommandError),
129}
130
131impl From<apps::AppCommandError> for CliError {
132 fn from(error: apps::AppCommandError) -> Self {
133 Self::Apps(Box::new(error))
134 }
135}
136
137impl From<admission::AdmissionCommandError> for CliError {
138 fn from(error: admission::AdmissionCommandError) -> Self {
139 Self::Admission(Box::new(error))
140 }
141}
142
143impl From<auth::AuthCommandError> for CliError {
144 fn from(error: auth::AuthCommandError) -> Self {
145 Self::Auth(Box::new(error))
146 }
147}
148
149impl From<backup::BackupCommandError> for CliError {
150 fn from(error: backup::BackupCommandError) -> Self {
151 Self::Backup(Box::new(error))
152 }
153}
154
155impl From<cycles::CyclesCommandError> for CliError {
156 fn from(error: cycles::CyclesCommandError) -> Self {
157 Self::Cycles(Box::new(error))
158 }
159}
160
161impl From<fleet::FleetCommandError> for CliError {
162 fn from(error: fleet::FleetCommandError) -> Self {
163 Self::Fleet(Box::new(error))
164 }
165}
166
167impl From<inspect::InspectCommandError> for CliError {
168 fn from(error: inspect::InspectCommandError) -> Self {
169 Self::Inspect(Box::new(error))
170 }
171}
172
173impl From<medic::MedicCommandError> for CliError {
174 fn from(error: medic::MedicCommandError) -> Self {
175 Self::Medic(Box::new(error))
176 }
177}
178
179impl From<token::TokenCommandError> for CliError {
180 fn from(error: token::TokenCommandError) -> Self {
181 Self::Token(Box::new(error))
182 }
183}
184
185pub fn run_from_env() -> Result<(), CliError> {
187 let argv = std::env::args_os().collect::<Vec<_>>();
188 trace_if_enabled(&argv);
189 run(argv.into_iter().skip(1))
190}
191
192pub fn run<I>(args: I) -> Result<(), CliError>
194where
195 I: IntoIterator<Item = OsString>,
196{
197 let mut args = args.into_iter().collect::<Vec<_>>();
198 if args.is_empty() {
199 args.push(OsString::from("--help"));
200 }
201 if let Some(option) = misplaced_global_option(&args) {
202 let error = top_level_command().error(
203 ErrorKind::UnknownArgument,
204 format!("unexpected argument '{option}' found; put it before the command"),
205 );
206 return Err(CliError::Clap(error));
207 }
208 let matches = match parse_matches(top_level_command(), args) {
209 Ok(matches) => matches,
210 Err(error) if !error.use_stderr() => {
211 let _ = error.print();
212 return Ok(());
213 }
214 Err(error) => return Err(CliError::Clap(error)),
215 };
216 let global_icp = cli::clap::string_option(&matches, "icp");
217 let global_environment = cli::clap::string_option(&matches, "environment");
218
219 let (command, subcommand_matches) = matches
220 .subcommand()
221 .unwrap_or_else(|| unreachable!("Clap requires one top-level command"));
222 let mut tail = subcommand_matches
223 .get_many::<OsString>(DISPATCH_ARGS)
224 .map(|values| values.cloned().collect::<Vec<_>>())
225 .unwrap_or_default();
226 if let Some(conflict) =
227 global_environment_conflict(command, &tail, global_environment.as_deref())
228 {
229 return Err(CliError::Clap(
230 top_level_command().error(ErrorKind::InvalidValue, conflict.to_string()),
231 ));
232 }
233 apply_global_icp(command, &mut tail, global_icp);
234 apply_global_environment(command, &mut tail, global_environment);
235 let tail = tail.into_iter();
236
237 match command {
238 "admission" => admission::run(tail).map_err(CliError::from),
239 "app" => apps::run(tail).map_err(CliError::from),
240 "auth" => auth::run(tail).map_err(CliError::from),
241 "backup" => backup::run(tail).map_err(CliError::from),
242 "build" => build::run(tail).map_err(CliError::from),
243 "component" => component::run(tail).map_err(|error| CliError::Component(Box::new(error))),
244 "cycles" => cycles::run(tail).map_err(CliError::from),
245 "diagnostic" => diagnostic::run(tail).map_err(CliError::from),
246 "evidence" => evidence::run(tail).map_err(CliError::from),
247 "fleet" => fleet::run(tail).map_err(CliError::from),
248 "frontend" => frontend::run(tail).map_err(|error| CliError::Frontend(Box::new(error))),
249 "info" => info::run(tail).map_err(CliError::from),
250 "inspect" => inspect::run(tail).map_err(CliError::from),
251 "medic" => medic::run(tail).map_err(CliError::from),
252 "network" => network::run(tail).map_err(CliError::from),
253 "observatory" => observatory::run(tail).map_err(CliError::from),
254 "replica" => replica::run(tail).map_err(CliError::from),
255 "restore" => restore::run(tail).map_err(CliError::from),
256 "scaffold" => scaffold::run(tail).map_err(CliError::from),
257 "state" => state::run(tail).map_err(CliError::from),
258 "status" => status::run(tail).map_err(CliError::from),
259 "token" => token::run(tail).map_err(CliError::from),
260 "toolchain" => toolchain::run(tail).map_err(CliError::from),
261 _ => unreachable!("top-level dispatch command only defines known commands"),
262 }
263}
264
265#[must_use]
266pub const fn version_text() -> &'static str {
267 VERSION_TEXT
268}
269
270#[must_use]
271pub fn render_cli_error(error: &CliError) -> String {
272 match error {
273 CliError::Fleet(err)
274 if matches!(err.as_ref(), fleet::FleetCommandError::JsonReported { .. }) =>
275 {
276 err.to_string()
277 }
278 CliError::Build(build::BuildCommandError::Clap(err)) | CliError::Clap(err) => {
279 err.to_string().trim_end().to_string()
280 }
281 CliError::Inspect(err) if err.suppress_stderr() => String::new(),
282 CliError::Medic(err) if err.suppress_stderr() => String::new(),
283 CliError::State(err) if err.suppress_stderr() => String::new(),
284 _ => error.to_string(),
285 }
286}
287
288#[must_use]
289pub fn cli_error_exit_code(err: &CliError) -> i32 {
290 match err {
291 CliError::Auth(err) => i32::from(err.exit_code()),
292 CliError::Build(err) => err.exit_code(),
293 CliError::Clap(err) => err.exit_code(),
294 CliError::Info(err) => i32::from(err.exit_code()),
295 CliError::Inspect(err) => i32::from(err.exit_code()),
296 CliError::Medic(err) => i32::from(err.exit_code()),
297 CliError::State(err) => i32::from(err.exit_code()),
298 _ => 1,
299 }
300}
301
302#[cfg(test)]
303mod tests;