Skip to main content

canic_cli/
lib.rs

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///
45/// CliError
46///
47
48#[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
108/// Run the CLI from process arguments.
109pub fn run_from_env() -> Result<(), CliError> {
110    run(std::env::args_os().skip(1))
111}
112
113/// Run the CLI from an argument iterator.
114pub 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;