ralph/template/loader/
list.rs1use std::collections::HashSet;
22use std::path::Path;
23
24use crate::contracts::Task;
25use crate::template::builtin::{
26 get_builtin_template, get_template_description, list_builtin_templates,
27};
28
29use super::types::{TemplateInfo, TemplateSource};
30
31pub fn list_templates(project_root: &Path) -> Vec<TemplateInfo> {
35 let mut templates = Vec::new();
36 let mut seen_names = HashSet::new();
37
38 let custom_dir = project_root.join(".ralph/templates");
39 if let Ok(entries) = std::fs::read_dir(&custom_dir) {
40 for entry in entries.flatten() {
41 let path = entry.path();
42 if path.extension().is_some_and(|ext| ext == "json")
43 && let Some(name) = path.file_stem()
44 {
45 let name = name.to_string_lossy().to_string();
46 seen_names.insert(name.clone());
47
48 let description = if let Ok(content) = std::fs::read_to_string(&path) {
49 if let Ok(task) = serde_json::from_str::<Task>(&content) {
50 task.plan
51 .first()
52 .cloned()
53 .unwrap_or_else(|| "Custom template".to_string())
54 } else {
55 "Custom template".to_string()
56 }
57 } else {
58 "Custom template".to_string()
59 };
60
61 templates.push(TemplateInfo {
62 name,
63 source: TemplateSource::Custom(path),
64 description,
65 });
66 }
67 }
68 }
69
70 for name in list_builtin_templates() {
71 if !seen_names.contains(name) {
72 templates.push(TemplateInfo {
73 name: name.to_string(),
74 source: TemplateSource::Builtin(name.to_string()),
75 description: get_template_description(name).to_string(),
76 });
77 }
78 }
79
80 templates.sort_by(|a, b| a.name.cmp(&b.name));
81 templates
82}
83
84pub fn template_exists(name: &str, project_root: &Path) -> bool {
86 let custom_path = project_root
87 .join(".ralph/templates")
88 .join(format!("{}.json", name));
89 custom_path.exists() || get_builtin_template(name).is_some()
90}