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