1pub mod backup;
2pub mod manifest;
3pub mod restore;
4pub mod snapshot;
5
6use std::ffi::OsString;
7use thiserror::Error as ThisError;
8
9#[derive(Debug, ThisError)]
14pub enum CliError {
15 #[error("{0}")]
16 Usage(&'static str),
17
18 #[error(transparent)]
19 Backup(#[from] backup::BackupCommandError),
20
21 #[error(transparent)]
22 Manifest(#[from] manifest::ManifestCommandError),
23
24 #[error(transparent)]
25 Snapshot(#[from] snapshot::SnapshotCommandError),
26
27 #[error(transparent)]
28 Restore(#[from] restore::RestoreCommandError),
29}
30
31pub fn run_from_env() -> Result<(), CliError> {
33 run(std::env::args_os().skip(1))
34}
35
36pub fn run<I>(args: I) -> Result<(), CliError>
38where
39 I: IntoIterator<Item = OsString>,
40{
41 let mut args = args.into_iter();
42 let Some(command) = args.next().and_then(|arg| arg.into_string().ok()) else {
43 return Err(CliError::Usage(usage()));
44 };
45
46 match command.as_str() {
47 "backup" => backup::run(args).map_err(CliError::from),
48 "manifest" => manifest::run(args).map_err(CliError::from),
49 "snapshot" => snapshot::run(args).map_err(CliError::from),
50 "restore" => restore::run(args).map_err(CliError::from),
51 "help" | "--help" | "-h" => {
52 println!("{}", usage());
53 Ok(())
54 }
55 _ => Err(CliError::Usage(usage())),
56 }
57}
58
59const fn usage() -> &'static str {
61 "usage: canic snapshot download --canister <id> --out <dir> [--root <id> | --registry-json <file>] [--include-children] [--recursive] [--dry-run] [--stop-before-snapshot] [--resume-after-snapshot] [--network <name>]\n canic backup preflight --dir <backup-dir> --out-dir <dir> [--mapping <file>] [--require-restore-ready]\n canic backup inspect --dir <backup-dir> [--out <file>] [--require-ready]\n canic backup provenance --dir <backup-dir> [--out <file>] [--require-consistent]\n canic backup status --dir <backup-dir> [--out <file>] [--require-complete]\n canic backup verify --dir <backup-dir> [--out <file>]\n canic manifest validate --manifest <file> [--out <file>]\n canic restore plan (--manifest <file> | --backup-dir <dir>) [--mapping <file>] [--out <file>] [--require-verified] [--require-restore-ready]\n canic restore status --plan <file> [--out <file>]\n canic restore apply --plan <file> [--status <file>] [--backup-dir <dir>] --dry-run [--out <file>] [--journal-out <file>]\n canic restore apply-status --journal <file> [--out <file>] [--require-ready] [--require-no-pending] [--require-no-failed] [--require-complete] [--require-remaining-count <n>] [--require-attention-count <n>] [--require-completion-basis-points <n>] [--require-no-pending-before <text>]\n canic restore apply-report --journal <file> [--out <file>] [--require-no-attention] [--require-remaining-count <n>] [--require-attention-count <n>] [--require-completion-basis-points <n>] [--require-no-pending-before <text>]\n canic restore run --journal <file> (--dry-run | --execute | --unclaim-pending) [--dfx <path>] [--network <name>] [--max-steps <n>] [--updated-at <text>] [--out <file>] [--require-complete] [--require-no-attention] [--require-run-mode <text>] [--require-stopped-reason <text>] [--require-next-action <text>] [--require-executed-count <n>] [--require-receipt-count <n>] [--require-completed-receipt-count <n>] [--require-failed-receipt-count <n>] [--require-recovered-receipt-count <n>] [--require-receipt-updated-at <text>] [--require-state-updated-at <text>] [--require-batch-initial-ready-count <n>] [--require-batch-executed-count <n>] [--require-batch-remaining-ready-count <n>] [--require-batch-ready-delta <n>] [--require-batch-remaining-delta <n>] [--require-batch-stopped-by-max-steps true|false] [--require-remaining-count <n>] [--require-attention-count <n>] [--require-completion-basis-points <n>] [--require-no-pending-before <text>]\n canic restore apply-next --journal <file> [--out <file>]\n canic restore apply-command --journal <file> [--dfx <path>] [--network <name>] [--out <file>] [--require-command]\n canic restore apply-claim --journal <file> [--sequence <n>] [--updated-at <text>] [--out <file>]\n canic restore apply-unclaim --journal <file> [--sequence <n>] [--updated-at <text>] [--out <file>]\n canic restore apply-mark --journal <file> --sequence <n> --state completed|failed [--reason <text>] [--updated-at <text>] [--out <file>] [--require-pending]"
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
70 fn usage_lists_restore_readiness_gates() {
71 let text = usage();
72
73 assert!(text.contains(
74 "canic backup preflight --dir <backup-dir> --out-dir <dir> [--mapping <file>] [--require-restore-ready]"
75 ));
76 assert!(text.contains(
77 "canic restore plan (--manifest <file> | --backup-dir <dir>) [--mapping <file>] [--out <file>] [--require-verified] [--require-restore-ready]"
78 ));
79 assert!(text.contains("canic restore status --plan <file> [--out <file>]"));
80 assert!(text.contains(
81 "canic restore apply --plan <file> [--status <file>] [--backup-dir <dir>] --dry-run [--out <file>] [--journal-out <file>]"
82 ));
83 assert!(text.contains(
84 "canic restore apply-status --journal <file> [--out <file>] [--require-ready] [--require-no-pending] [--require-no-failed] [--require-complete] [--require-remaining-count <n>] [--require-attention-count <n>] [--require-completion-basis-points <n>] [--require-no-pending-before <text>]"
85 ));
86 assert!(text.contains(
87 "canic restore apply-report --journal <file> [--out <file>] [--require-no-attention] [--require-remaining-count <n>] [--require-attention-count <n>] [--require-completion-basis-points <n>] [--require-no-pending-before <text>]"
88 ));
89 assert!(text.contains("canic restore run --journal <file> (--dry-run | --execute | --unclaim-pending) [--dfx <path>] [--network <name>] [--max-steps <n>] [--updated-at <text>] [--out <file>] [--require-complete] [--require-no-attention] [--require-run-mode <text>] [--require-stopped-reason <text>] [--require-next-action <text>] [--require-executed-count <n>] [--require-receipt-count <n>] [--require-completed-receipt-count <n>] [--require-failed-receipt-count <n>] [--require-recovered-receipt-count <n>] [--require-receipt-updated-at <text>] [--require-state-updated-at <text>] [--require-batch-initial-ready-count <n>] [--require-batch-executed-count <n>] [--require-batch-remaining-ready-count <n>] [--require-batch-ready-delta <n>] [--require-batch-remaining-delta <n>] [--require-batch-stopped-by-max-steps true|false] [--require-remaining-count <n>] [--require-attention-count <n>] [--require-completion-basis-points <n>] [--require-no-pending-before <text>]"));
90 assert!(text.contains("canic restore apply-next --journal <file> [--out <file>]"));
91 assert!(text.contains(
92 "canic restore apply-command --journal <file> [--dfx <path>] [--network <name>] [--out <file>] [--require-command]"
93 ));
94 assert!(text.contains(
95 "canic restore apply-claim --journal <file> [--sequence <n>] [--updated-at <text>] [--out <file>]"
96 ));
97 assert!(text.contains(
98 "canic restore apply-unclaim --journal <file> [--sequence <n>] [--updated-at <text>] [--out <file>]"
99 ));
100 assert!(text.contains(
101 "canic restore apply-mark --journal <file> --sequence <n> --state completed|failed [--reason <text>] [--updated-at <text>] [--out <file>] [--require-pending]"
102 ));
103 }
104}