Skip to main content

canic_cli/
lib.rs

1pub mod backup;
2pub mod list;
3pub mod manifest;
4pub mod restore;
5pub mod snapshot;
6
7use std::ffi::OsString;
8use thiserror::Error as ThisError;
9
10///
11/// CliError
12///
13
14#[derive(Debug, ThisError)]
15pub enum CliError {
16    #[error("{0}")]
17    Usage(&'static str),
18
19    #[error(transparent)]
20    Backup(#[from] backup::BackupCommandError),
21
22    #[error(transparent)]
23    List(#[from] list::ListCommandError),
24
25    #[error(transparent)]
26    Manifest(#[from] manifest::ManifestCommandError),
27
28    #[error(transparent)]
29    Snapshot(#[from] snapshot::SnapshotCommandError),
30
31    #[error(transparent)]
32    Restore(#[from] restore::RestoreCommandError),
33}
34
35/// Run the CLI from process arguments.
36pub fn run_from_env() -> Result<(), CliError> {
37    run(std::env::args_os().skip(1))
38}
39
40/// Run the CLI from an argument iterator.
41pub fn run<I>(args: I) -> Result<(), CliError>
42where
43    I: IntoIterator<Item = OsString>,
44{
45    let mut args = args.into_iter();
46    let Some(command) = args.next().and_then(|arg| arg.into_string().ok()) else {
47        return Err(CliError::Usage(usage()));
48    };
49
50    match command.as_str() {
51        "backup" => backup::run(args).map_err(CliError::from),
52        "list" => list::run(args).map_err(CliError::from),
53        "manifest" => manifest::run(args).map_err(CliError::from),
54        "snapshot" => snapshot::run(args).map_err(CliError::from),
55        "restore" => restore::run(args).map_err(CliError::from),
56        "help" | "--help" | "-h" => {
57            println!("{}", usage());
58            Ok(())
59        }
60        _ => Err(CliError::Usage(usage())),
61    }
62}
63
64// Return the top-level usage text.
65const fn usage() -> &'static str {
66    "usage: canic <command> [<args>]\n\ncommands:\n  list       Show registry canisters as an ASCII tree.\n  snapshot   Capture and download canister snapshots.\n  backup     Inspect, verify, preflight, or smoke-check a backup directory.\n  manifest   Validate fleet backup manifests.\n  restore    Plan, preview, summarize, or run restore journals.\n\nhelp:\n  canic help\n  canic <command> help"
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    // Ensure top-level help stays compact as command surfaces grow.
74    #[test]
75    fn usage_lists_command_families_without_nested_flags() {
76        let text = usage();
77
78        assert!(text.contains("usage: canic <command> [<args>]"));
79        assert!(text.contains("list"));
80        assert!(text.contains("snapshot"));
81        assert!(text.contains("backup"));
82        assert!(text.contains("manifest"));
83        assert!(text.contains("restore"));
84        assert!(text.contains("canic <command> help"));
85        assert!(!text.contains("--require-batch-ready-delta"));
86        assert!(!text.contains("--require-no-pending-before"));
87    }
88
89    // Ensure command-family help paths return successfully instead of erroring.
90    #[test]
91    fn command_family_help_returns_ok() {
92        assert!(run([OsString::from("backup"), OsString::from("help")]).is_ok());
93        assert!(run([OsString::from("list"), OsString::from("help")]).is_ok());
94        assert!(run([OsString::from("restore"), OsString::from("help")]).is_ok());
95        assert!(run([OsString::from("manifest"), OsString::from("help")]).is_ok());
96        assert!(run([OsString::from("snapshot"), OsString::from("help")]).is_ok());
97    }
98}