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