1mod auth;
2mod backup;
3mod blob_storage;
4mod build;
5mod cli;
6mod cycles;
7mod deploy;
8mod endpoints;
9mod evidence;
10mod evidence_support;
11mod fleets;
12mod info;
13mod info_env;
14mod install;
15mod list;
16mod medic;
17mod metrics;
18mod output;
19mod replica;
20mod restore;
21mod scaffold;
22mod snapshot;
23mod state;
24mod status;
25mod support;
26#[cfg(test)]
27mod test_support;
28mod token;
29
30use crate::cli::{
31 clap::parse_matches,
32 globals::{
33 DISPATCH_ARGS, apply_global_icp, apply_global_network, command_local_global_option,
34 top_level_dispatch_command,
35 },
36 help::{first_arg_is_help, usage},
37};
38pub use cli::top_level_command;
39use std::ffi::OsString;
40use thiserror::Error as ThisError;
41
42const VERSION_TEXT: &str = concat!("canic ", env!("CARGO_PKG_VERSION"));
43
44#[derive(Debug, ThisError)]
49pub enum CliError {
50 #[error("{0}")]
51 Usage(String),
52
53 #[error("backup: {0}")]
54 Backup(#[from] backup::BackupCommandError),
55
56 #[error("auth: {0}")]
57 Auth(#[from] auth::AuthCommandError),
58
59 #[error("blob-storage: {0}")]
60 BlobStorage(#[from] blob_storage::BlobStorageCommandError),
61
62 #[error("build: {0}")]
63 Build(#[from] build::BuildCommandError),
64
65 #[error("cycles: {0}")]
66 Cycles(#[from] cycles::CyclesCommandError),
67
68 #[error("deploy: {0}")]
69 Deploy(#[from] deploy::DeployCommandError),
70
71 #[error("evidence: {0}")]
72 Evidence(#[from] evidence::EvidenceCommandError),
73
74 #[error("install: {0}")]
75 Install(#[from] install::InstallCommandError),
76
77 #[error("info: {0}")]
78 Info(#[from] info::InfoCommandError),
79
80 #[error("medic: {0}")]
81 Medic(#[from] medic::MedicCommandError),
82
83 #[error("fleet: {0}")]
84 Fleets(#[from] fleets::FleetCommandError),
85
86 #[error("snapshot: {0}")]
87 Snapshot(#[from] snapshot::SnapshotCommandError),
88
89 #[error("state: {0}")]
90 State(#[from] state::StateCommandError),
91
92 #[error("scaffold: {0}")]
93 Scaffold(#[from] scaffold::ScaffoldCommandError),
94
95 #[error("restore: {0}")]
96 Restore(#[from] restore::RestoreCommandError),
97
98 #[error("replica: {0}")]
99 Replica(#[from] replica::ReplicaCommandError),
100
101 #[error("status: {0}")]
102 Status(#[from] status::StatusCommandError),
103
104 #[error("token: {0}")]
105 Token(#[from] token::TokenCommandError),
106}
107
108pub fn run_from_env() -> Result<(), CliError> {
110 run(std::env::args_os().skip(1))
111}
112
113pub fn run<I>(args: I) -> Result<(), CliError>
115where
116 I: IntoIterator<Item = OsString>,
117{
118 let args = args.into_iter().collect::<Vec<_>>();
119 if first_arg_is_help(&args) {
120 println!("{}", usage());
121 return Ok(());
122 }
123 if let Some(option) = command_local_global_option(&args) {
124 return Err(CliError::Usage(format!(
125 "{option} is a top-level option; put it before the command\n\n{}",
126 usage()
127 )));
128 }
129
130 let matches =
131 parse_matches(top_level_dispatch_command(), args).map_err(|_| CliError::Usage(usage()))?;
132 if matches.get_flag("version") {
133 println!("{}", version_text());
134 return Ok(());
135 }
136 let global_icp = cli::clap::string_option(&matches, "icp");
137 let global_network = cli::clap::string_option(&matches, "network");
138
139 let Some((command, subcommand_matches)) = matches.subcommand() else {
140 return Err(CliError::Usage(usage()));
141 };
142 let mut tail = subcommand_matches
143 .get_many::<OsString>(DISPATCH_ARGS)
144 .map(|values| values.cloned().collect::<Vec<_>>())
145 .unwrap_or_default();
146 apply_global_icp(command, &mut tail, global_icp);
147 apply_global_network(command, &mut tail, global_network);
148 let tail = tail.into_iter();
149
150 match command {
151 "backup" => backup::run(tail).map_err(CliError::from),
152 "auth" => auth::run(tail).map_err(CliError::from),
153 "blob-storage" => blob_storage::run(tail).map_err(CliError::from),
154 "build" => build::run(tail).map_err(CliError::from),
155 "cycles" => cycles::run(tail).map_err(CliError::from),
156 "deploy" => deploy::run(tail).map_err(CliError::from),
157 "evidence" => evidence::run(tail).map_err(CliError::from),
158 "fleet" => fleets::run(tail).map_err(CliError::from),
159 "info" => info::run(tail).map_err(CliError::from),
160 "install" => install::run(tail).map_err(CliError::from),
161 "medic" => medic::run(tail).map_err(CliError::from),
162 "replica" => replica::run(tail).map_err(CliError::from),
163 "scaffold" => scaffold::run(tail).map_err(CliError::from),
164 "snapshot" => snapshot::run(tail).map_err(CliError::from),
165 "state" => state::run(tail).map_err(CliError::from),
166 "status" => status::run(tail).map_err(CliError::from),
167 "token" => token::run(tail).map_err(CliError::from),
168 "restore" => restore::run(tail).map_err(CliError::from),
169 _ => unreachable!("top-level dispatch command only defines known commands"),
170 }
171}
172
173#[must_use]
174pub const fn version_text() -> &'static str {
175 VERSION_TEXT
176}
177
178#[must_use]
179pub fn render_cli_error(error: &CliError) -> String {
180 match error {
181 CliError::BlobStorage(err) => err.json_error_report().unwrap_or_else(|| error.to_string()),
182 CliError::Deploy(err) if err.suppress_stderr() => String::new(),
183 CliError::Medic(err) if err.suppress_stderr() => String::new(),
184 CliError::State(err) if err.suppress_stderr() => String::new(),
185 _ => error.to_string(),
186 }
187}
188
189#[must_use]
190pub fn cli_error_exit_code(err: &CliError) -> i32 {
191 match err {
192 CliError::Usage(_) => 2,
193 CliError::Auth(err) => i32::from(err.exit_code()),
194 CliError::BlobStorage(err) => i32::from(err.exit_code()),
195 CliError::Deploy(err) => i32::from(err.exit_code()),
196 CliError::Info(err) => i32::from(err.exit_code()),
197 CliError::Medic(err) => i32::from(err.exit_code()),
198 CliError::State(err) => i32::from(err.exit_code()),
199 _ => 1,
200 }
201}
202
203#[cfg(test)]
204mod tests;