Skip to main content

canic_cli/
lib.rs

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