use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use trusty_mpm::core::paths::FrameworkPaths;
use trusty_mpm::core::skill_deploy_tiers::{SkillDeployTier, skill_deploy_tiers};
use trusty_mpm::core::skill_reconcile::adopt_unmanaged_bundled_skills;
use trusty_mpm::core::skill_tiers::{deploy_all_skill_tiers, list_source_stems};
use trusty_mpm::core::skill_unmanaged::unmanaged_bundled_skills;
pub(crate) const RECONCILE_BACKUP_PREFIX: &str = "backup-reconcile-skills-";
pub(crate) fn backup_root(paths: &FrameworkPaths) -> PathBuf {
let stamp = chrono::Utc::now().format("%Y%m%d%H%M%S");
paths.root.join(format!("{RECONCILE_BACKUP_PREFIX}{stamp}"))
}
fn scope(
paths: &FrameworkPaths,
project_dir: Option<&Path>,
) -> (BTreeSet<String>, Vec<SkillDeployTier>) {
let bundled = list_source_stems(&paths.skill_source_dir()).unwrap_or_default();
(bundled, skill_deploy_tiers(paths, project_dir))
}
pub(crate) fn unmanaged_report_lines(
paths: &FrameworkPaths,
project_dir: Option<&Path>,
) -> Vec<String> {
let (bundled, tiers) = scope(paths, project_dir);
if bundled.is_empty() {
return Vec::new();
}
let mut lines = Vec::new();
for tier in tiers {
for skill in unmanaged_bundled_skills(&tier.dir, &bundled) {
lines.push(format!(
"! {} (untracked at {} \u{2014} not managed, not refreshed; \
see `tm install --reconcile-skills`)",
skill.stem,
tier.dir.display()
));
}
}
lines
}
pub(crate) fn reconcile_skills(
paths: &FrameworkPaths,
project_dir: Option<&Path>,
) -> anyhow::Result<()> {
let (bundled, tiers) = scope(paths, project_dir);
if bundled.is_empty() {
println!(
"Reconcile skipped: no bundled skill source at {} — run `tm install` first.",
paths.skill_source_dir().display()
);
return Ok(());
}
let backups = backup_root(paths);
let mut planned = 0usize;
println!(
"Reconciling unmanaged bundled skills (backups: {})",
backups.display()
);
for tier in &tiers {
let found = unmanaged_bundled_skills(&tier.dir, &bundled);
if found.is_empty() {
println!(
" {} ({}): nothing unmanaged",
tier.label,
tier.dir.display()
);
continue;
}
println!(" {} ({}):", tier.label, tier.dir.display());
for skill in &found {
planned += 1;
for file in &skill.files {
println!(" will adopt + refresh {}", file.display());
}
}
}
if planned == 0 {
println!(" nothing to reconcile.");
return Ok(());
}
for tier in &tiers {
let adopted = adopt_unmanaged_bundled_skills(&tier.dir, &bundled, &backups)?;
if adopted.is_empty() {
continue;
}
for skill in &adopted {
println!(
" \u{2713} adopted {} at {} (backup: {})",
skill.stem,
tier.dir.display(),
skill.backup_dir.display()
);
}
let stats = deploy_all_skill_tiers(
&paths.skill_source_dir(),
&paths.user_skill_source_dir(),
&tier.dir,
|_| true,
)?
.stats;
for skill in &adopted {
if stats.deployed.contains(&skill.stem) {
println!(" \u{2713} {} refreshed from bundled source", skill.stem);
} else {
println!(" = {} (already matched bundled source)", skill.stem);
}
}
}
Ok(())
}
#[cfg(test)]
#[path = "install_skills_tests.rs"]
mod tests;