claude-plugin-validate 0.1.1

CLI validator for Claude Code plugin manifests and plugin content schemas.
Documentation
use super::common::{is_rel_path, obj_get, push_issue};
use crate::ValidationIssue;
use serde_json::{Map, Value};

pub(super) fn validate_agents_skills_output_styles(
    root: &Map<String, Value>,
    issues: &mut Vec<ValidationIssue>,
) {
    validate_path_or_path_array(root, "agents", true, issues);
    validate_path_or_path_array(root, "skills", false, issues);
    validate_path_or_path_array(root, "outputStyles", false, issues);
}

fn validate_path_or_path_array(
    root: &Map<String, Value>,
    key: &str,
    markdown_only: bool,
    issues: &mut Vec<ValidationIssue>,
) {
    let Some(value) = obj_get(root, key) else {
        return;
    };

    let validate_one = |path: &str| -> Option<&'static str> {
        if !is_rel_path(path) {
            return Some("Path must be relative and start with ./");
        }
        if markdown_only && !path.ends_with(".md") {
            return Some("Path must point to a .md file");
        }
        None
    };

    if let Some(s) = value.as_str() {
        if let Some(msg) = validate_one(s) {
            push_issue(issues, key, "invalid_string", msg);
        }
        return;
    }

    if let Some(arr) = value.as_array() {
        for (idx, item) in arr.iter().enumerate() {
            match item.as_str() {
                Some(s) => {
                    if let Some(msg) = validate_one(s) {
                        push_issue(issues, &format!("{key}.{idx}"), "invalid_string", msg);
                    }
                }
                None => push_issue(
                    issues,
                    &format!("{key}.{idx}"),
                    "invalid_type",
                    "Item must be a string path",
                ),
            }
        }
        return;
    }

    push_issue(
        issues,
        key,
        "invalid_type",
        "Value must be a string path or array of string paths",
    );
}