Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
cli/sync: migrate to clap
Merged fintohaps opened 6 months ago

Due to the idiosyncracies of this command there are a few things to note about this migration.

The rad sync command acts as a default subcommand in itself, in that it has options that do not apply to its subcommand, rad sync status. This means that the options will print with rad sync --help, but they will not print with rad sync status.

The --inventory flag is not compatible with any of the other flags and options, thus they are all marked with conflicts_with. This cannot be done for a positional argument, so clap will “helpfully” print out the usage, for example, as:

error: the argument '--inventory' cannot be used with '--fetch'

Usage: rad sync --inventory [RID]

For more information, try '--help'.

Which is incorrect, since RID is ignored. This is explained in the --help documentation for --inventory.

4 files changed +311 -296 d1e19a87 3c895250
modified crates/radicle-cli/src/commands/help.rs
@@ -139,7 +139,10 @@ const COMMANDS: &[CommandItem] = &[
        name: "stats",
        about: crate::commands::stats::ABOUT,
    },
-
    CommandItem::Lexopt(crate::commands::sync::HELP),
+
    CommandItem::Clap {
+
        name: "sync",
+
        about: crate::commands::sync::ABOUT,
+
    },
    CommandItem::Clap {
        name: "watch",
        about: crate::commands::watch::ABOUT,
modified crates/radicle-cli/src/commands/sync.rs
@@ -1,9 +1,8 @@
+
mod args;
+

use std::cmp::Ordering;
use std::collections::BTreeMap;
-
use std::collections::BTreeSet;
use std::collections::HashSet;
-
use std::ffi::OsString;
-
use std::str::FromStr;
use std::time;

use anyhow::{anyhow, Context as _};
@@ -23,266 +22,14 @@ use radicle_term::Element;
use crate::node::SyncReporting;
use crate::node::SyncSettings;
use crate::terminal as term;
-
use crate::terminal::args::{Args, Error, Help};
use crate::terminal::format::Author;
use crate::terminal::{Table, TableOptions};

-
pub const HELP: Help = Help {
-
    name: "sync",
-
    description: "Sync repositories to the network",
-
    version: env!("RADICLE_VERSION"),
-
    usage: r#"
-
Usage
-

-
    rad sync [--fetch | --announce] [<rid>] [<option>...]
-
    rad sync --inventory [<option>...]
-
    rad sync status [<rid>] [<option>...]
-

-
    By default, the current repository is synchronized both ways.
-
    If an <rid> is specified, that repository is synced instead.
-

-
    The process begins by fetching changes from connected seeds,
-
    followed by announcing local refs to peers, thereby prompting
-
    them to fetch from us.
-

-
    When `--fetch` is specified, any number of seeds may be given
-
    using the `--seed` option, eg. `--seed <nid>@<addr>:<port>`.
-

-
    When `--replicas` is specified, the given replication factor will try
-
    to be matched. For example, `--replicas 5` will sync with 5 seeds.
-

-
    The synchronization process can be configured using `--replicas <min>` and
-
    `--replicas-max <max>`. If these options are used independently, then the
-
    replication factor is taken as the given `<min>`/`<max>` value. If the
-
    options are used together, then the replication factor has a minimum and
-
    maximum bound.
-

-
    For fetching, the synchronization process will be considered successful if
-
    at least `<min>` seeds were fetched from *or* all preferred seeds were
-
    fetched from. If `<max>` is specified then the process will continue and
-
    attempt to sync with `<max>` seeds.
-

-
    For reference announcing, the synchronization process will be considered
-
    successful if at least `<min>` seeds were pushed to *and* all preferred
-
    seeds were pushed to.
-

-
    When `--fetch` or `--announce` are specified on their own, this command
-
    will only fetch or announce.
-

-
    If `--inventory` is specified, the node's inventory is announced to
-
    the network. This mode does not take an `<rid>`.
-

-
Commands
-

-
    status                    Display the sync status of a repository
-

-
Options
-

-
        --sort-by       <field>   Sort the table by column (options: nid, alias, status)
-
    -f, --fetch                   Turn on fetching (default: true)
-
    -a, --announce                Turn on ref announcing (default: true)
-
    -i, --inventory               Turn on inventory announcing (default: false)
-
        --timeout       <secs>    How many seconds to wait while syncing
-
        --seed          <nid>     Sync with the given node (may be specified multiple times)
-
    -r, --replicas      <count>   Sync with a specific number of seeds
-
        --replicas-max  <count>   Sync with an upper bound number of seeds
-
    -v, --verbose                 Verbose output
-
        --debug                   Print debug information afer sync
-
        --help                    Print help
-
"#,
-
};
-

-
#[derive(Debug, Clone, PartialEq, Eq, Default)]
-
pub enum Operation {
-
    Synchronize(SyncMode),
-
    #[default]
-
    Status,
-
}
-

-
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
-
pub enum SortBy {
-
    Nid,
-
    Alias,
-
    #[default]
-
    Status,
-
}
-

-
impl FromStr for SortBy {
-
    type Err = &'static str;
-

-
    fn from_str(s: &str) -> Result<Self, Self::Err> {
-
        match s {
-
            "nid" => Ok(Self::Nid),
-
            "alias" => Ok(Self::Alias),
-
            "status" => Ok(Self::Status),
-
            _ => Err("invalid `--sort-by` field"),
-
        }
-
    }
-
}
-

-
#[derive(Debug, Clone, PartialEq, Eq)]
-
pub enum SyncMode {
-
    Repo {
-
        settings: SyncSettings,
-
        direction: SyncDirection,
-
    },
-
    Inventory,
-
}
-

-
impl Default for SyncMode {
-
    fn default() -> Self {
-
        Self::Repo {
-
            settings: SyncSettings::default(),
-
            direction: SyncDirection::default(),
-
        }
-
    }
-
}
-

-
#[derive(Debug, Default, PartialEq, Eq, Clone)]
-
pub enum SyncDirection {
-
    Fetch,
-
    Announce,
-
    #[default]
-
    Both,
-
}
-

-
#[derive(Default, Debug)]
-
pub struct Options {
-
    pub rid: Option<RepoId>,
-
    pub debug: bool,
-
    pub verbose: bool,
-
    pub sort_by: SortBy,
-
    pub op: Operation,
-
}
-

-
impl Args for Options {
-
    fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
-
        use lexopt::prelude::*;
-

-
        let mut parser = lexopt::Parser::from_args(args);
-
        let mut verbose = false;
-
        let mut timeout = time::Duration::from_secs(9);
-
        let mut rid = None;
-
        let mut fetch = false;
-
        let mut announce = false;
-
        let mut inventory = false;
-
        let mut debug = false;
-
        let mut replicas = None;
-
        let mut max_replicas = None;
-
        let mut seeds = BTreeSet::new();
-
        let mut sort_by = SortBy::default();
-
        let mut op: Option<Operation> = None;
-

-
        while let Some(arg) = parser.next()? {
-
            match arg {
-
                Long("debug") => {
-
                    debug = true;
-
                }
-
                Long("verbose") | Short('v') => {
-
                    verbose = true;
-
                }
-
                Long("fetch") | Short('f') => {
-
                    fetch = true;
-
                }
-
                Long("replicas") | Short('r') => {
-
                    let val = parser.value()?;
-
                    let count = term::args::number(&val)?;
-

-
                    if count == 0 {
-
                        anyhow::bail!("value for `--replicas` must be greater than zero");
-
                    }
-
                    replicas = Some(count);
-
                }
-
                Long("replicas-max") => {
-
                    let val = parser.value()?;
-
                    let count = term::args::number(&val)?;
-

-
                    if count == 0 {
-
                        anyhow::bail!("value for `--replicas-max` must be greater than zero");
-
                    }
-
                    max_replicas = Some(count);
-
                }
-
                Long("seed") => {
-
                    let val = parser.value()?;
-
                    let nid = term::args::nid(&val)?;
-

-
                    seeds.insert(nid);
-
                }
-
                Long("announce") | Short('a') => {
-
                    announce = true;
-
                }
-
                Long("inventory") | Short('i') => {
-
                    inventory = true;
-
                }
-
                Long("sort-by") if matches!(op, Some(Operation::Status)) => {
-
                    let value = parser.value()?;
-
                    sort_by = value.parse()?;
-
                }
-
                Long("timeout") | Short('t') => {
-
                    let value = parser.value()?;
-
                    let secs = term::args::parse_value("timeout", value)?;
-

-
                    timeout = time::Duration::from_secs(secs);
-
                }
-
                Long("help") | Short('h') => {
-
                    return Err(Error::Help.into());
-
                }
-
                Value(val) if rid.is_none() => match val.to_string_lossy().as_ref() {
-
                    "s" | "status" => {
-
                        op = Some(Operation::Status);
-
                    }
-
                    _ => {
-
                        rid = Some(term::args::rid(&val)?);
-
                    }
-
                },
-
                arg => {
-
                    return Err(anyhow!(arg.unexpected()));
-
                }
-
            }
-
        }
-

-
        let sync = if inventory && fetch {
-
            anyhow::bail!("`--inventory` cannot be used with `--fetch`");
-
        } else if inventory {
-
            SyncMode::Inventory
-
        } else {
-
            let direction = match (fetch, announce) {
-
                (true, true) | (false, false) => SyncDirection::Both,
-
                (true, false) => SyncDirection::Fetch,
-
                (false, true) => SyncDirection::Announce,
-
            };
-
            let mut settings = SyncSettings::default().timeout(timeout);
-

-
            let replicas = match (replicas, max_replicas) {
-
                (None, None) => sync::ReplicationFactor::default(),
-
                (None, Some(min)) => sync::ReplicationFactor::must_reach(min),
-
                (Some(min), None) => sync::ReplicationFactor::must_reach(min),
-
                (Some(min), Some(max)) => sync::ReplicationFactor::range(min, max),
-
            };
-
            settings.replicas = replicas;
-
            if !seeds.is_empty() {
-
                settings.seeds = seeds;
-
            }
-
            SyncMode::Repo {
-
                settings,
-
                direction,
-
            }
-
        };
-

-
        Ok((
-
            Options {
-
                rid,
-
                debug,
-
                verbose,
-
                sort_by,
-
                op: op.unwrap_or(Operation::Synchronize(sync)),
-
            },
-
            vec![],
-
        ))
-
    }
-
}
+
pub use args::Args;
+
pub(crate) use args::ABOUT;
+
use args::{Command, SortBy, SyncDirection, SyncMode};

