use std::path::Path;
use crate::core::agent_deployer::deploy_agents_filtered;
use crate::core::agent_manifest::{AgentManifest, MANIFEST_FILE as AGENT_MANIFEST_FILE};
use crate::core::manifest::HarnessPlan;
use crate::core::paths::FrameworkPaths;
use crate::core::skill_manifest::{SKILL_MANIFEST_FILE, SkillManifest};
use crate::core::skill_tiers::deploy_all_skill_tiers;
use crate::provisioner::GitBackend;
#[derive(Debug, thiserror::Error)]
pub enum ApplyError {
#[error("catalog sync failed: {0}")]
Sync(String),
#[error("agent redeploy failed: {0}")]
AgentDeploy(String),
#[error("skill redeploy failed: {0}")]
SkillDeploy(String),
#[error("prune io error: {0}")]
Prune(String),
}
#[derive(Debug, Default)]
pub struct ApplyReport {
pub fetched: bool,
pub agents_deployed: Vec<String>,
pub agents_skipped: Vec<String>,
pub skills_deployed: Vec<String>,
pub skills_skipped: Vec<String>,
pub agents_pruned: Vec<String>,
pub skills_pruned: Vec<String>,
}
pub fn apply_catalog<G: GitBackend>(
git: G,
fw: &FrameworkPaths,
project_dir: &Path,
force: bool,
prune: bool,
) -> Result<ApplyReport, ApplyError> {
let catalog_root = crate::content::catalog_root_for(&fw.root);
let config = crate::core::config::MpmConfig::load(&fw.root);
let sync =
crate::content::CatalogSync::with_config(git, catalog_root.clone(), Some(&config.manifest));
let sync_result = sync
.sync(force)
.map_err(|e| ApplyError::Sync(e.to_string()))?;
let sources =
crate::core::manifest::ManifestSources::resolve(project_dir, &fw.root, &catalog_root);
let manifest = crate::core::manifest::resolve_manifest(&sources);
let plan = HarnessPlan::from_manifest(&manifest, fw, &catalog_root);
let mut report = ApplyReport {
fetched: sync_result.fetched,
..ApplyReport::default()
};
let agent_target = fw.claude_agents_dir();
let deploy = deploy_agents_filtered(&plan.agent_source, &agent_target, |name| {
plan.agent_selected(name)
})
.map_err(|e| ApplyError::AgentDeploy(e.to_string()))?;
report.agents_deployed = deploy.deployed;
report.agents_skipped = deploy.skipped;
let skill_target = fw.claude_skills_dir();
let skill_deploy = deploy_all_skill_tiers(
&plan.skill_source,
&fw.user_skill_source_dir(),
&skill_target,
|name| plan.skill_selected(name),
)
.map_err(|e| ApplyError::SkillDeploy(e.to_string()))?;
report.skills_deployed = skill_deploy.stats.deployed;
report.skills_skipped = skill_deploy.stats.skipped;
if prune {
report.agents_pruned = prune_agents(&agent_target, &plan)?;
report.skills_pruned = prune_skills(&skill_target, &plan)?;
}
Ok(report)
}
fn prune_agents(target: &Path, plan: &HarnessPlan) -> Result<Vec<String>, ApplyError> {
let mut manifest = AgentManifest::load(target);
let mut pruned: Vec<String> = Vec::new();
let candidates: Vec<String> = manifest.managed.keys().cloned().collect();
for filename in candidates {
let stem = filename.trim_end_matches(".md");
if plan.agent_selected(stem) {
continue;
}
remove_if_present(&target.join(&filename))?;
manifest.managed.remove(&filename);
pruned.push(filename);
}
if !pruned.is_empty() {
manifest
.save(target)
.map_err(|e| ApplyError::Prune(e.to_string()))?;
debug_assert!(target.join(AGENT_MANIFEST_FILE).exists());
}
pruned.sort_unstable();
Ok(pruned)
}
fn prune_skills(target: &Path, plan: &HarnessPlan) -> Result<Vec<String>, ApplyError> {
let mut manifest = SkillManifest::load(target);
let mut pruned: Vec<String> = Vec::new();
let candidates: Vec<String> = manifest.managed.keys().cloned().collect();
for stem in candidates {
if plan.skill_selected(&stem) {
continue;
}
let skill_dir = target.join(&stem);
if skill_dir.is_dir() {
std::fs::remove_dir_all(&skill_dir).map_err(|e| ApplyError::Prune(e.to_string()))?;
}
manifest.managed.remove(&stem);
pruned.push(stem);
}
if !pruned.is_empty() {
manifest
.save(target)
.map_err(|e| ApplyError::Prune(e.to_string()))?;
debug_assert!(target.join(SKILL_MANIFEST_FILE).exists());
}
pruned.sort_unstable();
Ok(pruned)
}
fn remove_if_present(path: &Path) -> Result<(), ApplyError> {
match std::fs::remove_file(path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(ApplyError::Prune(e.to_string())),
}
}
#[cfg(test)]
mod tests;