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    /// Ignore package `requires-mars` version constraints.
40    #[arg(long)]
41    pub ignore_requires_mars: bool,
42
43    /// Ignore package `requires-meridian` version constraints.
44    #[arg(long)]
45    pub ignore_requires_meridian: bool,
46}
47
48/// Run `mars sync`.
49pub fn run(args: &SyncArgs, ctx: &super::MarsContext, json: bool) -> Result<i32, MarsError> {
50    let no_upgrade_hint = args.no_upgrade_hint || no_upgrade_hint_from_env();
51    let request = SyncRequest {
52        resolution: ResolutionMode::Normal,
53        mutation: None,
54        options: SyncOptions {
55            force: args.force,
56            dry_run: args.diff,
57            frozen: args.frozen,
58            refresh_models: args.refresh_models,
59            no_refresh_models: args.no_refresh_models,
60            check_upgrades: !no_upgrade_hint,
61            ignore_requires_mars: args.ignore_requires_mars,
62            ignore_requires_meridian: args.ignore_requires_meridian,
63        },
64        recovery: Default::default(),
65        lossiness_mode: if args.verbose {
66            crate::diagnostic::LossinessMode::Verbose
67        } else {
68            crate::diagnostic::LossinessMode::Surface
69        },
70    };
71
72    let report = crate::sync::execute(ctx, &request)?;
73
74    output::print_sync_report(&report, json, no_upgrade_hint);
75
76    Ok(0)
77}
78
79fn no_upgrade_hint_from_env() -> bool {
80    match std::env::var("MARS_NO_UPGRADE_HINT") {
81        Ok(value) => value.trim() == "1",
82        Err(_) => false,
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use crate::cli::{Cli, Command};
89    use clap::Parser;
90
91    #[test]
92    fn parses_no_refresh_models() {
93        let cli = Cli::try_parse_from(["mars", "sync", "--no-refresh-models"]).unwrap();
94        let Command::Sync(args) = cli.command else {
95            panic!("expected sync command");
96        };
97        assert!(args.no_refresh_models);
98    }
99
100    #[test]
101    fn parses_refresh_models() {
102        let cli = Cli::try_parse_from(["mars", "sync", "--refresh-models"]).unwrap();
103        let Command::Sync(args) = cli.command else {
104            panic!("expected sync command");
105        };
106        assert!(args.refresh_models);
107    }
108
109    #[test]
110    fn refresh_and_no_refresh_conflict() {
111        assert!(
112            Cli::try_parse_from(["mars", "sync", "--refresh-models", "--no-refresh-models"])
113                .is_err()
114        );
115    }
116
117    #[test]
118    fn parses_no_upgrade_hint() {
119        let cli = Cli::try_parse_from(["mars", "sync", "--no-upgrade-hint"]).unwrap();
120        let Command::Sync(args) = cli.command else {
121            panic!("expected sync command");
122        };
123        assert!(args.no_upgrade_hint);
124    }
125
126    #[test]
127    fn parses_verbose() {
128        let cli = Cli::try_parse_from(["mars", "sync", "--verbose"]).unwrap();
129        let Command::Sync(args) = cli.command else {
130            panic!("expected sync command");
131        };
132        assert!(args.verbose);
133    }
134
135    #[test]
136    fn engine_ignore_flags_parse_on_all_syncing_commands() {
137        for command in ["sync", "upgrade", "add", "repair"] {
138            let mut argv = vec!["mars", command];
139            if command == "add" {
140                argv.push("owner/repo");
141            }
142            argv.extend(["--ignore-requires-mars", "--ignore-requires-meridian"]);
143            let cli = Cli::try_parse_from(argv).unwrap();
144            match cli.command {
145                Command::Sync(args) => {
146                    assert!(args.ignore_requires_mars);
147                    assert!(args.ignore_requires_meridian);
148                }
149                Command::Upgrade(args) => {
150                    assert!(args.ignore_requires_mars);
151                    assert!(args.ignore_requires_meridian);
152                }
153                Command::Add(args) => {
154                    assert!(args.ignore_requires_mars);
155                    assert!(args.ignore_requires_meridian);
156                }
157                Command::Repair(args) => {
158                    assert!(args.ignore_requires_mars);
159                    assert!(args.ignore_requires_meridian);
160                }
161                _ => panic!("unexpected command"),
162            }
163        }
164    }
165}