Skip to main content

canic_cli/
lib.rs

1pub mod backup;
2pub mod fleets;
3pub mod install;
4pub mod list;
5pub mod manifest;
6pub mod restore;
7pub mod snapshot;
8
9mod output;
10
11use clap::{Arg, ArgAction, Command};
12use std::ffi::OsString;
13use thiserror::Error as ThisError;
14
15const VERSION_TEXT: &str = concat!("canic ", env!("CARGO_PKG_VERSION"));
16
17///
18/// CliError
19///
20
21#[derive(Debug, ThisError)]
22pub enum CliError {
23    #[error("{0}")]
24    Usage(String),
25
26    #[error(transparent)]
27    Backup(#[from] backup::BackupCommandError),
28
29    #[error(transparent)]
30    Install(#[from] install::InstallCommandError),
31
32    #[error(transparent)]
33    Fleets(#[from] fleets::FleetCommandError),
34
35    #[error(transparent)]
36    List(#[from] list::ListCommandError),
37
38    #[error(transparent)]
39    Manifest(#[from] manifest::ManifestCommandError),
40
41    #[error(transparent)]
42    Snapshot(#[from] snapshot::SnapshotCommandError),
43
44    #[error(transparent)]
45    Restore(#[from] restore::RestoreCommandError),
46}
47
48/// Run the CLI from process arguments.
49pub fn run_from_env() -> Result<(), CliError> {
50    run(std::env::args_os().skip(1))
51}
52
53/// Run the CLI from an argument iterator.
54pub fn run<I>(args: I) -> Result<(), CliError>
55where
56    I: IntoIterator<Item = OsString>,
57{
58    let args = args.into_iter().collect::<Vec<_>>();
59    if args
60        .iter()
61        .filter_map(|arg| arg.to_str())
62        .any(|arg| matches!(arg, "version" | "--version" | "-V"))
63    {
64        println!("{}", version_text());
65        return Ok(());
66    }
67
68    let mut args = args.into_iter();
69    let Some(command) = args.next().and_then(|arg| arg.into_string().ok()) else {
70        return Err(CliError::Usage(usage()));
71    };
72
73    match command.as_str() {
74        "backup" => backup::run(args).map_err(CliError::from),
75        "fleets" => fleets::run(args).map_err(CliError::from),
76        "install" => install::run(args).map_err(CliError::from),
77        "list" => list::run(args).map_err(CliError::from),
78        "manifest" => manifest::run(args).map_err(CliError::from),
79        "snapshot" => snapshot::run(args).map_err(CliError::from),
80        "restore" => restore::run(args).map_err(CliError::from),
81        "use" => fleets::run_use(args).map_err(CliError::from),
82        "help" | "--help" | "-h" => {
83            println!("{}", usage());
84            Ok(())
85        }
86        _ => Err(CliError::Usage(usage())),
87    }
88}
89
90/// Build the top-level command metadata.
91#[must_use]
92pub fn top_level_command() -> Command {
93    Command::new("canic")
94        .about("Operator CLI for Canic install, backup, and restore workflows")
95        .disable_version_flag(true)
96        .arg(
97            Arg::new("version")
98                .short('V')
99                .long("version")
100                .action(ArgAction::SetTrue)
101                .help("Print version"),
102        )
103        .subcommand(Command::new("install").about("Install and bootstrap a Canic fleet"))
104        .subcommand(Command::new("fleets").about("List installed Canic fleets"))
105        .subcommand(Command::new("use").about("Select the current Canic fleet"))
106        .subcommand(Command::new("list").about("Show registry canisters as a tree table"))
107        .subcommand(Command::new("snapshot").about("Capture and download canister snapshots"))
108        .subcommand(
109            Command::new("backup")
110                .about("Inspect, verify, preflight, or smoke-check a backup directory"),
111        )
112        .subcommand(Command::new("manifest").about("Validate fleet backup manifests"))
113        .subcommand(
114            Command::new("restore").about("Plan, preview, summarize, or run restore journals"),
115        )
116        .after_help("Run `canic <command> help` for command-specific help.")
117}
118
119/// Return the CLI version banner.
120#[must_use]
121pub const fn version_text() -> &'static str {
122    VERSION_TEXT
123}
124
125// Return the top-level usage text.
126fn usage() -> String {
127    let mut command = top_level_command();
128    command.render_help().to_string()
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    // Ensure top-level help stays compact as command surfaces grow.
136    #[test]
137    fn usage_lists_command_families() {
138        let text = usage();
139
140        assert!(text.contains("Usage: canic"));
141        assert!(text.contains("list"));
142        assert!(text.contains("fleets"));
143        assert!(text.contains("use"));
144        assert!(text.contains("install"));
145        assert!(text.contains("snapshot"));
146        assert!(text.contains("backup"));
147        assert!(text.contains("manifest"));
148        assert!(text.contains("restore"));
149        assert!(text.contains("canic <command> help"));
150    }
151
152    // Ensure command-family help paths return successfully instead of erroring.
153    #[test]
154    fn command_family_help_returns_ok() {
155        assert!(run([OsString::from("backup"), OsString::from("help")]).is_ok());
156        assert!(run([OsString::from("install"), OsString::from("help")]).is_ok());
157        assert!(run([OsString::from("fleets"), OsString::from("help")]).is_ok());
158        assert!(run([OsString::from("list"), OsString::from("help")]).is_ok());
159        assert!(run([OsString::from("restore"), OsString::from("help")]).is_ok());
160        assert!(run([OsString::from("manifest"), OsString::from("help")]).is_ok());
161        assert!(run([OsString::from("snapshot"), OsString::from("help")]).is_ok());
162        assert!(run([OsString::from("use"), OsString::from("help")]).is_ok());
163    }
164
165    // Ensure version flags are accepted at the top level and command-family level.
166    #[test]
167    fn version_flags_return_ok() {
168        assert_eq!(version_text(), concat!("canic ", env!("CARGO_PKG_VERSION")));
169        assert!(run([OsString::from("--version")]).is_ok());
170        assert!(run([OsString::from("backup"), OsString::from("--version")]).is_ok());
171        assert!(run([OsString::from("install"), OsString::from("--version")]).is_ok());
172        assert!(run([OsString::from("fleets"), OsString::from("--version")]).is_ok());
173        assert!(run([OsString::from("list"), OsString::from("--version")]).is_ok());
174        assert!(run([OsString::from("restore"), OsString::from("--version")]).is_ok());
175        assert!(run([OsString::from("manifest"), OsString::from("--version")]).is_ok());
176        assert!(run([OsString::from("snapshot"), OsString::from("--version")]).is_ok());
177        assert!(run([OsString::from("use"), OsString::from("--version")]).is_ok());
178        assert!(
179            run([
180                OsString::from("backup"),
181                OsString::from("preflight"),
182                OsString::from("--version")
183            ])
184            .is_ok()
185        );
186    }
187}