Skip to main content

mars_agents/cli/
sync.rs

1//! `mars sync` — resolve + install (make reality match config).
2
3use crate::error::MarsError;
4use crate::sync::{ResolutionMode, SyncOptions, SyncRequest};
5
6use super::output;
7
8/// Arguments for `mars sync`.
9#[derive(Debug, clap::Args)]
10pub struct SyncArgs {
11    /// Overwrite local modifications for managed files.
12    #[arg(long)]
13    pub force: bool,
14
15    /// Dry run — show what would change.
16    #[arg(long)]
17    pub diff: bool,
18
19    /// Install exactly from lock file, error if stale.
20    #[arg(long)]
21    pub frozen: bool,
22
23    /// Refresh models.dev catalog and harness probes synchronously before sync (blocks until complete).
24    #[arg(long, conflicts_with = "no_refresh_models")]
25    pub refresh_models: bool,
26
27    /// Skip the automatic models-cache refresh during sync.
28    #[arg(long, conflicts_with = "refresh_models")]
29    pub no_refresh_models: bool,
30
31    /// Suppress the post-sync upgrade hint line.
32    #[arg(long)]
33    pub no_upgrade_hint: bool,
34
35    /// Show per-item detail for launch-time fields handled by meridian at spawn.
36    #[arg(long)]
37    pub verbose: bool,
38}
39
40/// Run `mars sync`.
41pub fn run(args: &SyncArgs, ctx: &super::MarsContext, json: bool) -> Result<i32, MarsError> {
42    let no_upgrade_hint = args.no_upgrade_hint || no_upgrade_hint_from_env();
43    let request = SyncRequest {
44        resolution: ResolutionMode::Normal,
45        mutation: None,
46        options: SyncOptions {
47            force: args.force,
48            dry_run: args.diff,
49            frozen: args.frozen,
50            refresh_models: args.refresh_models,
51            no_refresh_models: args.no_refresh_models,
52            check_upgrades: !no_upgrade_hint,
53        },
54        recovery: Default::default(),
55        lossiness_mode: if args.verbose {
56            crate::diagnostic::LossinessMode::Verbose
57        } else {
58            crate::diagnostic::LossinessMode::Surface
59        },
60    };
61
62    let report = crate::sync::execute(ctx, &request)?;
63
64    output::print_sync_report(&report, json, no_upgrade_hint);
65
66    Ok(0)
67}
68
69fn no_upgrade_hint_from_env() -> bool {
70    match std::env::var("MARS_NO_UPGRADE_HINT") {
71        Ok(value) => value.trim() == "1",
72        Err(_) => false,
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use crate::cli::{Cli, Command};
79    use clap::Parser;
80
81    #[test]
82    fn parses_no_refresh_models() {
83        let cli = Cli::try_parse_from(["mars", "sync", "--no-refresh-models"]).unwrap();
84        let Command::Sync(args) = cli.command else {
85            panic!("expected sync command");
86        };
87        assert!(args.no_refresh_models);
88    }
89
90    #[test]
91    fn parses_refresh_models() {
92        let cli = Cli::try_parse_from(["mars", "sync", "--refresh-models"]).unwrap();
93        let Command::Sync(args) = cli.command else {
94            panic!("expected sync command");
95        };
96        assert!(args.refresh_models);
97    }
98
99    #[test]
100    fn refresh_and_no_refresh_conflict() {
101        assert!(
102            Cli::try_parse_from(["mars", "sync", "--refresh-models", "--no-refresh-models"])
103                .is_err()
104        );
105    }
106
107    #[test]
108    fn parses_no_upgrade_hint() {
109        let cli = Cli::try_parse_from(["mars", "sync", "--no-upgrade-hint"]).unwrap();
110        let Command::Sync(args) = cli.command else {
111            panic!("expected sync command");
112        };
113        assert!(args.no_upgrade_hint);
114    }
115
116    #[test]
117    fn parses_verbose() {
118        let cli = Cli::try_parse_from(["mars", "sync", "--verbose"]).unwrap();
119        let Command::Sync(args) = cli.command else {
120            panic!("expected sync command");
121        };
122        assert!(args.verbose);
123    }
124}