claude_priority/validators/
naming.rs

1use crate::models::{CheckType, ValidationResult};
2use regex::Regex;
3use std::fs;
4use std::path::Path;
5use walkdir::WalkDir;
6
7const VALID_NAME_PATTERN: &str = r"^[a-z0-9-]+$";
8
9pub fn validate_naming(plugin_path: &Path) -> ValidationResult {
10    let mut result = ValidationResult::new(CheckType::Naming);
11    let pattern = Regex::new(VALID_NAME_PATTERN).unwrap();
12
13    // Security: Validate path doesn't contain directory traversal
14    if let Some(path_str) = plugin_path.to_str() {
15        if path_str.contains("..") {
16            result.add_error("Security Error: Directory traversal not allowed".to_string());
17            return result;
18        }
19    }
20
21    // Check plugin directory name
22    if let Some(plugin_name) = plugin_path.file_name().and_then(|n| n.to_str()) {
23        if !pattern.is_match(plugin_name) {
24            result.add_error(format!(
25                "Plugin directory '{}' violates naming convention. Must be lowercase-with-hyphens (pattern: {})",
26                plugin_name, VALID_NAME_PATTERN
27            ));
28        }
29    }
30
31    // Check for marketplace.json
32    let marketplace_json = plugin_path.join("marketplace.json");
33    if !marketplace_json.exists() {
34        result.add_error("Missing marketplace.json in plugin root".to_string());
35    }
36
37    // Check skills directory
38    let skills_dir = plugin_path.join("skills");
39    if skills_dir.exists() && skills_dir.is_dir() {
40        for entry in WalkDir::new(&skills_dir)
41            .min_depth(1)
42            .max_depth(1)
43            .into_iter()
44            .filter_map(|e| e.ok())
45        {
46            if entry.file_type().is_dir() {
47                if let Some(skill_name) = entry.file_name().to_str() {
48                    if !pattern.is_match(skill_name) {
49                        result.add_error(format!(
50                            "Skill directory '{}' violates naming convention. Must be lowercase-with-hyphens",
51                            skill_name
52                        ));
53                    } else {
54                        // Check for skill.md
55                        let skill_md = entry.path().join("skill.md");
56                        if !skill_md.exists() {
57                            result.add_error(format!(
58                                "Missing skill.md in: {}",
59                                skill_name
60                            ));
61                        } else {
62                            // Check frontmatter name matches directory
63                            if let Ok(content) = fs::read_to_string(&skill_md) {
64                                if let Some(fm_name) = extract_frontmatter_name(&content) {
65                                    if fm_name != skill_name {
66                                        result.add_warning(format!(
67                                            "Frontmatter name '{}' doesn't match directory '{}'",
68                                            fm_name, skill_name
69                                        ));
70                                    }
71                                }
72                            }
73                        }
74                    }
75                }
76            }
77        }
78    }
79
80    // Check commands directory if it exists
81    let commands_dir = plugin_path.join("commands");
82    if commands_dir.exists() && commands_dir.is_dir() {
83        for entry in WalkDir::new(&commands_dir)
84            .min_depth(1)
85            .max_depth(1)
86            .into_iter()
87            .filter_map(|e| e.ok())
88        {
89            if entry.file_type().is_dir() {
90                if let Some(command_name) = entry.file_name().to_str() {
91                    if !pattern.is_match(command_name) {
92                        result.add_error(format!(
93                            "Command directory '{}' violates naming convention. Must be lowercase-with-hyphens",
94                            command_name
95                        ));
96                    }
97                }
98            }
99        }
100    }
101
102    // Check agents directory if it exists
103    let agents_dir = plugin_path.join("agents");
104    if agents_dir.exists() && agents_dir.is_dir() {
105        for entry in WalkDir::new(&agents_dir)
106            .min_depth(1)
107            .max_depth(1)
108            .into_iter()
109            .filter_map(|e| e.ok())
110        {
111            if entry.file_type().is_dir() {
112                if let Some(agent_name) = entry.file_name().to_str() {
113                    if !pattern.is_match(agent_name) {
114                        result.add_error(format!(
115                            "Agent directory '{}' violates naming convention. Must be lowercase-with-hyphens",
116                            agent_name
117                        ));
118                    }
119                }
120            }
121        }
122    }
123
124    result
125}
126
127fn extract_frontmatter_name(content: &str) -> Option<String> {
128    let lines: Vec<&str> = content.lines().collect();
129    if lines.is_empty() || lines[0] != "---" {
130        return None;
131    }
132
133    let mut in_frontmatter = false;
134    for line in lines.iter().skip(1) {
135        if *line == "---" {
136            break;
137        }
138        if !in_frontmatter {
139            in_frontmatter = true;
140        }
141
142        if line.starts_with("name:") {
143            let name = line.strip_prefix("name:")
144                .map(|s| s.trim())
145                .map(|s| s.to_string());
146            return name;
147        }
148    }
149
150    None
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_extract_frontmatter_name() {
159        let content = "---\nname: test-plugin\ndescription: Test\n---\n";
160        assert_eq!(extract_frontmatter_name(content), Some("test-plugin".to_string()));
161    }
162
163    #[test]
164    fn test_extract_frontmatter_no_name() {
165        let content = "---\ndescription: Test\n---\n";
166        assert_eq!(extract_frontmatter_name(content), None);
167    }
168}