use std::cmp::Ordering;
use crate::doctor::check::{CheckResult, FixAction};
use crate::skill;
use super::Ctx;
pub fn check(_ctx: &Ctx) -> Vec<CheckResult> {
let binary = skill::binary_cli_version();
let mut out = Vec::new();
for name in skill::bundled_skill_names() {
let id = format!("skill.sync.{name}");
let suggest_install = format!("orchestratectl skill install {name} --force");
let Some(path) = skill::claude_default_path(name) else {
out.push(CheckResult::warn(
"skill.sync",
"cannot locate skill installs (HOME unset)",
"set HOME so the default skill path resolves",
));
break;
};
if !path.exists() {
out.push(CheckResult::warn(
id,
format!("skill '{name}' is not installed at {}", path.display()),
suggest_install,
));
continue;
}
let on_disk = skill::read_on_disk_cli_version(&path);
match on_disk
.as_deref()
.and_then(|v| compare(v, binary).map(|o| (v, o)))
{
Some((_, Ordering::Equal)) => {
out.push(CheckResult::ok(
id,
format!("skill '{name}' in sync at cli_version {binary}"),
));
}
Some((v, Ordering::Less)) => {
out.push(
CheckResult::warn(
id,
format!("skill '{name}' is cli_version {v}, binary is {binary}"),
suggest_install,
)
.with_safe_fix(FixAction::InstallSkill(name.to_string())),
);
}
Some((v, Ordering::Greater)) => {
out.push(CheckResult::warn(
id,
format!(
"skill '{name}' on disk is cli_version {v}, newer than binary {binary}"
),
"upgrade the orchestratectl binary to match the installed skill",
));
}
None => {
out.push(CheckResult::warn(
id,
format!(
"skill '{name}' has an unreadable/unparseable cli_version at {}",
path.display()
),
suggest_install,
));
}
}
if let Some(skill_dir) = path.parent() {
for companion in skill::companion_sources(name) {
let companion_path = skill_dir.join(companion.filename);
out.push(check_companion(name, &companion, &companion_path, binary));
}
}
}
for (name, dir) in skill::managed_orphans() {
out.push(CheckResult::warn(
format!("skill.orphan.{name}"),
format!(
"skill '{name}' at {} is orchestratectl-managed but no longer in the catalog (de-registered)",
dir.display()
),
"orchestratectl skill install --force",
));
}
out
}
fn check_companion(
skill_name: &str,
companion: &skill::CompanionSource,
path: &std::path::Path,
binary: &str,
) -> CheckResult {
let filename = companion.filename;
let id = format!("skill.sync.{skill_name}.{filename}");
let suggest_install = format!("orchestratectl skill install {skill_name} --force");
let on_disk = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return CheckResult::warn(
id,
format!(
"companion '{filename}' for skill '{skill_name}' is not installed at {}",
path.display()
),
suggest_install,
);
}
Err(e) => {
return CheckResult::warn(
id,
format!(
"companion '{filename}' for skill '{skill_name}' is unreadable at {}: {e}",
path.display()
),
suggest_install,
);
}
};
if on_disk == companion.bundled_body {
return CheckResult::ok(
id,
format!(
"companion '{filename}' for skill '{skill_name}' matches the bundled content for binary {binary}"
),
);
}
let disk_version = skill::cli_version_of(&on_disk);
match disk_version
.as_deref()
.and_then(|v| compare(v, binary).map(|o| (v, o)))
{
Some((v, Ordering::Less)) => CheckResult::warn(
id,
format!(
"companion '{filename}' for skill '{skill_name}' is cli_version {v}, binary is {binary}"
),
suggest_install,
)
.with_safe_fix(FixAction::InstallSkill(skill_name.to_string())),
Some((v, Ordering::Greater)) => CheckResult::warn(
id,
format!(
"companion '{filename}' for skill '{skill_name}' differs from the bundled copy and declares cli_version {v}, newer than binary {binary}"
),
"upgrade the orchestratectl binary, or reinstall with --force to restore the bundled companion",
),
Some((_, Ordering::Equal)) => CheckResult::warn(
id,
format!(
"companion '{filename}' for skill '{skill_name}' differs from the bundled copy while its cli_version matches binary {binary} (possible local edits)"
),
suggest_install,
),
None => CheckResult::warn(
id,
format!(
"companion '{filename}' for skill '{skill_name}' differs from the bundled copy and declares no parseable cli_version at {}",
path.display()
),
suggest_install,
),
}
}
fn compare(a: &str, b: &str) -> Option<Ordering> {
let av = semver::Version::parse(a).ok()?;
let bv = semver::Version::parse(b).ok()?;
Some(av.cmp(&bv))
}