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