use std::path::Path;
use crate::error::MarsError;
use crate::fs::{atomic_install_dir, atomic_write};
use crate::lock::{ItemId, ItemKind};
use crate::platform::fs as fs_ops;
use crate::sync::plan::{PlannedAction, SyncPlan};
use crate::sync::target::TargetItem;
pub use crate::sync::types::SyncOptions;
use crate::types::{ContentHash, DestPath, ItemName, SourceName};
#[derive(Debug, Clone)]
pub struct ApplyResult {
pub outcomes: Vec<ActionOutcome>,
}
#[derive(Debug, Clone)]
pub struct ActionOutcome {
pub item_id: ItemId,
pub action: ActionTaken,
pub dest_path: DestPath,
pub source_name: SourceName,
pub source_checksum: Option<ContentHash>,
pub installed_checksum: Option<ContentHash>,
}
#[derive(Debug, Clone)]
pub enum ActionTaken {
Installed,
Updated,
Removed,
Skipped,
Kept,
}
pub fn execute(
root: &Path,
plan: &SyncPlan,
options: &SyncOptions,
) -> Result<ApplyResult, MarsError> {
let mut outcomes = Vec::new();
for action in &plan.actions {
let outcome = if options.dry_run {
dry_run_action(action)
} else {
execute_action(root, action)?
};
outcomes.push(outcome);
}
Ok(ApplyResult { outcomes })
}
fn execute_action(root: &Path, action: &PlannedAction) -> Result<ActionOutcome, MarsError> {
match action {
PlannedAction::Install { target } => {
let dest = target.dest_path.resolve(root);
let installed_checksum = install_item(target, &dest)?;
Ok(ActionOutcome {
item_id: target.id.clone(),
action: ActionTaken::Installed,
dest_path: target.dest_path.clone(),
source_name: target.source_name.clone(),
source_checksum: Some(target.source_hash.clone()),
installed_checksum: Some(installed_checksum),
})
}
PlannedAction::Overwrite { target } => {
let dest = target.dest_path.resolve(root);
let installed_checksum = install_item(target, &dest)?;
Ok(ActionOutcome {
item_id: target.id.clone(),
action: ActionTaken::Updated,
dest_path: target.dest_path.clone(),
source_name: target.source_name.clone(),
source_checksum: Some(target.source_hash.clone()),
installed_checksum: Some(installed_checksum),
})
}
PlannedAction::Remove { locked } => {
let dest = removal_path(root, &locked.dest_path, locked.kind);
if dest.exists() {
fs_ops::safe_remove(&dest)?;
}
let item_id = ItemId {
kind: locked.kind,
name: ItemName::from(locked.dest_path.item_name(locked.kind)),
};
Ok(ActionOutcome {
item_id,
action: ActionTaken::Removed,
dest_path: locked.dest_path.clone(),
source_name: locked.source.clone(),
source_checksum: None,
installed_checksum: None,
})
}
PlannedAction::Skip {
item_id,
dest_path,
source_name,
installed_checksum,
} => Ok(ActionOutcome {
item_id: item_id.clone(),
action: ActionTaken::Skipped,
dest_path: dest_path.clone(),
source_name: source_name.clone(),
source_checksum: None,
installed_checksum: installed_checksum.clone(),
}),
PlannedAction::KeepLocal {
item_id,
dest_path,
source_name,
} => Ok(ActionOutcome {
item_id: item_id.clone(),
action: ActionTaken::Kept,
dest_path: dest_path.clone(),
source_name: source_name.clone(),
source_checksum: None,
installed_checksum: None,
}),
}
}
fn dry_run_action(action: &PlannedAction) -> ActionOutcome {
match action {
PlannedAction::Install { target } => ActionOutcome {
item_id: target.id.clone(),
action: ActionTaken::Installed,
dest_path: target.dest_path.clone(),
source_name: target.source_name.clone(),
source_checksum: Some(target.source_hash.clone()),
installed_checksum: None, },
PlannedAction::Overwrite { target } => ActionOutcome {
item_id: target.id.clone(),
action: ActionTaken::Updated,
dest_path: target.dest_path.clone(),
source_name: target.source_name.clone(),
source_checksum: Some(target.source_hash.clone()),
installed_checksum: None,
},
PlannedAction::Remove { locked } => {
let item_id = ItemId {
kind: locked.kind,
name: ItemName::from(locked.dest_path.item_name(locked.kind)),
};
ActionOutcome {
item_id,
action: ActionTaken::Removed,
dest_path: locked.dest_path.clone(),
source_name: locked.source.clone(),
source_checksum: None,
installed_checksum: None,
}
}
PlannedAction::Skip {
item_id,
dest_path,
source_name,
installed_checksum,
..
} => ActionOutcome {
item_id: item_id.clone(),
action: ActionTaken::Skipped,
dest_path: dest_path.clone(),
source_name: source_name.clone(),
source_checksum: None,
installed_checksum: installed_checksum.clone(),
},
PlannedAction::KeepLocal {
item_id,
dest_path,
source_name,
} => ActionOutcome {
item_id: item_id.clone(),
action: ActionTaken::Kept,
dest_path: dest_path.clone(),
source_name: source_name.clone(),
source_checksum: None,
installed_checksum: None,
},
}
}
fn install_item(target: &TargetItem, dest: &Path) -> Result<ContentHash, MarsError> {
match target.id.kind {
ItemKind::Agent | ItemKind::McpServer => {
let content = content_to_install(target)?;
write_file_and_verify(dest, &content)
}
ItemKind::BootstrapDoc => {
let doc_dest = dest.parent().ok_or_else(|| {
std::io::Error::other(format!(
"bootstrap destination has no parent directory: {}",
dest.display()
))
})?;
atomic_install_dir(&target.source_path, doc_dest)?;
crate::hash::compute_hash(doc_dest, ItemKind::BootstrapDoc).map(ContentHash::from)
}
ItemKind::Skill | ItemKind::Hook => {
if target.is_flat_skill {
crate::fs::atomic_install_dir_filtered(
&target.source_path,
dest,
crate::fs::FLAT_SKILL_EXCLUDED_TOP_LEVEL,
)?;
} else {
atomic_install_dir(&target.source_path, dest)?;
}
crate::hash::compute_hash(dest, ItemKind::Skill).map(ContentHash::from)
}
}
}
fn write_file_and_verify(dest: &Path, content: &[u8]) -> Result<ContentHash, MarsError> {
atomic_write(dest, content)?;
let expected = ContentHash::from(crate::hash::hash_bytes(content));
let persisted = std::fs::read(dest)?;
let actual = ContentHash::from(crate::hash::hash_bytes(&persisted));
if expected != actual {
return Err(std::io::Error::other(format!(
"post-write verification failed for {}: expected {expected}, got {actual}",
dest.display()
))
.into());
}
Ok(actual)
}
fn content_to_install(target: &TargetItem) -> Result<Vec<u8>, MarsError> {
if let Some(content) = &target.rewritten_content {
Ok(content.as_bytes().to_vec())
} else if target.id.kind == ItemKind::BootstrapDoc {
Ok(std::fs::read(target.source_path.join("BOOTSTRAP.md"))?)
} else {
Ok(std::fs::read(&target.source_path)?)
}
}
fn removal_path(root: &Path, dest_path: &DestPath, kind: ItemKind) -> std::path::PathBuf {
let dest = dest_path.resolve(root);
if kind == ItemKind::BootstrapDoc {
if dest_path.as_str().split('/').count() >= 3 {
dest.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| dest.clone())
} else {
dest
}
} else {
dest
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash;
use crate::lock::{ItemId, ItemKind, LockedItem};
use crate::sync::plan::{PlannedAction, SyncPlan};
use crate::sync::target::TargetItem;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
fn make_agent_target(name: &str, source_path: PathBuf, content: &[u8]) -> TargetItem {
TargetItem {
id: ItemId {
kind: ItemKind::Agent,
name: name.into(),
},
source_name: "test-source".into(),
source_path,
dest_path: format!("agents/{name}.md").into(),
source_hash: hash::hash_bytes(content).into(),
is_flat_skill: false,
rewritten_content: None,
}
}
fn make_bootstrap_target(name: &str, source_path: PathBuf) -> TargetItem {
TargetItem {
id: ItemId {
kind: ItemKind::BootstrapDoc,
name: name.into(),
},
source_name: "test-source".into(),
source_hash: crate::hash::compute_hash(&source_path, ItemKind::BootstrapDoc)
.unwrap()
.into(),
source_path,
dest_path: format!("bootstrap/{name}/BOOTSTRAP.md").into(),
is_flat_skill: false,
rewritten_content: None,
}
}
fn setup_source_agent(dir: &Path, name: &str, content: &[u8]) -> PathBuf {
let agents_dir = dir.join("source").join("agents");
fs::create_dir_all(&agents_dir).unwrap();
let path = agents_dir.join(format!("{name}.md"));
fs::write(&path, content).unwrap();
path
}
#[test]
fn install_creates_new_file() {
let root = TempDir::new().unwrap();
let source_dir = TempDir::new().unwrap();
let content = b"# new agent content";
let source_path = setup_source_agent(source_dir.path(), "coder", content);
let target = make_agent_target("coder", source_path, content);
let plan = SyncPlan {
actions: vec![PlannedAction::Install {
target: target.clone(),
}],
};
let options = SyncOptions::default();
let result = execute(root.path(), &plan, &options).unwrap();
assert_eq!(result.outcomes.len(), 1);
let outcome = &result.outcomes[0];
assert!(matches!(outcome.action, ActionTaken::Installed));
let installed_path = root.path().join("agents/coder.md");
assert!(installed_path.exists());
assert_eq!(fs::read(&installed_path).unwrap(), content);
assert_eq!(
outcome.source_checksum.as_deref(),
Some(hash::hash_bytes(content).as_str())
);
assert!(outcome.installed_checksum.is_some());
}
#[test]
fn overwrite_replaces_existing_file() {
let root = TempDir::new().unwrap();
let source_dir = TempDir::new().unwrap();
let agents_dir = root.path().join("agents");
fs::create_dir_all(&agents_dir).unwrap();
fs::write(agents_dir.join("coder.md"), b"# old content").unwrap();
let new_content = b"# new content";
let source_path = setup_source_agent(source_dir.path(), "coder", new_content);
let target = make_agent_target("coder", source_path, new_content);
let plan = SyncPlan {
actions: vec![PlannedAction::Overwrite { target }],
};
let options = SyncOptions::default();
let result = execute(root.path(), &plan, &options).unwrap();
assert!(matches!(result.outcomes[0].action, ActionTaken::Updated));
let installed = fs::read(root.path().join("agents/coder.md")).unwrap();
assert_eq!(installed, new_content);
}
#[test]
fn install_bootstrap_doc_directory_to_canonical_file_path() {
let root = TempDir::new().unwrap();
let source_dir = TempDir::new().unwrap();
let bootstrap_dir = source_dir.path().join("bootstrap/global-auth");
fs::create_dir_all(&bootstrap_dir).unwrap();
fs::write(bootstrap_dir.join("BOOTSTRAP.md"), b"# auth").unwrap();
let target = make_bootstrap_target("global-auth", bootstrap_dir);
let plan = SyncPlan {
actions: vec![PlannedAction::Install { target }],
};
let options = SyncOptions::default();
let result = execute(root.path(), &plan, &options).unwrap();
assert!(matches!(result.outcomes[0].action, ActionTaken::Installed));
assert_eq!(
fs::read(root.path().join("bootstrap/global-auth/BOOTSTRAP.md")).unwrap(),
b"# auth"
);
}
#[test]
fn remove_deletes_file() {
let root = TempDir::new().unwrap();
let agents_dir = root.path().join("agents");
fs::create_dir_all(&agents_dir).unwrap();
fs::write(agents_dir.join("orphan.md"), b"# orphan").unwrap();
let locked = LockedItem {
source: "old-source".into(),
kind: ItemKind::Agent,
version: None,
source_checksum: "sha256:aaa".into(),
installed_checksum: "sha256:bbb".into(),
dest_path: "agents/orphan.md".into(),
};
let plan = SyncPlan {
actions: vec![PlannedAction::Remove { locked }],
};
let options = SyncOptions::default();
let result = execute(root.path(), &plan, &options).unwrap();
assert!(matches!(result.outcomes[0].action, ActionTaken::Removed));
assert!(!root.path().join("agents/orphan.md").exists());
}
#[test]
fn remove_skill_directory() {
let root = TempDir::new().unwrap();
let skill_dir = root.path().join("skills/old-skill");
fs::create_dir_all(&skill_dir).unwrap();
fs::write(skill_dir.join("SKILL.md"), b"# old skill").unwrap();
let locked = LockedItem {
source: "old-source".into(),
kind: ItemKind::Skill,
version: None,
source_checksum: "sha256:aaa".into(),
installed_checksum: "sha256:bbb".into(),
dest_path: "skills/old-skill".into(),
};
let plan = SyncPlan {
actions: vec![PlannedAction::Remove { locked }],
};
let options = SyncOptions::default();
let result = execute(root.path(), &plan, &options).unwrap();
assert!(matches!(result.outcomes[0].action, ActionTaken::Removed));
assert!(!root.path().join("skills/old-skill").exists());
}
#[test]
fn remove_bootstrap_doc_removes_container_directory() {
let root = TempDir::new().unwrap();
let bootstrap_dir = root.path().join("bootstrap/global-auth");
fs::create_dir_all(&bootstrap_dir).unwrap();
fs::write(bootstrap_dir.join("BOOTSTRAP.md"), b"# auth").unwrap();
let locked = LockedItem {
source: "old-source".into(),
kind: ItemKind::BootstrapDoc,
version: None,
source_checksum: "sha256:aaa".into(),
installed_checksum: "sha256:bbb".into(),
dest_path: "bootstrap/global-auth/BOOTSTRAP.md".into(),
};
let plan = SyncPlan {
actions: vec![PlannedAction::Remove { locked }],
};
let options = SyncOptions::default();
let result = execute(root.path(), &plan, &options).unwrap();
assert!(matches!(result.outcomes[0].action, ActionTaken::Removed));
assert!(!bootstrap_dir.exists());
}
#[test]
fn remove_degenerate_bootstrap_doc_path_removes_exact_file_only() {
let root = TempDir::new().unwrap();
let bootstrap_dir = root.path().join("bootstrap");
fs::create_dir_all(&bootstrap_dir).unwrap();
fs::write(bootstrap_dir.join("BOOTSTRAP.md"), b"# root").unwrap();
fs::write(bootstrap_dir.join("keep.md"), b"# keep").unwrap();
let locked = LockedItem {
source: "old-source".into(),
kind: ItemKind::BootstrapDoc,
version: None,
source_checksum: "sha256:aaa".into(),
installed_checksum: "sha256:bbb".into(),
dest_path: "bootstrap/BOOTSTRAP.md".into(),
};
let plan = SyncPlan {
actions: vec![PlannedAction::Remove { locked }],
};
let options = SyncOptions::default();
let result = execute(root.path(), &plan, &options).unwrap();
assert!(matches!(result.outcomes[0].action, ActionTaken::Removed));
assert!(!bootstrap_dir.join("BOOTSTRAP.md").exists());
assert!(bootstrap_dir.join("keep.md").exists());
}
#[test]
fn dry_run_does_not_modify_files() {
let root = TempDir::new().unwrap();
let source_dir = TempDir::new().unwrap();
let content = b"# new agent";
let source_path = setup_source_agent(source_dir.path(), "coder", content);
let target = make_agent_target("coder", source_path, content);
let plan = SyncPlan {
actions: vec![PlannedAction::Install { target }],
};
let options = SyncOptions {
dry_run: true,
..SyncOptions::default()
};
let result = execute(root.path(), &plan, &options).unwrap();
assert_eq!(result.outcomes.len(), 1);
assert!(matches!(result.outcomes[0].action, ActionTaken::Installed));
assert!(!root.path().join("agents/coder.md").exists());
}
#[test]
fn skip_produces_skipped_outcome() {
let root = TempDir::new().unwrap();
let plan = SyncPlan {
actions: vec![PlannedAction::Skip {
item_id: ItemId {
kind: ItemKind::Agent,
name: "stable".into(),
},
dest_path: "agents/stable.md".into(),
source_name: "base".into(),
installed_checksum: Some("sha256:stable".into()),
}],
};
let options = SyncOptions::default();
let result = execute(root.path(), &plan, &options).unwrap();
assert!(matches!(result.outcomes[0].action, ActionTaken::Skipped));
assert_eq!(
result.outcomes[0].dest_path,
crate::types::DestPath::from("agents/stable.md")
);
assert_eq!(result.outcomes[0].source_name, "base");
assert_eq!(
result.outcomes[0].installed_checksum.as_deref(),
Some("sha256:stable")
);
}
#[test]
fn keep_local_produces_kept_outcome() {
let root = TempDir::new().unwrap();
let plan = SyncPlan {
actions: vec![PlannedAction::KeepLocal {
item_id: ItemId {
kind: ItemKind::Agent,
name: "modified".into(),
},
dest_path: "agents/modified.md".into(),
source_name: "base".into(),
}],
};
let options = SyncOptions::default();
let result = execute(root.path(), &plan, &options).unwrap();
assert!(matches!(result.outcomes[0].action, ActionTaken::Kept));
assert_eq!(
result.outcomes[0].dest_path,
crate::types::DestPath::from("agents/modified.md")
);
assert_eq!(result.outcomes[0].source_name, "base");
}
#[test]
fn install_skill_directory() {
let root = TempDir::new().unwrap();
let source_dir = TempDir::new().unwrap();
let source_skill = source_dir.path().join("skills/planning");
fs::create_dir_all(&source_skill).unwrap();
fs::write(source_skill.join("SKILL.md"), b"# Planning skill").unwrap();
fs::write(source_skill.join("helper.md"), b"# Helper").unwrap();
let skill_hash = hash::compute_hash(&source_skill, ItemKind::Skill).unwrap();
let target = TargetItem {
id: ItemId {
kind: ItemKind::Skill,
name: "planning".into(),
},
source_name: "test".into(),
source_path: source_skill,
dest_path: "skills/planning".into(),
source_hash: skill_hash.into(),
is_flat_skill: false,
rewritten_content: None,
};
let plan = SyncPlan {
actions: vec![PlannedAction::Install { target }],
};
let options = SyncOptions::default();
let result = execute(root.path(), &plan, &options).unwrap();
assert!(matches!(result.outcomes[0].action, ActionTaken::Installed));
let installed_dir = root.path().join("skills/planning");
assert!(installed_dir.exists());
assert!(installed_dir.join("SKILL.md").exists());
assert!(installed_dir.join("helper.md").exists());
assert_eq!(
fs::read_to_string(installed_dir.join("SKILL.md")).unwrap(),
"# Planning skill"
);
}
#[test]
fn install_flat_skill_excludes_repo_metadata() {
let root = TempDir::new().unwrap();
let source_dir = TempDir::new().unwrap();
let flat_source = source_dir.path().join("flat-skill");
fs::create_dir_all(flat_source.join(".git")).unwrap();
fs::create_dir_all(flat_source.join("resources")).unwrap();
fs::write(flat_source.join("SKILL.md"), b"# Flat skill").unwrap();
fs::write(flat_source.join("resources/guide.md"), b"# Guide").unwrap();
fs::write(flat_source.join("mars.toml"), b"[sources]").unwrap();
fs::write(flat_source.join(".gitignore"), b"target/").unwrap();
fs::write(flat_source.join(".git/config"), b"[core]").unwrap();
let source_hash = hash::compute_skill_hash_filtered(
&flat_source,
crate::fs::FLAT_SKILL_EXCLUDED_TOP_LEVEL,
)
.unwrap();
let target = TargetItem {
id: ItemId {
kind: ItemKind::Skill,
name: "flat-skill".into(),
},
source_name: "test".into(),
source_path: flat_source,
dest_path: "skills/flat-skill".into(),
source_hash: source_hash.into(),
is_flat_skill: true,
rewritten_content: None,
};
let plan = SyncPlan {
actions: vec![PlannedAction::Install { target }],
};
let options = SyncOptions::default();
execute(root.path(), &plan, &options).unwrap();
let installed = root.path().join("skills/flat-skill");
assert!(installed.join("SKILL.md").exists());
assert!(installed.join("resources/guide.md").exists());
assert!(!installed.join(".git").exists());
assert!(!installed.join("mars.toml").exists());
assert!(!installed.join(".gitignore").exists());
}
#[test]
fn extract_agent_name() {
assert_eq!(
crate::types::DestPath::from("agents/coder.md").item_name(ItemKind::Agent),
"coder"
);
}
#[test]
fn extract_skill_name() {
assert_eq!(
crate::types::DestPath::from("skills/planning").item_name(ItemKind::Skill),
"planning"
);
}
}