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            no_refresh_models: false,
42        },
43    };
44
45    // Force sync: overwrites everything, rebuilds from dependencies.
46    let report = if recovered_corrupt_lock {
47        execute_repair_with_collision_cleanup(ctx, &request)?
48    } else {
49        crate::sync::execute(ctx, &request)?
50    };
51
52    output::print_sync_report(&report, json, true);
53
54    if report.has_conflicts() { Ok(1) } else { Ok(0) }
55}
56
57fn execute_repair_with_collision_cleanup(
58    ctx: &super::MarsContext,
59    request: &SyncRequest,
60) -> Result<SyncReport, MarsError> {
61    const MAX_RETRIES: usize = 1024;
62    let mut retries = 0usize;
63
64    loop {
65        match crate::sync::execute(ctx, request) {
66            Ok(report) => return Ok(report),
67            Err(err) => {
68                if let Some(path) = extract_unmanaged_collision_path(&err) {
69                    if retries >= MAX_RETRIES {
70                        return Err(MarsError::InvalidRequest {
71                            message: format!(
72                                "repair exceeded {MAX_RETRIES} unmanaged-collision retries while rebuilding from corrupt lock"
73                            ),
74                        });
75                    }
76
77                    let mars_dir = ctx.project_root.join(".mars");
78                    let full_path = mars_dir.join(path);
79                    if full_path.is_dir() {
80                        std::fs::remove_dir_all(&full_path)?;
81                    } else if full_path.exists() {
82                        std::fs::remove_file(&full_path)?;
83                    }
84
85                    eprintln!(
86                        "warning: removing unmanaged path `{}` to rebuild from corrupt lock",
87                        path.display()
88                    );
89                    retries += 1;
90                    continue;
91                }
92
93                return Err(err);
94            }
95        }
96    }
97}
98
99fn extract_unmanaged_collision_path(err: &MarsError) -> Option<&std::path::Path> {
100    match err {
101        MarsError::UnmanagedCollision { path, .. } => Some(path.as_path()),
102        _ => None,
103    }
104}