cc_audit/discovery/
targets.rs1use std::path::PathBuf;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum TargetKind {
8 Skill,
10 Command,
12 Mcp,
14 Hook,
16 Dependency,
18 Docker,
20 Plugin,
22 Subagent,
24 RulesDir,
26 TextFile,
28 Unknown,
30}
31
32impl TargetKind {
33 pub fn from_path(path: &std::path::Path) -> Self {
35 let file_name = path
36 .file_name()
37 .and_then(|n| n.to_str())
38 .unwrap_or_default();
39
40 match file_name {
42 "SKILL.md" | "CLAUDE.md" => return Self::Skill,
43 "mcp.json" | ".mcp.json" => return Self::Mcp,
44 "settings.json" => return Self::Hook,
45 "plugin.json" | "marketplace.json" => return Self::Plugin,
46 "Dockerfile" | "dockerfile" => return Self::Docker,
47 "package.json" | "Cargo.toml" | "requirements.txt" | "Pipfile" | "pyproject.toml"
48 | "go.mod" | "Gemfile" | "composer.json" | "pom.xml" => return Self::Dependency,
49 _ => {}
50 }
51
52 let path_str = path.to_string_lossy();
54
55 if (path_str.contains(".claude/commands/")
56 || path_str.contains("/commands/")
57 || path_str.starts_with("commands/"))
58 && file_name.ends_with(".md")
59 {
60 return Self::Command;
61 }
62
63 if path_str.contains(".claude/agents/") || path_str.starts_with(".claude/agents/") {
64 return Self::Subagent;
65 }
66
67 if path_str.contains(".claude/rules/")
68 || path_str.contains("/rules/")
69 || path_str.starts_with("rules/")
70 || path_str.starts_with(".claude/rules/")
71 {
72 return Self::RulesDir;
73 }
74
75 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
77 match ext {
78 "md" => {
79 if path_str.contains("scripts/") || path_str.contains(".claude/") {
80 return Self::Skill;
81 }
82 return Self::TextFile;
83 }
84 "json" => return Self::Unknown,
85 "yml" | "yaml" => {
86 if file_name.contains("docker-compose") || file_name.contains("compose") {
87 return Self::Docker;
88 }
89 return Self::Unknown;
90 }
91 _ => return Self::Unknown,
92 }
93 }
94
95 Self::Unknown
96 }
97}
98
99#[derive(Debug, Clone)]
101pub struct ScanTarget {
102 pub path: PathBuf,
104 pub kind: TargetKind,
106 pub source: DiscoverySource,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
112pub enum DiscoverySource {
113 UserSpecified,
115 FileSystem,
117 ClientDetection,
119 Remote,
121}
122
123impl ScanTarget {
124 pub fn new(path: PathBuf, kind: TargetKind, source: DiscoverySource) -> Self {
126 Self { path, kind, source }
127 }
128
129 pub fn from_path(path: PathBuf, source: DiscoverySource) -> Self {
131 let kind = TargetKind::from_path(&path);
132 Self { path, kind, source }
133 }
134
135 pub fn user_specified(path: PathBuf) -> Self {
137 Self::from_path(path, DiscoverySource::UserSpecified)
138 }
139
140 pub fn discovered(path: PathBuf) -> Self {
142 Self::from_path(path, DiscoverySource::FileSystem)
143 }
144
145 pub fn from_client(path: PathBuf) -> Self {
147 Self::from_path(path, DiscoverySource::ClientDetection)
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use std::path::Path;
155
156 #[test]
157 fn test_target_kind_skill() {
158 assert_eq!(
159 TargetKind::from_path(Path::new("SKILL.md")),
160 TargetKind::Skill
161 );
162 assert_eq!(
163 TargetKind::from_path(Path::new("CLAUDE.md")),
164 TargetKind::Skill
165 );
166 assert_eq!(
167 TargetKind::from_path(Path::new(".claude/CLAUDE.md")),
168 TargetKind::Skill
169 );
170 }
171
172 #[test]
173 fn test_target_kind_command() {
174 assert_eq!(
175 TargetKind::from_path(Path::new(".claude/commands/test.md")),
176 TargetKind::Command
177 );
178 assert_eq!(
179 TargetKind::from_path(Path::new("commands/deploy.md")),
180 TargetKind::Command
181 );
182 }
183
184 #[test]
185 fn test_target_kind_mcp() {
186 assert_eq!(
187 TargetKind::from_path(Path::new("mcp.json")),
188 TargetKind::Mcp
189 );
190 assert_eq!(
191 TargetKind::from_path(Path::new(".mcp.json")),
192 TargetKind::Mcp
193 );
194 }
195
196 #[test]
197 fn test_target_kind_dependency() {
198 assert_eq!(
199 TargetKind::from_path(Path::new("package.json")),
200 TargetKind::Dependency
201 );
202 assert_eq!(
203 TargetKind::from_path(Path::new("Cargo.toml")),
204 TargetKind::Dependency
205 );
206 assert_eq!(
207 TargetKind::from_path(Path::new("requirements.txt")),
208 TargetKind::Dependency
209 );
210 }
211
212 #[test]
213 fn test_target_kind_docker() {
214 assert_eq!(
215 TargetKind::from_path(Path::new("Dockerfile")),
216 TargetKind::Docker
217 );
218 assert_eq!(
219 TargetKind::from_path(Path::new("docker-compose.yml")),
220 TargetKind::Docker
221 );
222 }
223
224 #[test]
225 fn test_target_kind_plugin() {
226 assert_eq!(
227 TargetKind::from_path(Path::new("plugin.json")),
228 TargetKind::Plugin
229 );
230 assert_eq!(
231 TargetKind::from_path(Path::new("marketplace.json")),
232 TargetKind::Plugin
233 );
234 }
235
236 #[test]
237 fn test_target_kind_subagent() {
238 assert_eq!(
239 TargetKind::from_path(Path::new(".claude/agents/helper.md")),
240 TargetKind::Subagent
241 );
242 }
243
244 #[test]
245 fn test_target_kind_rules_dir() {
246 assert_eq!(
247 TargetKind::from_path(Path::new(".claude/rules/custom.md")),
248 TargetKind::RulesDir
249 );
250 assert_eq!(
251 TargetKind::from_path(Path::new("rules/security.md")),
252 TargetKind::RulesDir
253 );
254 }
255
256 #[test]
257 fn test_scan_target_creation() {
258 let target = ScanTarget::user_specified(PathBuf::from("SKILL.md"));
259 assert_eq!(target.kind, TargetKind::Skill);
260 assert_eq!(target.source, DiscoverySource::UserSpecified);
261
262 let target = ScanTarget::discovered(PathBuf::from("package.json"));
263 assert_eq!(target.kind, TargetKind::Dependency);
264 assert_eq!(target.source, DiscoverySource::FileSystem);
265
266 let target = ScanTarget::from_client(PathBuf::from(".claude/CLAUDE.md"));
267 assert_eq!(target.kind, TargetKind::Skill);
268 assert_eq!(target.source, DiscoverySource::ClientDetection);
269 }
270}