Skip to main content

canic_cli/
lib.rs

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