use crate::check::Check;
use crate::checks::project::bundle_exists::find_bundle;
use crate::project::Project;
use crate::runner::HelpOutput;
use crate::types::{CheckGroup, CheckLayer, CheckResult, CheckStatus, Confidence};
pub struct BundleInstallCheck;
impl Check for BundleInstallCheck {
fn id(&self) -> &str {
"p8-bundle-install"
}
fn label(&self) -> &'static str {
"Skill bundle has install path (`tool skill install [<host>]`)"
}
fn group(&self) -> CheckGroup {
CheckGroup::P8
}
fn layer(&self) -> CheckLayer {
CheckLayer::Behavioral
}
fn covers(&self) -> &'static [&'static str] {
&["p8-must-bundle-install"]
}
fn applicable(&self, project: &Project) -> bool {
project.runner.is_some()
}
fn run(&self, project: &Project) -> anyhow::Result<CheckResult> {
if find_bundle(&project.path).is_none() {
return Ok(CheckResult {
id: self.id().to_string(),
label: self.label().into(),
group: self.group(),
layer: self.layer(),
status: CheckStatus::Pass,
confidence: Confidence::High,
});
}
let status = match project.help_output() {
None => CheckStatus::Skip("could not probe --help".into()),
Some(help) => check_bundle_install(help),
};
Ok(CheckResult {
id: self.id().to_string(),
label: self.label().into(),
group: self.group(),
layer: self.layer(),
status,
confidence: Confidence::Medium,
})
}
}
pub(crate) fn check_bundle_install(help: &HelpOutput) -> CheckStatus {
let subs = help.subcommands();
let has_skill_subcommand = subs.iter().any(|s| s.eq_ignore_ascii_case("skill"));
if has_skill_subcommand {
return CheckStatus::Pass;
}
let raw = help.raw().to_lowercase();
let non_canonical_patterns = ["init --skill", "skills add", "agents add"];
if non_canonical_patterns.iter().any(|p| raw.contains(p)) {
return CheckStatus::Pass;
}
CheckStatus::Fail(
"skill bundle present but no install path (`skill install`, \
`init --skill`, etc.) advertised in --help. Without an install \
path the bundle stays unread until a human manually copies it."
.into(),
)
}
#[cfg(test)]
mod tests {
use super::*;
const HELP_WITH_SKILL_SUBCMD: &str = r#"Usage: tool [OPTIONS] <COMMAND>
Commands:
check Run checks
skill Manage skill bundle installation
schema Print output schema
Options:
-h, --help Show help
"#;
const HELP_NO_SKILL_SURFACE: &str = r#"Usage: tool [OPTIONS] <COMMAND>
Commands:
check Run checks
schema Print output schema
Options:
-h, --help Show help
"#;
const HELP_INIT_SKILL: &str = r#"Usage: tool [OPTIONS] <COMMAND>
Commands:
check Run checks
init Initialize the project; pass `init --skill` to install bundle.
Options:
-h, --help Show help
"#;
#[test]
fn happy_path_skill_subcommand() {
let help = HelpOutput::from_raw(HELP_WITH_SKILL_SUBCMD);
assert_eq!(check_bundle_install(&help), CheckStatus::Pass);
}
#[test]
fn pass_with_non_canonical_init_skill() {
let help = HelpOutput::from_raw(HELP_INIT_SKILL);
assert_eq!(check_bundle_install(&help), CheckStatus::Pass);
}
#[test]
fn fail_no_skill_install_path() {
let help = HelpOutput::from_raw(HELP_NO_SKILL_SURFACE);
match check_bundle_install(&help) {
CheckStatus::Fail(msg) => assert!(msg.contains("install")),
other => panic!("expected Fail, got {other:?}"),
}
}
}