use std::collections::BTreeSet;
use std::path::Path;
use crate::core::doctor::{CheckStatus, DoctorCheck};
use crate::core::paths::FrameworkPaths;
use crate::core::skill_deploy_tiers::skill_deploy_tiers;
use crate::core::skill_tiers::list_source_stems;
use crate::core::skill_unmanaged::unmanaged_bundled_skills;
const CHECK_NAME: &str = "skill_unmanaged";
const MAX_NAMED: usize = 5;
pub(super) fn check_skill_unmanaged(
paths: &FrameworkPaths,
project_dir: Option<&Path>,
) -> DoctorCheck {
let bundled: BTreeSet<String> =
list_source_stems(&paths.skill_source_dir()).unwrap_or_default();
if bundled.is_empty() {
return DoctorCheck::new(
CHECK_NAME,
CheckStatus::Unknown,
"no bundled skill source found — cannot tell which deployed skills are tm's \
(run `tm install` to populate it)",
);
}
let mut findings: Vec<String> = Vec::new();
for tier in skill_deploy_tiers(paths, project_dir) {
let found = match unmanaged_bundled_skills(&tier.dir, &bundled) {
Ok(found) => found,
Err(e) => {
return DoctorCheck::new(
CHECK_NAME,
CheckStatus::Unknown,
format!(
"the ownership ledger at {} could not be read, so which deployed \
skills are tm's is undetermined: {e}",
tier.dir.display()
),
);
}
};
for skill in found {
findings.push(format!("{}/{}", tier.label, skill.stem));
}
}
if findings.is_empty() {
return DoctorCheck::new(
CHECK_NAME,
CheckStatus::Ok,
"every deployed bundled skill is tracked by its tier's manifest",
);
}
let shown: Vec<&str> = findings
.iter()
.take(MAX_NAMED)
.map(String::as_str)
.collect();
let suffix = if findings.len() > shown.len() {
format!(", … (+{} more)", findings.len() - shown.len())
} else {
String::new()
};
DoctorCheck::new(
CHECK_NAME,
CheckStatus::Unknown,
format!(
"{} bundled skill(s) are deployed but UNMANAGED ({}{}) — untracked at that tier, \
so no deploy can refresh them and their contents cannot be verified; \
run `tm install --reconcile-skills` to adopt and refresh them",
findings.len(),
shown.join(", "),
suffix
),
)
}
#[cfg(test)]
#[path = "doctor_skill_unmanaged_tests.rs"]
mod tests;