claude-plugin-validate 0.1.1

CLI validator for Claude Code plugin manifests and plugin content schemas.
Documentation
use serde_json::Value;

mod component;
mod manifest;

pub use component::validate_component_markdown;
pub use manifest::validate_plugin_manifest;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationIssue {
    pub path: String,
    pub code: String,
    pub message: String,
}

#[derive(Debug, Clone, PartialEq)]
pub enum ValidationResult {
    Success { data: Value },
    Failure { issues: Vec<ValidationIssue> },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ComponentValidation {
    pub path: String,
    pub errors: Vec<ValidationIssue>,
    pub warnings: Vec<ValidationIssue>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn manifest_valid_minimal() {
        let input = serde_json::json!({
            "name": "demo-plugin",
            "commands": {
                "about": {
                    "source": "./commands/about.md"
                }
            }
        });

        let result = validate_plugin_manifest(input);
        match result {
            ValidationResult::Success { .. } => {}
            ValidationResult::Failure { issues } => {
                panic!("expected success, got: {issues:#?}")
            }
        }
    }

    #[test]
    fn manifest_invalid_should_fail() {
        let input = serde_json::json!({
            "name": "bad plugin",
            "commands": {
                "x": {
                    "source": "commands/no-prefix.md",
                    "content": "bad"
                }
            },
            "hooks": {
                "NotARealHookEvent": []
            }
        });

        let result = validate_plugin_manifest(input);
        match result {
            ValidationResult::Failure { issues } => {
                assert!(!issues.is_empty());
            }
            ValidationResult::Success { .. } => panic!("expected failure"),
        }
    }

    #[test]
    fn component_frontmatter_validation() {
        let markdown = "---\nname: reviewer\ndescription: checks code\nallowed-tools:\n  - Bash\nshell: bash\n---\n\n# Reviewer\n";
        let result = validate_component_markdown("agents/reviewer.md", markdown, "agent");
        assert!(result.errors.is_empty());
    }

    #[test]
    fn component_invalid_frontmatter_validation() {
        let markdown =
            "---\nname: 1\ndescription:\n  nested: yes\nshell: fish\n---\n\n# Reviewer\n";
        let result = validate_component_markdown("agents/reviewer.md", markdown, "agent");
        assert!(!result.errors.is_empty());
    }

    #[test]
    fn fixture_manifest_valid() {
        let s =
            std::fs::read_to_string("fixtures/manifest/valid/plugin.json").expect("read fixture");
        let v: Value = serde_json::from_str(&s).expect("json");
        match validate_plugin_manifest(v) {
            ValidationResult::Success { .. } => {}
            ValidationResult::Failure { issues } => {
                panic!("expected fixture valid to pass, got {issues:#?}")
            }
        }
    }

    #[test]
    fn fixture_manifest_invalid() {
        let s =
            std::fs::read_to_string("fixtures/manifest/invalid/plugin.json").expect("read fixture");
        let v: Value = serde_json::from_str(&s).expect("json");
        match validate_plugin_manifest(v) {
            ValidationResult::Failure { issues } => assert!(!issues.is_empty()),
            ValidationResult::Success { .. } => panic!("expected fixture invalid to fail"),
        }
    }

    #[test]
    fn fixture_component_valid() {
        let s = std::fs::read_to_string("fixtures/components/valid/agent.md").expect("read md");
        let r = validate_component_markdown("fixtures/components/valid/agent.md", &s, "agent");
        assert!(r.errors.is_empty());
    }

    #[test]
    fn fixture_component_invalid() {
        let s = std::fs::read_to_string("fixtures/components/invalid/agent.md").expect("read md");
        let r = validate_component_markdown("fixtures/components/invalid/agent.md", &s, "agent");
        assert!(!r.errors.is_empty());
    }
}