Skip to main content

canic_cli/
lib.rs

1pub mod backup;
2pub mod manifest;
3pub mod restore;
4pub mod snapshot;
5
6use std::ffi::OsString;
7use thiserror::Error as ThisError;
8
9///
10/// CliError
11///
12
13#[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
31/// Run the CLI from process arguments.
32pub fn run_from_env() -> Result<(), CliError> {
33    run(std::env::args_os().skip(1))
34}
35
36/// Run the CLI from an argument iterator.
37pub 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
59// Return the top-level usage text.
60const 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>]\n       canic restore apply-next --journal <file> [--out <file>]\n       canic restore apply-mark --journal <file> --sequence <n> --state completed|failed [--reason <text>] [--out <file>]"
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    // Ensure top-level help stays aligned with restore-readiness gates.
69    #[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("canic restore apply-status --journal <file> [--out <file>]"));
84        assert!(text.contains("canic restore apply-next --journal <file> [--out <file>]"));
85        assert!(text.contains(
86            "canic restore apply-mark --journal <file> --sequence <n> --state completed|failed [--reason <text>] [--out <file>]"
87        ));
88    }
89}