Skip to main content

ralph/template/loader/
list.rs

1//! Purpose: Enumerate available templates and answer simple existence queries.
2//!
3//! Responsibilities:
4//! - List custom and built-in templates with custom override precedence.
5//! - Derive display descriptions for template listings.
6//! - Report whether a template name resolves to any source.
7//!
8//! Scope:
9//! - Listing/query behavior only; parsing and substitution live elsewhere.
10//!
11//! Usage:
12//! - Used by CLI surfaces that present template choices or validate a template
13//!   name before loading.
14//!
15//! Invariants/Assumptions:
16//! - Custom templates override built-ins with the same name.
17//! - Listing order remains stable via name sorting.
18//! - Only `.json` files under `.ralph/templates/` are considered custom
19//!   templates.
20
21use 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
31/// List all available templates (built-in + custom).
32///
33/// Custom templates override built-ins with the same name.
34pub 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
84/// Check if a template exists (either custom or built-in).
85pub 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}