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#[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
58pub fn run_from_env() -> Result<(), CliError> {
60 run(std::env::args_os().skip(1))
61}
62
63pub 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#[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#[must_use]
128pub const fn version_text() -> &'static str {
129 VERSION_TEXT
130}
131
132fn 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 #[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 #[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 #[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}