cc_audit/discovery/
patterns.rs1use std::path::Path;
4
5#[derive(Debug, Clone)]
7pub struct FilePattern {
8 pub root_dirs: &'static [&'static str],
10 pub extensions: &'static [&'static str],
12 pub file_names: &'static [&'static str],
14}
15
16impl FilePattern {
17 pub fn matches(&self, path: &Path) -> bool {
19 if let Some(file_name) = path.file_name().and_then(|n| n.to_str())
21 && self.file_names.contains(&file_name)
22 {
23 return true;
24 }
25
26 if !self.extensions.is_empty() {
28 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
29 return self.extensions.contains(&ext);
30 }
31 return false;
32 }
33
34 true
35 }
36
37 pub fn is_in_root_dir(&self, path: &Path, base: &Path) -> bool {
39 if self.root_dirs.is_empty() {
40 return true;
41 }
42
43 for root in self.root_dirs {
44 let root_path = base.join(root);
45 if path.starts_with(&root_path) {
46 return true;
47 }
48 }
49
50 false
51 }
52}
53
54pub static SKILL_PATTERNS: FilePattern = FilePattern {
56 root_dirs: &[".claude", "scripts"],
57 extensions: &["md"],
58 file_names: &["SKILL.md", "CLAUDE.md"],
59};
60
61pub static COMMAND_PATTERNS: FilePattern = FilePattern {
63 root_dirs: &[".claude/commands", "commands"],
64 extensions: &["md"],
65 file_names: &[],
66};
67
68pub static MCP_PATTERNS: FilePattern = FilePattern {
70 root_dirs: &[".claude"],
71 extensions: &["json"],
72 file_names: &["mcp.json", ".mcp.json"],
73};
74
75pub static DEPENDENCY_PATTERNS: FilePattern = FilePattern {
77 root_dirs: &[],
78 extensions: &["json", "toml", "txt", "lock"],
79 file_names: &[
80 "package.json",
81 "package-lock.json",
82 "Cargo.toml",
83 "Cargo.lock",
84 "requirements.txt",
85 "Pipfile",
86 "Pipfile.lock",
87 "pyproject.toml",
88 "poetry.lock",
89 "go.mod",
90 "go.sum",
91 "Gemfile",
92 "Gemfile.lock",
93 "composer.json",
94 "composer.lock",
95 "pom.xml",
96 "build.gradle",
97 "build.gradle.kts",
98 ],
99};
100
101pub static DOCKER_PATTERNS: FilePattern = FilePattern {
103 root_dirs: &[],
104 extensions: &["yml", "yaml"],
105 file_names: &[
106 "Dockerfile",
107 "dockerfile",
108 "docker-compose.yml",
109 "docker-compose.yaml",
110 "compose.yml",
111 "compose.yaml",
112 ],
113};
114
115pub static HOOK_PATTERNS: FilePattern = FilePattern {
117 root_dirs: &[".claude"],
118 extensions: &["json"],
119 file_names: &["settings.json"],
120};
121
122pub static SUBAGENT_PATTERNS: FilePattern = FilePattern {
124 root_dirs: &[".claude/agents"],
125 extensions: &["md", "yaml", "yml"],
126 file_names: &[],
127};
128
129pub static RULES_DIR_PATTERNS: FilePattern = FilePattern {
131 root_dirs: &[".claude/rules", "rules"],
132 extensions: &["md", "yaml", "yml"],
133 file_names: &[],
134};
135
136pub static PLUGIN_PATTERNS: FilePattern = FilePattern {
138 root_dirs: &[],
139 extensions: &["json"],
140 file_names: &["plugin.json", "marketplace.json"],
141};
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn test_skill_patterns_matches_skill_md() {
149 assert!(SKILL_PATTERNS.matches(Path::new("SKILL.md")));
150 assert!(SKILL_PATTERNS.matches(Path::new("path/to/SKILL.md")));
151 }
152
153 #[test]
154 fn test_skill_patterns_matches_claude_md() {
155 assert!(SKILL_PATTERNS.matches(Path::new("CLAUDE.md")));
156 assert!(SKILL_PATTERNS.matches(Path::new(".claude/CLAUDE.md")));
157 }
158
159 #[test]
160 fn test_skill_patterns_matches_md_extension() {
161 assert!(SKILL_PATTERNS.matches(Path::new("readme.md")));
162 assert!(!SKILL_PATTERNS.matches(Path::new("file.txt")));
163 }
164
165 #[test]
166 fn test_command_patterns() {
167 assert!(COMMAND_PATTERNS.matches(Path::new("test.md")));
168 assert!(!COMMAND_PATTERNS.matches(Path::new("test.txt")));
169 }
170
171 #[test]
172 fn test_mcp_patterns() {
173 assert!(MCP_PATTERNS.matches(Path::new("mcp.json")));
174 assert!(MCP_PATTERNS.matches(Path::new(".mcp.json")));
175 assert!(!MCP_PATTERNS.matches(Path::new("config.yaml")));
176 }
177
178 #[test]
179 fn test_dependency_patterns() {
180 assert!(DEPENDENCY_PATTERNS.matches(Path::new("package.json")));
181 assert!(DEPENDENCY_PATTERNS.matches(Path::new("Cargo.toml")));
182 assert!(DEPENDENCY_PATTERNS.matches(Path::new("requirements.txt")));
183 assert!(!DEPENDENCY_PATTERNS.matches(Path::new("README.md")));
184 }
185
186 #[test]
187 fn test_docker_patterns() {
188 assert!(DOCKER_PATTERNS.matches(Path::new("Dockerfile")));
189 assert!(DOCKER_PATTERNS.matches(Path::new("docker-compose.yml")));
190 assert!(DOCKER_PATTERNS.matches(Path::new("compose.yaml")));
191 }
192
193 #[test]
194 fn test_is_in_root_dir() {
195 let base = Path::new("/project");
196
197 assert!(SKILL_PATTERNS.is_in_root_dir(Path::new("/project/.claude/CLAUDE.md"), base));
198 assert!(SKILL_PATTERNS.is_in_root_dir(Path::new("/project/scripts/helper.md"), base));
199 assert!(!SKILL_PATTERNS.is_in_root_dir(Path::new("/project/other/file.md"), base));
200 }
201}