1use include_dir::{Dir, include_dir};
24use std::collections::HashMap;
25
26static COMMANDS_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/src/commands");
28
29const MAX_DESCRIPTION_LEN: usize = 60;
31
32pub fn load_predefined_commands() -> Vec<(String, String, String)> {
42 let mut best: HashMap<String, (u32, String, String)> = HashMap::new();
44
45 for file in COMMANDS_DIR.files() {
46 let path = file.path();
47
48 let ext = path.extension().and_then(|e| e.to_str());
50 if ext != Some("md") {
51 continue;
52 }
53
54 let stem = match path.file_stem().and_then(|s| s.to_str()) {
55 Some(s) => s,
56 None => continue,
57 };
58
59 let (name, version) = parse_versioned_name(stem);
61
62 if !is_valid_command_name(&name) {
64 continue;
65 }
66
67 let content = match file.contents_utf8() {
68 Some(c) => c.trim(),
69 None => continue,
70 };
71
72 if content.is_empty() {
73 continue;
74 }
75
76 if let Some(existing) = best.get(&name)
78 && existing.0 >= version
79 {
80 continue;
81 }
82
83 let (description, prompt_body) = extract_front_matter(content);
84
85 let description = description.unwrap_or_else(|| {
86 let first_line = prompt_body
87 .lines()
88 .find(|l| !l.trim().is_empty())
89 .unwrap_or("Predefined command");
90 let first_line = first_line.trim();
91 if first_line.len() > MAX_DESCRIPTION_LEN {
92 let truncated: String = first_line.chars().take(MAX_DESCRIPTION_LEN).collect();
93 format!("{truncated}...")
94 } else {
95 first_line.to_string()
96 }
97 });
98
99 best.insert(name, (version, description, prompt_body.to_string()));
100 }
101
102 let mut commands: Vec<(String, String, String)> = best
103 .into_iter()
104 .map(|(name, (_, desc, content))| (name, desc, content))
105 .collect();
106
107 commands.sort_by(|a, b| a.0.cmp(&b.0));
109
110 commands
111}
112
113#[allow(clippy::string_slice)] fn parse_versioned_name(stem: &str) -> (String, u32) {
121 if let Some(dot_pos) = stem.rfind('.')
122 && let Some(suffix) = stem.get(dot_pos + 1..)
123 && let Some(num_str) = suffix.strip_prefix('v')
124 && let Ok(version) = num_str.parse::<u32>()
125 {
126 return (stem[..dot_pos].to_string(), version);
128 }
129 (stem.to_string(), 0)
130}
131
132fn is_valid_command_name(name: &str) -> bool {
136 !name.is_empty()
137 && name
138 .chars()
139 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
140}
141
142#[allow(clippy::string_slice)] fn extract_front_matter(content: &str) -> (Option<String>, &str) {
155 if !content.starts_with("---") {
157 return (None, content);
158 }
159
160 let after_first = &content[3..];
162 let closing = after_first
163 .match_indices("---")
164 .find(|(pos, _)| {
165 *pos == 0 || after_first.as_bytes().get(pos.wrapping_sub(1)) == Some(&b'\n')
166 })
167 .map(|(pos, _)| pos);
168
169 match closing {
170 Some(pos) => {
171 let front_matter = after_first[..pos].trim();
172 let body = after_first[pos + 3..].trim();
173
174 let description = front_matter.lines().find_map(|line| {
176 let line = line.trim();
177 if let Some(value) = line.strip_prefix("description:") {
178 let value = value.trim();
179 let value = value
181 .strip_prefix('"')
182 .and_then(|v| v.strip_suffix('"'))
183 .or_else(|| value.strip_prefix('\'').and_then(|v| v.strip_suffix('\'')))
184 .unwrap_or(value);
185 if !value.is_empty() {
186 Some(value.to_string())
187 } else {
188 None
189 }
190 } else {
191 None
192 }
193 });
194
195 if body.is_empty() {
196 (description, content)
197 } else {
198 (description, body)
199 }
200 }
201 None => (None, content),
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_parse_versioned_name() {
211 assert_eq!(parse_versioned_name("review.v1"), ("review".into(), 1));
212 assert_eq!(parse_versioned_name("claw.v2"), ("claw".into(), 2));
213 assert_eq!(
214 parse_versioned_name("my-command.v10"),
215 ("my-command".into(), 10)
216 );
217 assert_eq!(parse_versioned_name("plain"), ("plain".into(), 0));
218 assert_eq!(
219 parse_versioned_name("dotted.name.v3"),
220 ("dotted.name".into(), 3)
221 );
222 }
223
224 #[test]
225 fn test_is_valid_command_name() {
226 assert!(is_valid_command_name("hello"));
227 assert!(is_valid_command_name("hello-world"));
228 assert!(is_valid_command_name("hello_world"));
229 assert!(is_valid_command_name("Hello123"));
230 assert!(!is_valid_command_name(""));
231 assert!(!is_valid_command_name("hello world"));
232 assert!(!is_valid_command_name("hello.world"));
233 assert!(!is_valid_command_name("hello/world"));
234 }
235
236 #[test]
237 fn test_extract_front_matter_with_description() {
238 let content = "---\ndescription: Run a security audit\n---\n\nPerform a comprehensive security audit.";
239 let (desc, body) = extract_front_matter(content);
240 assert_eq!(desc, Some("Run a security audit".into()));
241 assert_eq!(body, "Perform a comprehensive security audit.");
242 }
243
244 #[test]
245 fn test_extract_front_matter_with_quoted_description() {
246 let content = "---\ndescription: \"Check health status\"\n---\n\nCheck the health.";
247 let (desc, body) = extract_front_matter(content);
248 assert_eq!(desc, Some("Check health status".into()));
249 assert_eq!(body, "Check the health.");
250 }
251
252 #[test]
253 fn test_extract_front_matter_no_front_matter() {
254 let content = "Just a plain prompt.";
255 let (desc, body) = extract_front_matter(content);
256 assert_eq!(desc, None);
257 assert_eq!(body, content);
258 }
259
260 #[test]
261 fn test_load_predefined_commands_returns_known_commands() {
262 let commands = load_predefined_commands();
263 let names: Vec<&str> = commands.iter().map(|(n, _, _)| n.as_str()).collect();
264 assert!(names.contains(&"claw"), "Expected 'claw' in {names:?}");
265 assert!(names.contains(&"review"), "Expected 'review' in {names:?}");
266 }
267
268 #[test]
269 fn test_predefined_commands_have_descriptions() {
270 let commands = load_predefined_commands();
271 for (name, desc, _) in &commands {
272 assert!(
273 !desc.is_empty(),
274 "Command '{name}' has an empty description"
275 );
276 }
277 }
278
279 #[test]
280 fn test_predefined_commands_have_content() {
281 let commands = load_predefined_commands();
282 for (name, _, content) in &commands {
283 assert!(
284 !content.is_empty(),
285 "Command '{name}' has empty prompt content"
286 );
287 }
288 }
289}