use std::path::Path;
use super::settings::deploy_output_style;
use crate::core::agent_deployer::{DeployResult, deploy_agents_filtered};
use crate::core::agent_skill_codeploy::co_deploy_skill_set;
use crate::core::paths::FrameworkPaths;
use crate::core::skill_deployer::DeployStats;
use crate::core::skill_tiers::deploy_all_skill_tiers;
#[derive(Debug, thiserror::Error)]
pub enum SyncAssetsError {
#[error("agent redeploy failed: {0}")]
AgentDeploy(String),
#[error("skill redeploy failed: {0}")]
SkillDeploy(String),
}
#[derive(Debug, Default)]
pub struct SyncAssetsReport {
pub agents_deployed: Vec<String>,
pub agents_skipped: Vec<String>,
pub skills_deployed: Vec<String>,
pub skills_skipped: Vec<String>,
pub output_style_synced: bool,
}
pub fn sync_session_assets(
fw: &FrameworkPaths,
project_dir: &Path,
) -> Result<SyncAssetsReport, SyncAssetsError> {
let catalog_root = crate::content::catalog_root_for(&fw.root);
let sources =
crate::core::manifest::ManifestSources::resolve(project_dir, &fw.root, &catalog_root);
let manifest = crate::core::manifest::resolve_manifest(&sources);
let plan = crate::core::manifest::HarnessPlan::from_manifest(&manifest, fw, &catalog_root);
let deploy: DeployResult =
deploy_agents_filtered(&plan.agent_source, &fw.claude_agents_dir(), |name| {
plan.agent_selected(name)
})
.map_err(|e| SyncAssetsError::AgentDeploy(e.to_string()))?;
let co_deploy_skills = co_deploy_skill_set(&deploy.declared_skills);
let skill_deploy: DeployStats = deploy_all_skill_tiers(
&plan.skill_source,
&fw.user_skill_source_dir(),
&fw.claude_skills_dir(),
|name| plan.skill_selected(name) || co_deploy_skills.contains(name),
)
.map_err(|e| SyncAssetsError::SkillDeploy(e.to_string()))?
.stats;
let output_style_synced = deploy_output_style(project_dir).is_ok();
Ok(SyncAssetsReport {
agents_deployed: deploy.deployed,
agents_skipped: deploy.skipped,
skills_deployed: skill_deploy.deployed,
skills_skipped: skill_deploy.skipped,
output_style_synced,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sync_assets_redeploys_new_agent() {
let tmp = tempfile::tempdir().unwrap();
let mut fw = FrameworkPaths::under(tmp.path().join("home"));
fw.trusty_mpm_root = None;
let bundled = fw.agent_source_dir();
std::fs::create_dir_all(&bundled).unwrap();
std::fs::write(
bundled.join("rust-engineer.md"),
"---\nname: rust-engineer\n---\nv1",
)
.unwrap();
let project_dir = tmp.path().join("workspace");
std::fs::create_dir_all(&project_dir).unwrap();
let mut ws_fw = fw.clone();
ws_fw.claude_agents = project_dir.join(".claude").join("agents");
ws_fw.claude_skills = project_dir.join(".claude").join("skills");
let first = sync_session_assets(&ws_fw, &project_dir).unwrap();
assert_eq!(first.agents_deployed, vec!["rust-engineer.md".to_string()]);
assert!(!ws_fw.claude_agents_dir().join("qa.md").exists());
std::fs::write(bundled.join("qa.md"), "---\nname: qa\n---\nv1").unwrap();
let second = sync_session_assets(&ws_fw, &project_dir).unwrap();
assert!(second.agents_deployed.contains(&"qa.md".to_string()));
assert!(ws_fw.claude_agents_dir().join("qa.md").exists());
}
#[test]
fn sync_assets_refreshes_stale_skill() {
let tmp = tempfile::tempdir().unwrap();
let mut fw = FrameworkPaths::under(tmp.path().join("home"));
fw.trusty_mpm_root = None;
let bundled = fw.skill_source_dir();
std::fs::create_dir_all(&bundled).unwrap();
std::fs::write(bundled.join("tm-doctor.md"), "v1.0.0").unwrap();
let project_dir = tmp.path().join("workspace");
std::fs::create_dir_all(&project_dir).unwrap();
let mut ws_fw = fw.clone();
ws_fw.claude_agents = project_dir.join(".claude").join("agents");
ws_fw.claude_skills = project_dir.join(".claude").join("skills");
sync_session_assets(&ws_fw, &project_dir).unwrap();
let deployed_path = ws_fw.claude_skills_dir().join("tm-doctor").join("SKILL.md");
assert_eq!(std::fs::read_to_string(&deployed_path).unwrap(), "v1.0.0");
std::fs::write(bundled.join("tm-doctor.md"), "v1.1.0").unwrap();
let report = sync_session_assets(&ws_fw, &project_dir).unwrap();
assert!(report.skills_deployed.contains(&"tm-doctor".to_string()));
assert_eq!(std::fs::read_to_string(&deployed_path).unwrap(), "v1.1.0");
}
#[test]
fn sync_assets_refreshes_project_output_style() {
let tmp = tempfile::tempdir().unwrap();
let mut fw = FrameworkPaths::under(tmp.path().join("home"));
fw.trusty_mpm_root = None;
let project_dir = tmp.path().join("workspace");
std::fs::create_dir_all(&project_dir).unwrap();
let mut ws_fw = fw.clone();
ws_fw.claude_agents = project_dir.join(".claude").join("agents");
ws_fw.claude_skills = project_dir.join(".claude").join("skills");
let report = sync_session_assets(&ws_fw, &project_dir).unwrap();
assert!(report.output_style_synced);
assert!(
project_dir
.join(".claude")
.join("output-styles")
.join("trusty-mpm.md")
.exists()
);
}
#[test]
fn sync_assets_skips_user_modified_agent() {
let tmp = tempfile::tempdir().unwrap();
let mut fw = FrameworkPaths::under(tmp.path().join("home"));
fw.trusty_mpm_root = None;
let bundled = fw.agent_source_dir();
std::fs::create_dir_all(&bundled).unwrap();
std::fs::write(
bundled.join("rust-engineer.md"),
"---\nname: rust-engineer\n---\nv1",
)
.unwrap();
let project_dir = tmp.path().join("workspace");
std::fs::create_dir_all(&project_dir).unwrap();
let mut ws_fw = fw.clone();
ws_fw.claude_agents = project_dir.join(".claude").join("agents");
ws_fw.claude_skills = project_dir.join(".claude").join("skills");
sync_session_assets(&ws_fw, &project_dir).unwrap();
let deployed = ws_fw.claude_agents_dir().join("rust-engineer.md");
std::fs::write(&deployed, "operator hand-edited this").unwrap();
std::fs::write(
bundled.join("rust-engineer.md"),
"---\nname: rust-engineer\n---\nv2",
)
.unwrap();
let report = sync_session_assets(&ws_fw, &project_dir).unwrap();
assert!(
report
.agents_skipped
.contains(&"rust-engineer.md".to_string())
);
assert_eq!(
std::fs::read_to_string(&deployed).unwrap(),
"operator hand-edited this"
);
}
}