-
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
+
pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
    let profile = ctx.profile()?;
    let mut node = radicle::Node::new(profile.socket());
    if !node.is_running() {
@@ -290,10 +37,12 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
            "to sync a repository, your node must be running. To start it, run `rad node start`"
        );
    }
+
    let verbose = args.verbose;
+
    let debug = args.verbose;

-
    match &options.op {
-
        Operation::Status => {
-
            let rid = match options.rid {
+
    match args.command {
+
        Some(Command::Status { rid, sort_by }) => {
+
            let rid = match rid {
                Some(rid) => rid,
                None => {
                    let (_, rid) = radicle::rad::cwd()
@@ -301,37 +50,41 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
                    rid
                }
            };
-
            sync_status(rid, &mut node, &profile, &options)?;
+
            sync_status(rid, &mut node, &profile, &sort_by, verbose)?;
        }
-
        Operation::Synchronize(SyncMode::Repo {
-
            settings,
-
            direction,
-
        }) => {
-
            let rid = match options.rid {
-
                Some(rid) => rid,
-
                None => {
-
                    let (_, rid) = radicle::rad::cwd()
-
                        .context("Current directory is not a Radicle repository")?;
-
                    rid
-
                }
-
            };
-
            let settings = settings.clone().with_profile(&profile);
+
        None => match SyncMode::from(args.sync) {
+
            SyncMode::Repo {
+
                rid,
+
                settings,
+
                direction,
+
            } => {
+
                let rid = match rid {
+
                    Some(rid) => rid,
+
                    None => {
+
                        let (_, rid) = radicle::rad::cwd()
+
                            .context("Current directory is not a Radicle repository")?;
+
                        rid
+
                    }
+
                };
+
                let settings = settings.clone().with_profile(&profile);

-
            if [SyncDirection::Fetch, SyncDirection::Both].contains(direction) {
-
                if !profile.policies()?.is_seeding(&rid)? {
-
                    anyhow::bail!("repository {rid} is not seeded");
+
                if matches!(direction, SyncDirection::Fetch | SyncDirection::Both) {
+
                    if !profile.policies()?.is_seeding(&rid)? {
+
                        anyhow::bail!("repository {rid} is not seeded");
+
                    }
+
                    let result = fetch(rid, settings.clone(), &mut node, &profile)?;
+
                    display_fetch_result(&result, verbose)
+
                }
+
                if matches!(direction, SyncDirection::Announce | SyncDirection::Both) {
+
                    announce_refs(rid, settings, &mut node, &profile, verbose, debug)?;
                }
-
                let result = fetch(rid, settings.clone(), &mut node, &profile)?;
-
                display_fetch_result(&result, options.verbose)
            }
-
            if [SyncDirection::Announce, SyncDirection::Both].contains(direction) {
-
                announce_refs(rid, settings, &mut node, &profile, &options)?;
+
            SyncMode::Inventory => {
+
                announce_inventory(node)?;
            }
-
        }
-
        Operation::Synchronize(SyncMode::Inventory) => {
-
            announce_inventory(node)?;
-
        }
+
        },
    }
+

    Ok(())
}

@@ -339,7 +92,8 @@ fn sync_status(
    rid: RepoId,
    node: &mut Node,
    profile: &Profile,
-
    options: &Options,
+
    sort_by: &SortBy,
+
    verbose: bool,
) -> anyhow::Result<()> {
    const SYMBOL_STATE: &str = "?";
    const SYMBOL_STATE_UNKNOWN: &str = "•";
@@ -358,7 +112,7 @@ fn sync_status(
    ]);
    table.divider();

-
    sort_seeds_by(local_nid, &mut seeds, &aliases, &options.sort_by);
+
    sort_seeds_by(local_nid, &mut seeds, &aliases, sort_by);

    let seeds = seeds.into_iter().flat_map(|seed| {
        let (status, head, time) = match seed.sync {
@@ -386,7 +140,7 @@ fn sync_status(
                term::format::oid(oid),
                term::format::timestamp(timestamp),
            ),
-
            None if options.verbose => (
+
            None if verbose => (
                term::format::dim(SYMBOL_STATE_UNKNOWN),
                term::paint(String::new()),
                term::paint(String::new()),
@@ -394,7 +148,7 @@ fn sync_status(
            None => return None,
        };

-
        let (alias, nid) = Author::new(&seed.nid, profile, options.verbose).labels();
+
        let (alias, nid) = Author::new(&seed.nid, profile, verbose).labels();

        Some([
            nid,
@@ -448,7 +202,8 @@ fn announce_refs(
    settings: SyncSettings,
    node: &mut Node,
    profile: &Profile,
-
    options: &Options,
+
    verbose: bool,
+
    debug: bool,
) -> anyhow::Result<()> {
    let Ok(repo) = profile.storage.repository(rid) else {
        return Err(anyhow!(
@@ -470,14 +225,14 @@ fn announce_refs(
        &repo,
        settings,
        SyncReporting {
-
            debug: options.debug,
+
            debug,
            ..SyncReporting::default()
        },
        node,
        profile,
    )?;
    if let Some(result) = result {
-
        print_announcer_result(&result, options.verbose)
+
        print_announcer_result(&result, verbose)
    }

    Ok(())
added crates/radicle-cli/src/commands/sync/args.rs
@@ -0,0 +1,254 @@
+
use std::str::FromStr;
+
use std::time;
+

+
use clap::{Parser, Subcommand, ValueEnum};
+

+
use radicle::{
+
    node::{sync, NodeId},
+
    prelude::RepoId,
+
};
+

+
use crate::node::SyncSettings;
+

+
pub(crate) const ABOUT: &str = "Sync repositories to the network";
+

+
const LONG_ABOUT: &str = r#"
+
By default, the current repository is synchronized both ways.
+
If an <RID> is specified, that repository is synced instead.
+

+
The process begins by fetching changes from connected seeds,
+
followed by announcing local refs to peers, thereby prompting
+
them to fetch from us.
+

+
When `--fetch` is specified, any number of seeds may be given
+
using the `--seed` option, eg. `--seed <NID>@<ADDR>:<PORT>`.
+

+
When `--replicas` is specified, the given replication factor will try
+
to be matched. For example, `--replicas 5` will sync with 5 seeds.
+

+
The synchronization process can be configured using `--replicas <MIN>` and
+
`--replicas-max <MAX>`. If these options are used independently, then the
+
replication factor is taken as the given `<MIN>`/`<MAX>` value. If the
+
options are used together, then the replication factor has a minimum and
+
maximum bound.
+

+
For fetching, the synchronization process will be considered successful if
+
at least `<MIN>` seeds were fetched from *or* all preferred seeds were
+
fetched from. If `<MAX>` is specified then the process will continue and
+
attempt to sync with `<MAX>` seeds.
+

+
For reference announcing, the synchronization process will be considered
+
successful if at least `<MIN>` seeds were pushed to *and* all preferred
+
seeds were pushed to.
+

+
When `--fetch` or `--announce` are specified on their own, this command
+
will only fetch or announce.
+

+
If `--inventory` is specified, the node's inventory is announced to
+
the network. This mode does not take an `<RID>`.
+
"#;
+

+
#[derive(Parser, Debug)]
+
#[clap(about = ABOUT, long_about = LONG_ABOUT, disable_version_flag = true)]
+
pub struct Args {
+
    #[clap(subcommand)]
+
    pub(super) command: Option<Command>,
+

+
    #[clap(flatten)]
+
    pub(super) sync: SyncArgs,
+

+
    /// Enable debug information when synchronizing
+
    #[arg(long)]
+
    pub(super) debug: bool,
+

+
    /// Enable verbose information when synchronizing
+
    #[arg(long, short)]
+
    pub(super) verbose: bool,
+
}
+

+
#[derive(Parser, Debug)]
+
pub(super) struct SyncArgs {
+
    /// Enable fetching [default: true]
+
    ///
+
    /// Providing --announce without --fetch will disable fetching
+
    #[arg(long, short, conflicts_with = "inventory")]
+
    fetch: bool,
+

+
    /// Enable announcing [default: true]
+
    ///
+
    /// Providing --fetch without --announce will disable announcing
+
    #[arg(long, short, conflicts_with = "inventory")]
+
    announce: bool,
+

+
    /// Synchronize with the given node (may be specified multiple times)
+
    #[arg(
+
        long = "seed",
+
        value_name = "NID",
+
        action = clap::ArgAction::Append,
+
        conflicts_with = "inventory",
+
    )]
+
    seeds: Vec<NodeId>,
+

+
    /// How many seconds to wait while synchronizing
+
    #[arg(
+
        long,
+
        short,
+
        default_value_t = 9,
+
        value_name = "SECS",
+
        conflicts_with = "inventory"
+
    )]
+
    timeout: u64,
+

+
    /// The repository to perform the synchronizing for [default: cwd]
+
    rid: Option<RepoId>,
+

+
    /// Synchronize with a specific number of seeds
+
    ///
+
    /// The value must be greater than zero
+
    #[arg(
+
        long,
+
        short,
+
        value_name = "COUNT",
+
        value_parser = replicas_non_zero,
+
        conflicts_with = "inventory",
+
    )]
+
    replicas: Option<usize>,
+

+
    /// Synchronize with an upper bound number of seeds
+
    ///
+
    /// The value must be greater than zero
+
    #[arg(
+
        long,
+
        value_name = "COUNT",
+
        value_parser = replicas_non_zero,
+
        conflicts_with = "inventory",
+
    )]
+
    max_replicas: Option<usize>,
+

+
    /// Enable announcing inventory [default: false]
+
    ///
+
    /// --inventory is a standalone mode and is not compatible with the other
+
    /// options
+
    ///
+
    /// <RID> is ignored when announcing --inventory
+
    #[arg(long, short)]
+
    inventory: bool,
+
}
+

+
impl SyncArgs {
+
    fn direction(&self) -> SyncDirection {
+
        match (self.fetch, self.announce) {
+
            (true, true) | (false, false) => SyncDirection::Both,
+
            (true, false) => SyncDirection::Fetch,
+
            (false, true) => SyncDirection::Announce,
+
        }
+
    }
+

+
    fn timeout(&self) -> time::Duration {
+
        time::Duration::from_secs(self.timeout)
+
    }
+

+
    fn replication(&self) -> sync::ReplicationFactor {
+
        match (self.replicas, self.max_replicas) {
+
            (None, None) => sync::ReplicationFactor::default(),
+
            (None, Some(min)) => sync::ReplicationFactor::must_reach(min),
+
            (Some(min), None) => sync::ReplicationFactor::must_reach(min),
+
            (Some(min), Some(max)) => sync::ReplicationFactor::range(min, max),
+
        }
+
    }
+
}
+

+
#[derive(Subcommand, Debug)]
+
pub(super) enum Command {
+
    /// Display the sync status of a repository
+
    #[clap(alias = "s")]
+
    Status {
+
        /// The repository to display the status for [default: cwd]
+
        rid: Option<RepoId>,
+
        /// Sort the table by column
+
        #[arg(long, value_name = "FIELD", value_enum, default_value_t)]
+
        sort_by: SortBy,
+
    },
+
}
+

+
/// Sort the status table by the provided field
+
#[derive(ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)]
+
pub(super) enum SortBy {
+
    /// The NID of the entry
+
    Nid,
+
    /// The alias of the entry
+
    Alias,
+
    /// The status of the entry
+
    #[default]
+
    Status,
+
}
+

+
impl FromStr for SortBy {
+
    type Err = &'static str;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        match s {
+
            "nid" => Ok(Self::Nid),
+
            "alias" => Ok(Self::Alias),
+
            "status" => Ok(Self::Status),
+
            _ => Err("invalid `--sort-by` field"),
+
        }
+
    }
+
}
+

+
/// Whether we are performing a fetch/announce of a repository or only
+
/// announcing the node's inventory
+
pub(super) enum SyncMode {
+
    /// Fetch and/or announce a repositories references
+
    Repo {
+
        /// The repository being synchronized
+
        rid: Option<RepoId>,
+
        /// The settings for fetch/announce
+
        settings: SyncSettings,
+
        /// The direction of the synchronization
+
        direction: SyncDirection,
+
    },
+
    /// Announce the node's inventory
+
    Inventory,
+
}
+

+
impl From<SyncArgs> for SyncMode {
+
    fn from(args: SyncArgs) -> Self {
+
        if args.inventory {
+
            Self::Inventory
+
        } else {
+
            assert!(!args.inventory);
+
            let direction = args.direction();
+
            let mut settings = SyncSettings::default()
+
                .timeout(args.timeout())
+
                .replicas(args.replication());
+
            if !args.seeds.is_empty() {
+
                settings.seeds = args.seeds.into_iter().collect();
+
            }
+
            Self::Repo {
+
                rid: args.rid,
+
                settings,
+
                direction,
+
            }
+
        }
+
    }
+
}
+

+
/// The direction of the [`SyncMode`]
+
#[derive(Debug, PartialEq, Eq)]
+
pub(super) enum SyncDirection {
+
    /// Only fetching
+
    Fetch,
+
    /// Only announcing
+
    Announce,
+
    /// Both fetching and announcing
+
    Both,
+
}
+

+
fn replicas_non_zero(s: &str) -> Result<usize, String> {
+
    let r = usize::from_str(s).map_err(|_| format!("{s} is not a number"))?;
+
    if r == 0 {
+
        return Err(format!("{s} must be a value greater than zero"));
+
    }
+
    Ok(r)
+
}
modified crates/radicle-cli/src/main.rs
@@ -82,6 +82,7 @@ enum Commands {
    #[command(name = "self")]
    RadSelf(rad_self::Args),
    Stats(stats::Args),
+
    Sync(sync::Args),
    Unblock(unblock::Args),
    Unfollow(unfollow::Args),
    Unseed(unseed::Args),
@@ -334,7 +335,9 @@ pub(crate) fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyho
            }
        }
        "sync" => {
-
            term::run_command_args::<sync::Options, _>(sync::HELP, sync::run, args.to_vec());
+
            if let Some(Commands::Sync(args)) = CliArgs::parse().command {
+
                term::run_command_fn(sync::run, args);
+
            }
        }
        "seed" => {
            if let Some(Commands::Seed(args)) = CliArgs::parse().command {