claude_priority/validators/
naming.rs1use 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 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 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 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 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 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 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 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 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}