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        lossiness_mode: if args.verbose {
55            crate::diagnostic::LossinessMode::Verbose
56        } else {
57            crate::diagnostic::LossinessMode::Surface
58        },
59    };
60
61    let report = crate::sync::execute(ctx, &request)?;
62
63    output::print_sync_report(&report, json, no_upgrade_hint);
64
65    if report.has_conflicts() { Ok(1) } else { Ok(0) }
66}
67
68fn no_upgrade_hint_from_env() -> bool {
69    match std::env::var("MARS_NO_UPGRADE_HINT") {
70        Ok(value) => value.trim() == "1",
71        Err(_) => false,
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use crate::cli::{Cli, Command};
78    use clap::Parser;
79
80    #[test]
81    fn parses_no_refresh_models() {
82        let cli = Cli::try_parse_from(["mars", "sync", "--no-refresh-models"]).unwrap();
83        let Command::Sync(args) = cli.command else {
84            panic!("expected sync command");
85        };
86        assert!(args.no_refresh_models);
87    }
88
89    #[test]
90    fn parses_refresh_models() {
91        let cli = Cli::try_parse_from(["mars", "sync", "--refresh-models"]).unwrap();
92        let Command::Sync(args) = cli.command else {
93            panic!("expected sync command");
94        };
95        assert!(args.refresh_models);
96    }
97
98    #[test]
99    fn refresh_and_no_refresh_conflict() {
100        assert!(
101            Cli::try_parse_from(["mars", "sync", "--refresh-models", "--no-refresh-models"])
102                .is_err()
103        );
104    }
105
106    #[test]
107    fn parses_no_upgrade_hint() {
108        let cli = Cli::try_parse_from(["mars", "sync", "--no-upgrade-hint"]).unwrap();
109        let Command::Sync(args) = cli.command else {
110            panic!("expected sync command");
111        };
112        assert!(args.no_upgrade_hint);
113    }
114
115    #[test]
116    fn parses_verbose() {
117        let cli = Cli::try_parse_from(["mars", "sync", "--verbose"]).unwrap();
118        let Command::Sync(args) = cli.command else {
119            panic!("expected sync command");
120        };
121        assert!(args.verbose);
122    }
123}