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#[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
48pub fn run_from_env() -> Result<(), CliError> {
50 run(std::env::args_os().skip(1))
51}
52
53pub 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#[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#[must_use]
121pub const fn version_text() -> &'static str {
122 VERSION_TEXT
123}
124
125fn 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 #[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 #[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 #[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}