Skip to main content

mars_agents/cli/
repair.rs

1//! `mars repair` — rebuild state from lock + dependencies.
2
3use crate::error::{LockError, MarsError};
4use crate::lock::LockFile;
5use crate::sync::{ResolutionMode, SyncOptions, SyncReport, SyncRequest};
6
7use super::output;
8
9/// Arguments for `mars repair`.
10#[derive(Debug, clap::Args)]
11pub struct RepairArgs {}
12
13/// Run `mars repair`.
14///
15/// Re-syncs everything from config. This is effectively a forced sync
16/// that rebuilds the state. If lock exists, items are re-installed from
17/// dependencies to match it. If lock is missing, a fresh sync is performed.
18pub fn run(_args: &RepairArgs, ctx: &super::MarsContext, json: bool) -> Result<i32, MarsError> {
19    if !json {
20        output::print_info("repairing — re-syncing from dependencies...");
21    }
22
23    let recovered_corrupt_lock = match crate::lock::load(&ctx.project_root) {
24        Ok(_) => false,
25        Err(MarsError::Lock(LockError::Corrupt { message })) => {
26            eprintln!("warning: {message}");
27            eprintln!("warning: lock is corrupt, rebuilding from mars.toml + dependencies");
28            crate::lock::write(&ctx.project_root, &LockFile::empty())?;
29            true
30        }
31        Err(err) => return Err(err),
32    };
33
34    let request = SyncRequest {
35        resolution: ResolutionMode::Normal,
36        mutation: None,
37        options: SyncOptions {
38            force: true,
39            dry_run: false,
40            frozen: false,
41            refresh_models: false,
42            no_refresh_models: false,
43        },
44    };
45
46    // Force sync: overwrites everything, rebuilds from dependencies.
47    let report = if recovered_corrupt_lock {
48        execute_repair_with_collision_cleanup(ctx, &request)?
49    } else {
50        crate::sync::execute(ctx, &request)?
51    };
52
53    output::print_sync_report(&report, json, true);
54
55    if report.has_conflicts() { Ok(1) } else { Ok(0) }
56}
57
58fn execute_repair_with_collision_cleanup(
59    ctx: &super::MarsContext,
60    request: &SyncRequest,
61) -> Result<SyncReport, MarsError> {
62    const MAX_RETRIES: usize = 1024;
63    let mut retries = 0usize;
64
65    loop {
66        match crate::sync::execute(ctx, request) {
67            Ok(report) => return Ok(report),
68            Err(err) => {
69                if let Some(path) = extract_unmanaged_collision_path(&err) {
70                    if retries >= MAX_RETRIES {
71                        return Err(MarsError::InvalidRequest {
72                            message: format!(
73                                "repair exceeded {MAX_RETRIES} unmanaged-collision retries while rebuilding from corrupt lock"
74                            ),
75                        });
76                    }
77
78                    let mars_dir = ctx.project_root.join(".mars");
79                    let full_path = mars_dir.join(path);
80                    if full_path.is_dir() {
81                        std::fs::remove_dir_all(&full_path)?;
82                    } else if full_path.exists() {
83                        std::fs::remove_file(&full_path)?;
84                    }
85
86                    eprintln!(
87                        "warning: removing unmanaged path `{}` to rebuild from corrupt lock",
88                        path.display()
89                    );
90                    retries += 1;
91                    continue;
92                }
93
94                return Err(err);
95            }
96        }
97    }
98}
99
100fn extract_unmanaged_collision_path(err: &MarsError) -> Option<&std::path::Path> {
101    match err {
102        MarsError::UnmanagedCollision { path, .. } => Some(path.as_path()),
103        _ => None,
104    }
105}