1use std::{
2 path::{Path, PathBuf},
3 sync::LazyLock,
4};
5
6use parking_lot::Mutex;
7use rust_embed::RustEmbed;
8use tera::{Context, Tera};
9
10use crate::error::{CommitGenError, Result};
11
12#[derive(RustEmbed)]
14#[folder = "prompts/"]
15struct Prompts;
16
17static TERA: LazyLock<Mutex<Tera>> = LazyLock::new(|| {
20 if let Err(e) = ensure_prompts_dir() {
22 eprintln!("Warning: Failed to initialize prompts directory: {e}");
23 }
24
25 let mut tera = Tera::default();
26
27 if let Some(prompts_dir) = get_user_prompts_dir() {
29 if let Err(e) =
30 register_directory_templates(&mut tera, &prompts_dir.join("analysis"), "analysis")
31 {
32 eprintln!("Warning: {e}");
33 }
34 if let Err(e) =
35 register_directory_templates(&mut tera, &prompts_dir.join("summary"), "summary")
36 {
37 eprintln!("Warning: {e}");
38 }
39 }
40
41 for file in Prompts::iter() {
43 if tera.get_template_names().any(|name| name == file.as_ref()) {
44 continue;
45 }
46
47 if let Some(embedded_file) = Prompts::get(file.as_ref()) {
48 match std::str::from_utf8(embedded_file.data.as_ref()) {
49 Ok(content) => {
50 if let Err(e) = tera.add_raw_template(file.as_ref(), content) {
51 eprintln!(
52 "Warning: Failed to register embedded template {}: {}",
53 file.as_ref(),
54 e
55 );
56 }
57 },
58 Err(e) => {
59 eprintln!("Warning: Embedded template {} is not valid UTF-8: {}", file.as_ref(), e);
60 },
61 }
62 }
63 }
64
65 tera.autoescape_on(vec![]);
67
68 Mutex::new(tera)
69});
70
71fn get_user_prompts_dir() -> Option<PathBuf> {
73 std::env::var("HOME")
74 .or_else(|_| std::env::var("USERPROFILE"))
75 .ok()
76 .map(|home| PathBuf::from(home).join(".llm-git").join("prompts"))
77}
78
79pub fn ensure_prompts_dir() -> Result<()> {
81 let Some(user_prompts_dir) = get_user_prompts_dir() else {
82 return Ok(());
85 };
86
87 let user_llm_git_dir = user_prompts_dir
89 .parent()
90 .ok_or_else(|| CommitGenError::Other("Invalid prompts directory path".to_string()))?;
91
92 if !user_llm_git_dir.exists() {
94 std::fs::create_dir_all(user_llm_git_dir).map_err(|e| {
95 CommitGenError::Other(format!(
96 "Failed to create directory {}: {}",
97 user_llm_git_dir.display(),
98 e
99 ))
100 })?;
101 }
102
103 if !user_prompts_dir.exists() {
105 std::fs::create_dir_all(&user_prompts_dir).map_err(|e| {
106 CommitGenError::Other(format!(
107 "Failed to create directory {}: {}",
108 user_prompts_dir.display(),
109 e
110 ))
111 })?;
112 }
113
114 for file in Prompts::iter() {
116 let file_path = user_prompts_dir.join(file.as_ref());
117
118 if let Some(parent) = file_path.parent() {
120 std::fs::create_dir_all(parent).map_err(|e| {
121 CommitGenError::Other(format!("Failed to create directory {}: {}", parent.display(), e))
122 })?;
123 }
124
125 if let Some(embedded_file) = Prompts::get(file.as_ref()) {
126 let embedded_content = embedded_file.data;
127
128 let should_write = if file_path.exists() {
130 match std::fs::read(&file_path) {
131 Ok(existing_content) => existing_content != embedded_content.as_ref(),
132 Err(_) => true, }
134 } else {
135 true };
137
138 if should_write {
139 std::fs::write(&file_path, embedded_content.as_ref()).map_err(|e| {
140 CommitGenError::Other(format!("Failed to write file {}: {}", file_path.display(), e))
141 })?;
142 }
143 }
144 }
145
146 Ok(())
147}
148
149fn register_directory_templates(tera: &mut Tera, directory: &Path, category: &str) -> Result<()> {
150 if !directory.exists() {
151 return Ok(());
152 }
153
154 for entry in std::fs::read_dir(directory).map_err(|e| {
155 CommitGenError::Other(format!(
156 "Failed to read {} templates directory {}: {}",
157 category,
158 directory.display(),
159 e
160 ))
161 })? {
162 let entry = match entry {
163 Ok(entry) => entry,
164 Err(e) => {
165 eprintln!(
166 "Warning: Failed to iterate template entry in {}: {}",
167 directory.display(),
168 e
169 );
170 continue;
171 },
172 };
173
174 let path = entry.path();
175 if path.extension().and_then(|s| s.to_str()) != Some("md") {
176 continue;
177 }
178
179 let template_name = format!(
180 "{}/{}",
181 category,
182 path
183 .file_name()
184 .and_then(|s| s.to_str())
185 .unwrap_or_default()
186 );
187
188 if let Err(e) = tera.add_template_file(&path, Some(&template_name)) {
191 eprintln!("Warning: Failed to load template file {}: {}", path.display(), e);
192 }
193 }
194
195 Ok(())
196}
197
198fn load_template_file(category: &str, variant: &str) -> Result<String> {
200 if let Some(prompts_dir) = get_user_prompts_dir() {
202 let template_path = prompts_dir.join(category).join(format!("{variant}.md"));
203 if template_path.exists() {
204 return std::fs::read_to_string(&template_path).map_err(|e| {
205 CommitGenError::Other(format!(
206 "Failed to read template file {}: {}",
207 template_path.display(),
208 e
209 ))
210 });
211 }
212 }
213
214 let embedded_key = format!("{category}/{variant}.md");
216 if let Some(bytes) = Prompts::get(&embedded_key) {
217 return std::str::from_utf8(bytes.data.as_ref())
218 .map(|s| s.to_string())
219 .map_err(|e| {
220 CommitGenError::Other(format!(
221 "Embedded template {embedded_key} is not valid UTF-8: {e}"
222 ))
223 });
224 }
225
226 Err(CommitGenError::Other(format!(
227 "Template variant '{variant}' in category '{category}' not found as user override or \
228 embedded default"
229 )))
230}
231
232pub fn render_analysis_prompt(
234 variant: &str,
235 stat: &str,
236 diff: &str,
237 scope_candidates: &str,
238) -> Result<String> {
239 let template_content = load_template_file("analysis", variant)?;
241
242 let mut context = Context::new();
244 context.insert("stat", stat);
245 context.insert("diff", diff);
246 context.insert("scope_candidates", scope_candidates);
247
248 let mut tera = TERA.lock();
250
251 tera.render_str(&template_content, &context).map_err(|e| {
252 CommitGenError::Other(format!("Failed to render analysis prompt template '{variant}': {e}"))
253 })
254}
255
256pub fn render_summary_prompt(
258 variant: &str,
259 commit_type: &str,
260 scope: &str,
261 chars: &str,
262 details: &str,
263 stat: &str,
264 user_context: Option<&str>,
265) -> Result<String> {
266 let template_content = load_template_file("summary", variant)?;
268
269 let mut context = Context::new();
271 context.insert("commit_type", commit_type);
272 context.insert("scope", scope);
273 context.insert("chars", chars);
274 context.insert("details", details);
275 context.insert("stat", stat);
276 if let Some(ctx) = user_context {
277 context.insert("user_context", ctx);
278 }
279
280 let mut tera = TERA.lock();
282 tera.render_str(&template_content, &context).map_err(|e| {
283 CommitGenError::Other(format!("Failed to render summary prompt template '{variant}': {e}"))
284 })
285}