1use std::collections::HashMap;
12use std::fs;
13
14use crate::cli::args::TemplateCommands;
15use crate::logger::Colors;
16use crate::prompts::partials::get_shared_partials;
17use crate::prompts::template_catalog;
18use crate::prompts::template_registry::TemplateRegistry;
19use crate::prompts::{
20 extract_metadata, extract_partials, extract_variables, validate_template, Template,
21};
22
23fn get_all_templates() -> HashMap<String, (String, String)> {
25 template_catalog::get_templates_map()
26}
27
28pub fn handle_template_validate(colors: Colors) {
30 println!("{}Validating templates...{}", colors.bold(), colors.reset());
31 println!();
32
33 let templates = get_all_templates();
34 let partials_set: std::collections::HashSet<String> =
35 get_shared_partials().keys().cloned().collect();
36
37 let mut total_errors = 0;
38 let mut total_warnings = 0;
39
40 for (name, (content, _)) in {
41 let mut items: Vec<_> = templates.iter().collect();
42 items.sort_by(|a, b| a.0.cmp(b.0));
43 items
44 } {
45 let result = validate_template(content, &partials_set);
46
47 if result.is_valid {
48 println!(
49 "{}✓{} {}{}{}",
50 colors.green(),
51 colors.reset(),
52 colors.cyan(),
53 name,
54 colors.reset()
55 );
56 } else {
57 println!(
58 "{}✗{} {}{}{}",
59 colors.red(),
60 colors.reset(),
61 colors.cyan(),
62 name,
63 colors.reset()
64 );
65 }
66
67 for error in &result.errors {
68 println!(
69 " {}error:{} {}",
70 colors.red(),
71 colors.reset(),
72 format_error(error)
73 );
74 total_errors += 1;
75 }
76
77 for warning in &result.warnings {
78 println!(
79 " {}warning:{} {}",
80 colors.yellow(),
81 colors.reset(),
82 format_warning(warning)
83 );
84 total_warnings += 1;
85 }
86
87 if !result.variables.is_empty() {
88 let var_names: Vec<&str> = result.variables.iter().map(|v| v.name.as_str()).collect();
89 println!(
90 " {}variables:{} {}",
91 colors.dim(),
92 colors.reset(),
93 var_names.join(", ")
94 );
95 }
96
97 if !result.partials.is_empty() {
98 println!(
99 " {}partials:{} {}",
100 colors.dim(),
101 colors.reset(),
102 result.partials.join(", ")
103 );
104 }
105 }
106
107 println!();
108 if total_errors == 0 {
109 println!(
110 "{}All templates validated successfully!{}",
111 colors.green(),
112 colors.reset()
113 );
114 if total_warnings > 0 {
115 println!("{total_warnings} warnings");
116 }
117 } else {
118 println!(
119 "{}Validation failed with {} error(s){}",
120 colors.red(),
121 total_errors,
122 colors.reset()
123 );
124 if total_warnings > 0 {
125 println!("{total_warnings} warnings");
126 }
127 std::process::exit(1);
128 }
129}
130
131pub fn handle_template_list(colors: Colors) {
133 handle_template_list_impl(colors, false);
134}
135
136pub fn handle_template_list_all(colors: Colors) {
138 handle_template_list_impl(colors, true);
139}
140
141fn handle_template_list_impl(colors: Colors, include_deprecated: bool) {
143 let all_templates = get_all_templates();
144 let filtered_templates: Vec<_> = all_templates
145 .iter()
146 .filter(|(name, _)| {
147 if include_deprecated {
148 return true;
149 }
150 if let Some(meta) = template_catalog::get_template_metadata(name) {
152 !meta.deprecated
153 } else {
154 true
155 }
156 })
157 .map(|(name, (content, desc))| {
158 (name, content, desc)
160 })
161 .collect();
162
163 let header = if include_deprecated {
164 "All Templates (including deprecated):"
165 } else {
166 "Active Templates:"
167 };
168
169 println!("{}{}{}", colors.bold(), header, colors.reset());
170 println!();
171
172 for (name, _, description) in {
173 let mut items: Vec<_> = filtered_templates.clone();
174 items.sort_by(|a, b| a.0.cmp(b.0));
175 items
176 } {
177 let is_deprecated = if let Some(meta) = template_catalog::get_template_metadata(name) {
179 meta.deprecated
180 } else {
181 false
182 };
183
184 let deprecated_marker = if is_deprecated {
185 format!("{} [DEPRECATED]{}", colors.yellow(), colors.reset())
186 } else {
187 String::new()
188 };
189
190 println!(
191 " {}{}{}{} {}{}{}",
192 colors.cyan(),
193 name,
194 colors.reset(),
195 deprecated_marker,
196 colors.dim(),
197 description,
198 colors.reset()
199 );
200 }
201
202 println!();
203 if include_deprecated {
204 let deprecated_count = filtered_templates
205 .iter()
206 .filter(|(name, _, _)| {
207 if let Some(meta) = template_catalog::get_template_metadata(name) {
208 meta.deprecated
209 } else {
210 false
211 }
212 })
213 .count();
214
215 println!(
216 "Total: {} templates ({} active, {} deprecated)",
217 filtered_templates.len(),
218 filtered_templates.len() - deprecated_count,
219 deprecated_count
220 );
221 println!();
222 println!("{}Tip:{}", colors.yellow(), colors.reset());
223 println!(" Edit templates in ~/.config/ralph/templates/");
224 println!(" Deprecated templates are kept for backward compatibility.");
225 println!(
226 " Use {}--list{} to show only active templates.",
227 colors.bold(),
228 colors.reset()
229 );
230 } else {
231 println!("Total: {} active templates", filtered_templates.len());
232 println!();
233 println!("{}Tip:{}", colors.yellow(), colors.reset());
234 println!(" Edit templates in ~/.config/ralph/templates/");
235 println!(
236 " Use {}--list-all{} to include deprecated templates",
237 colors.bold(),
238 colors.reset()
239 );
240 }
241}
242
243pub fn handle_template_show(name: &str, colors: Colors) -> anyhow::Result<()> {
245 let templates = get_all_templates();
246
247 let (content, description) = templates
248 .get(name)
249 .ok_or_else(|| anyhow::anyhow!("Template '{name}' not found"))?;
250
251 println!(
252 "{}Template: {}{}{}{}",
253 colors.bold(),
254 colors.cyan(),
255 name,
256 colors.reset(),
257 colors.reset()
258 );
259 println!(
260 "{}Description: {}{}{}",
261 colors.dim(),
262 description,
263 colors.reset(),
264 colors.reset()
265 );
266 println!();
267
268 let metadata = extract_metadata(content);
270 if let Some(version) = metadata.version {
271 println!(
272 "{}Version: {}{}{}",
273 colors.dim(),
274 version,
275 colors.reset(),
276 colors.reset()
277 );
278 }
279 if let Some(purpose) = metadata.purpose {
280 println!(
281 "{}Purpose: {}{}{}",
282 colors.dim(),
283 purpose,
284 colors.reset(),
285 colors.reset()
286 );
287 }
288
289 println!();
290 println!("{}Variables:{}", colors.bold(), colors.reset());
291
292 let variables = extract_variables(content);
293 if variables.is_empty() {
294 println!(" (none)");
295 } else {
296 for var in &variables {
297 if var.has_default {
298 println!(
299 " {}{}{} = {}{}{}",
300 colors.cyan(),
301 var.name,
302 colors.reset(),
303 colors.green(),
304 var.default_value.as_deref().unwrap_or(""),
305 colors.reset()
306 );
307 } else {
308 println!(" {}{}{}", colors.cyan(), var.name, colors.reset());
309 }
310 }
311 }
312
313 println!();
314 println!("{}Partials:{}", colors.bold(), colors.reset());
315
316 let partials = extract_partials(content);
317 if partials.is_empty() {
318 println!(" (none)");
319 } else {
320 for partial in &partials {
321 println!(" {}{}{}", colors.cyan(), partial, colors.reset());
322 }
323 }
324
325 println!();
326 println!("{}Content:{}", colors.bold(), colors.reset());
327 println!("{}", colors.dim());
328 for line in content.lines().take(50) {
329 println!("{line}");
330 }
331 if content.lines().count() > 50 {
332 println!("... ({} more lines)", content.lines().count() - 50);
333 }
334 println!("{}", colors.reset());
335
336 Ok(())
337}
338
339pub fn handle_template_variables(name: &str, colors: Colors) -> anyhow::Result<()> {
341 let templates = get_all_templates();
342
343 let (content, _) = templates
344 .get(name)
345 .ok_or_else(|| anyhow::anyhow!("Template '{name}' not found"))?;
346
347 let variables = extract_variables(content);
348
349 println!(
350 "{}Variables in '{}':{}",
351 colors.bold(),
352 name,
353 colors.reset()
354 );
355 println!();
356
357 if variables.is_empty() {
358 println!(" (no variables found)");
359 } else {
360 for var in &variables {
361 let default = if var.has_default {
362 format!(
363 " = {}{}{}",
364 colors.green(),
365 var.default_value.as_deref().unwrap_or(""),
366 colors.reset()
367 )
368 } else {
369 String::new()
370 };
371 println!(
372 " {}{}{}{} {}line {}{}",
373 colors.cyan(),
374 var.name,
375 colors.reset(),
376 default,
377 colors.dim(),
378 var.line,
379 colors.reset()
380 );
381 }
382 }
383
384 println!();
385 println!("Total: {} variable(s)", variables.len());
386
387 Ok(())
388}
389
390pub fn handle_template_render(name: &str, colors: Colors) -> anyhow::Result<()> {
392 let templates = get_all_templates();
393
394 let (content, _) = templates
395 .get(name)
396 .ok_or_else(|| anyhow::anyhow!("Template '{name}' not found"))?;
397
398 let mut variables = HashMap::new();
400
401 variables.insert("PROMPT".to_string(), "Example prompt content".to_string());
404 variables.insert("PLAN".to_string(), "Example plan content".to_string());
405 variables.insert("DIFF".to_string(), "+ example line".to_string());
406
407 println!(
408 "{}Rendering template '{}'...{}",
409 colors.bold(),
410 name,
411 colors.reset()
412 );
413 println!();
414
415 let partials = get_shared_partials();
416 let template = Template::new(content);
417
418 match template.render_with_partials(
419 &variables
420 .iter()
421 .map(|(k, v)| (k.as_str(), v.clone()))
422 .collect(),
423 &partials,
424 ) {
425 Ok(rendered) => {
426 println!("{}", colors.dim());
427 println!("{rendered}");
428 println!("{}", colors.reset());
429 }
430 Err(e) => {
431 println!(
432 "{}Render error: {}{}{}",
433 colors.red(),
434 e,
435 colors.reset(),
436 colors.reset()
437 );
438 println!();
439 println!("{}Tip:{}", colors.yellow(), colors.reset());
440 println!(" Use --template-variables to see which variables are required.");
441 }
442 }
443
444 Ok(())
445}
446
447fn format_error(error: &crate::prompts::ValidationError) -> String {
449 match error {
450 crate::prompts::ValidationError::UnclosedConditional { line } => {
451 format!("unclosed conditional block on line {line}")
452 }
453 crate::prompts::ValidationError::UnclosedLoop { line } => {
454 format!("unclosed loop block on line {line}")
455 }
456 crate::prompts::ValidationError::InvalidConditional { line, syntax } => {
457 format!("invalid conditional syntax on line {line}: '{syntax}'")
458 }
459 crate::prompts::ValidationError::InvalidLoop { line, syntax } => {
460 format!("invalid loop syntax on line {line}: '{syntax}'")
461 }
462 crate::prompts::ValidationError::UnclosedComment { line } => {
463 format!("unclosed comment on line {line}")
464 }
465 crate::prompts::ValidationError::PartialNotFound { name } => {
466 format!("partial not found: '{name}'")
467 }
468 }
469}
470
471fn format_warning(warning: &crate::prompts::ValidationWarning) -> String {
473 match warning {
474 crate::prompts::ValidationWarning::VariableMayError { name } => {
475 format!("variable '{name}' may cause error if not provided")
476 }
477 }
478}
479
480fn handle_template_init(force: bool, colors: Colors) -> anyhow::Result<()> {
484 let templates_dir = TemplateRegistry::default_user_templates_dir()
485 .ok_or_else(|| anyhow::anyhow!("Cannot determine home directory for templates"))?;
486
487 let registry = TemplateRegistry::new(Some(templates_dir.clone()));
489
490 let source = registry.template_source("commit_message_xml");
492 let has_user = registry.has_user_template("commit_message_xml");
493
494 let _ = (source, has_user);
496
497 println!(
498 "{}Initializing user templates directory...{}",
499 colors.bold(),
500 colors.reset()
501 );
502 println!(
503 " Location: {}{}{}",
504 colors.cyan(),
505 templates_dir.display(),
506 colors.reset()
507 );
508 println!();
509
510 if templates_dir.exists() {
512 if force {
513 println!(
514 "{}Warning: {}Directory already exists. Overwriting...{}",
515 colors.yellow(),
516 colors.reset(),
517 colors.reset()
518 );
519 } else {
520 println!(
521 "{}Error: {}Directory already exists. Use --force to overwrite.{}",
522 colors.red(),
523 colors.reset(),
524 colors.reset()
525 );
526 println!();
527 println!("To reinitialize with defaults, run:");
528 println!(" ralph --template-init --force");
529 return Err(anyhow::anyhow!("Templates directory already exists"));
530 }
531 }
532
533 fs::create_dir_all(&templates_dir)?;
535
536 let shared_dir = templates_dir.join("shared");
537 fs::create_dir_all(&shared_dir)?;
538
539 let reviewer_dir = templates_dir.join("reviewer");
540 fs::create_dir_all(&reviewer_dir)?;
541
542 let templates = get_all_templates();
544 let mut copied = 0;
545 let mut skipped = 0;
546
547 for (name, (content, _)) in &templates {
548 let target_path = if name.starts_with("reviewer/") {
549 let parts: Vec<&str> = name.split('/').collect();
550 if parts.len() == 2 {
551 templates_dir
552 .join("reviewer")
553 .join(format!("{}.txt", parts[1]))
554 } else {
555 continue;
556 }
557 } else {
558 templates_dir.join(format!("{name}.txt"))
559 };
560
561 if target_path.exists() && !force {
563 skipped += 1;
564 continue;
565 }
566
567 fs::write(&target_path, content)?;
568 copied += 1;
569 }
570
571 let partials = get_shared_partials();
573 for (name, content) in &partials {
574 let target_path = templates_dir.join(format!("{name}.txt"));
575 if target_path.exists() && !force {
576 skipped += 1;
577 continue;
578 }
579 fs::write(&target_path, content)?;
580 copied += 1;
581 }
582
583 println!(
584 "{}Successfully initialized user templates!{}",
585 colors.green(),
586 colors.reset()
587 );
588 println!();
589 println!(" {copied} templates copied");
590 if skipped > 0 {
591 println!(" {skipped} templates skipped (already exists)");
592 }
593 println!();
594 println!("You can now edit templates in:");
595 println!(" {}", templates_dir.display());
596 println!();
597 println!("Changes to user templates will override the built-in templates.");
598
599 Ok(())
600}
601
602pub fn handle_template_commands(commands: &TemplateCommands, colors: Colors) -> anyhow::Result<()> {
604 if commands.init_templates_enabled() {
605 handle_template_init(commands.force, colors)?;
606 } else if commands.validate {
607 handle_template_validate(colors);
608 } else if let Some(ref name) = commands.show {
609 handle_template_show(name, colors)?;
610 } else if commands.list {
611 handle_template_list(colors);
612 } else if commands.list_all {
613 handle_template_list_all(colors);
614 } else if let Some(ref name) = commands.variables {
615 handle_template_variables(name, colors)?;
616 } else if let Some(ref name) = commands.render {
617 handle_template_render(name, colors)?;
618 }
619
620 Ok(())
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626
627 #[test]
628 fn test_get_all_templates_not_empty() {
629 let templates = get_all_templates();
630 assert!(!templates.is_empty());
631 assert!(templates.contains_key("developer_iteration_xml"));
632 assert!(templates.contains_key("commit_message_xml"));
633 }
634
635 #[test]
636 fn test_template_show_valid() {
637 let colors = Colors::new();
638 let result = handle_template_show("developer_iteration_xml", colors);
639 assert!(result.is_ok());
640 }
641
642 #[test]
643 fn test_template_show_invalid() {
644 let colors = Colors::new();
645 let result = handle_template_show("nonexistent", colors);
646 assert!(result.is_err());
647 }
648
649 #[test]
650 fn test_template_variables() {
651 let colors = Colors::new();
652 let result = handle_template_variables("developer_iteration_xml", colors);
653 assert!(result.is_ok());
654 }
655}