mars_agents/reconcile/
mod.rs1use std::path::{Path, PathBuf};
2
3use crate::error::MarsError;
4use crate::types::{ContentHash, ItemKind};
5
6pub mod fs_ops;
7
8pub use fs_ops::*;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum DestinationState {
12 Empty,
13 File { hash: ContentHash },
14 Directory { hash: ContentHash },
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum DesiredState {
19 CopyFile { source: PathBuf, hash: ContentHash },
20 CopyDir { source: PathBuf, hash: ContentHash },
21 Absent,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum ReconcileOutcome {
26 Created,
27 Updated,
28 Removed,
29 Skipped {
30 reason: &'static str,
31 },
32 Conflict {
33 existing: DestinationState,
34 desired: DesiredState,
35 },
36}
37
38pub fn scan_destination(path: &Path) -> DestinationState {
40 scan_destination_checked(path).unwrap_or(DestinationState::Empty)
41}
42
43pub fn reconcile_one(
45 dest: &Path,
46 desired: DesiredState,
47 force: bool,
48) -> Result<ReconcileOutcome, MarsError> {
49 let existing = scan_destination_checked(dest)?;
50
51 match desired {
52 DesiredState::Absent => {
53 if matches!(existing, DestinationState::Empty) {
54 Ok(ReconcileOutcome::Skipped {
55 reason: "already absent",
56 })
57 } else {
58 safe_remove(dest)?;
59 Ok(ReconcileOutcome::Removed)
60 }
61 }
62 DesiredState::CopyFile { source, hash } => match existing {
63 DestinationState::Empty => {
64 atomic_copy_file(&source, dest)?;
65 Ok(ReconcileOutcome::Created)
66 }
67 DestinationState::File {
68 hash: existing_hash,
69 } if existing_hash == hash => Ok(ReconcileOutcome::Skipped {
70 reason: "already up-to-date",
71 }),
72 existing_state => {
73 if !force {
74 return Ok(ReconcileOutcome::Conflict {
75 existing: existing_state,
76 desired: DesiredState::CopyFile { source, hash },
77 });
78 }
79 safe_remove(dest)?;
80 atomic_copy_file(&source, dest)?;
81 Ok(ReconcileOutcome::Updated)
82 }
83 },
84 DesiredState::CopyDir { source, hash } => match existing {
85 DestinationState::Empty => {
86 atomic_copy_dir(&source, dest)?;
87 Ok(ReconcileOutcome::Created)
88 }
89 DestinationState::Directory {
90 hash: existing_hash,
91 } if existing_hash == hash => Ok(ReconcileOutcome::Skipped {
92 reason: "already up-to-date",
93 }),
94 existing_state => {
95 if !force {
96 return Ok(ReconcileOutcome::Conflict {
97 existing: existing_state,
98 desired: DesiredState::CopyDir { source, hash },
99 });
100 }
101 safe_remove(dest)?;
102 atomic_copy_dir(&source, dest)?;
103 Ok(ReconcileOutcome::Updated)
104 }
105 },
106 }
107}
108
109fn scan_destination_checked(path: &Path) -> Result<DestinationState, MarsError> {
110 let metadata = match std::fs::metadata(path) {
111 Ok(metadata) => metadata,
112 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(DestinationState::Empty),
113 Err(e) => return Err(e.into()),
114 };
115
116 if metadata.is_file() {
117 return Ok(DestinationState::File {
118 hash: content_hash(path, ItemKind::Agent)?,
119 });
120 }
121
122 if metadata.is_dir() {
123 return Ok(DestinationState::Directory {
124 hash: content_hash(path, ItemKind::Skill)?,
125 });
126 }
127
128 Ok(DestinationState::Empty)
129}