ralph_workflow/cli/handlers/
template_mgmt.rs1use 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 let templates = get_all_templates();
134
135 println!("{}Available Templates:{}", colors.bold(), colors.reset());
136 println!();
137
138 for (name, (_, description)) in {
139 let mut items: Vec<_> = templates.iter().collect();
140 items.sort_by(|a, b| a.0.cmp(b.0));
141 items
142 } {
143 println!(
144 " {}{}{} {}{}{}",
145 colors.cyan(),
146 name,
147 colors.reset(),
148 colors.dim(),
149 description,
150 colors.reset()
151 );
152 }
153
154 println!();
155 println!("Total: {} templates", templates.len());
156}
157
158pub fn handle_template_show(name: &str, colors: Colors) -> anyhow::Result<()> {
160 let templates = get_all_templates();
161
162 let (content, description) = templates
163 .get(name)
164 .ok_or_else(|| anyhow::anyhow!("Template '{name}' not found"))?;
165
166 println!(
167 "{}Template: {}{}{}{}",
168 colors.bold(),
169 colors.cyan(),
170 name,
171 colors.reset(),
172 colors.reset()
173 );
174 println!(
175 "{}Description: {}{}{}",
176 colors.dim(),
177 description,
178 colors.reset(),
179 colors.reset()
180 );
181 println!();
182
183 let metadata = extract_metadata(content);
185 if let Some(version) = metadata.version {
186 println!(
187 "{}Version: {}{}{}",
188 colors.dim(),
189 version,
190 colors.reset(),
191 colors.reset()
192 );
193 }
194 if let Some(purpose) = metadata.purpose {
195 println!(
196 "{}Purpose: {}{}{}",
197 colors.dim(),
198 purpose,
199 colors.reset(),
200 colors.reset()
201 );
202 }
203
204 println!();
205 println!("{}Variables:{}", colors.bold(), colors.reset());
206
207 let variables = extract_variables(content);
208 if variables.is_empty() {
209 println!(" (none)");
210 } else {
211 for var in &variables {
212 if var.has_default {
213 println!(
214 " {}{}{} = {}{}{}",
215 colors.cyan(),
216 var.name,
217 colors.reset(),
218 colors.green(),
219 var.default_value.as_deref().unwrap_or(""),
220 colors.reset()
221 );
222 } else {
223 println!(" {}{}{}", colors.cyan(), var.name, colors.reset());
224 }
225 }
226 }
227
228 println!();
229 println!("{}Partials:{}", colors.bold(), colors.reset());
230
231 let partials = extract_partials(content);
232 if partials.is_empty() {
233 println!(" (none)");
234 } else {
235 for partial in &partials {
236 println!(" {}{}{}", colors.cyan(), partial, colors.reset());
237 }
238 }
239
240 println!();
241 println!("{}Content:{}", colors.bold(), colors.reset());
242 println!("{}", colors.dim());
243 for line in content.lines().take(50) {
244 println!("{line}");
245 }
246 if content.lines().count() > 50 {
247 println!("... ({} more lines)", content.lines().count() - 50);
248 }
249 println!("{}", colors.reset());
250
251 Ok(())
252}
253
254pub fn handle_template_variables(name: &str, colors: Colors) -> anyhow::Result<()> {
256 let templates = get_all_templates();
257
258 let (content, _) = templates
259 .get(name)
260 .ok_or_else(|| anyhow::anyhow!("Template '{name}' not found"))?;
261
262 let variables = extract_variables(content);
263
264 println!(
265 "{}Variables in '{}':{}",
266 colors.bold(),
267 name,
268 colors.reset()
269 );
270 println!();
271
272 if variables.is_empty() {
273 println!(" (no variables found)");
274 } else {
275 for var in &variables {
276 let default = if var.has_default {
277 format!(
278 " = {}{}{}",
279 colors.green(),
280 var.default_value.as_deref().unwrap_or(""),
281 colors.reset()
282 )
283 } else {
284 String::new()
285 };
286 println!(
287 " {}{}{}{} {}line {}{}",
288 colors.cyan(),
289 var.name,
290 colors.reset(),
291 default,
292 colors.dim(),
293 var.line,
294 colors.reset()
295 );
296 }
297 }
298
299 println!();
300 println!("Total: {} variable(s)", variables.len());
301
302 Ok(())
303}
304
305pub fn handle_template_render(name: &str, colors: Colors) -> anyhow::Result<()> {
307 let templates = get_all_templates();
308
309 let (content, _) = templates
310 .get(name)
311 .ok_or_else(|| anyhow::anyhow!("Template '{name}' not found"))?;
312
313 let mut variables = HashMap::new();
315
316 variables.insert("PROMPT".to_string(), "Example prompt content".to_string());
319 variables.insert("PLAN".to_string(), "Example plan content".to_string());
320 variables.insert("DIFF".to_string(), "+ example line".to_string());
321
322 println!(
323 "{}Rendering template '{}'...{}",
324 colors.bold(),
325 name,
326 colors.reset()
327 );
328 println!();
329
330 let partials = get_shared_partials();
331 let template = Template::new(content);
332
333 match template.render_with_partials(
334 &variables
335 .iter()
336 .map(|(k, v)| (k.as_str(), v.clone()))
337 .collect(),
338 &partials,
339 ) {
340 Ok(rendered) => {
341 println!("{}", colors.dim());
342 println!("{rendered}");
343 println!("{}", colors.reset());
344 }
345 Err(e) => {
346 println!(
347 "{}Render error: {}{}{}",
348 colors.red(),
349 e,
350 colors.reset(),
351 colors.reset()
352 );
353 println!();
354 println!("{}Tip:{}", colors.yellow(), colors.reset());
355 println!(" Use --template-variables to see which variables are required.");
356 }
357 }
358
359 Ok(())
360}
361
362fn format_error(error: &crate::prompts::ValidationError) -> String {
364 match error {
365 crate::prompts::ValidationError::UnclosedConditional { line } => {
366 format!("unclosed conditional block on line {line}")
367 }
368 crate::prompts::ValidationError::UnclosedLoop { line } => {
369 format!("unclosed loop block on line {line}")
370 }
371 crate::prompts::ValidationError::InvalidConditional { line, syntax } => {
372 format!("invalid conditional syntax on line {line}: '{syntax}'")
373 }
374 crate::prompts::ValidationError::InvalidLoop { line, syntax } => {
375 format!("invalid loop syntax on line {line}: '{syntax}'")
376 }
377 crate::prompts::ValidationError::UnclosedComment { line } => {
378 format!("unclosed comment on line {line}")
379 }
380 crate::prompts::ValidationError::PartialNotFound { name } => {
381 format!("partial not found: '{name}'")
382 }
383 }
384}
385
386fn format_warning(warning: &crate::prompts::ValidationWarning) -> String {
388 match warning {
389 crate::prompts::ValidationWarning::VariableMayError { name } => {
390 format!("variable '{name}' may cause error if not provided")
391 }
392 }
393}
394
395fn handle_template_init(force: bool, colors: Colors) -> anyhow::Result<()> {
399 let templates_dir = TemplateRegistry::default_user_templates_dir()
400 .ok_or_else(|| anyhow::anyhow!("Cannot determine home directory for templates"))?;
401
402 let registry = TemplateRegistry::new(Some(templates_dir.clone()));
404
405 let source = registry.template_source("commit_message_xml");
407 let has_user = registry.has_user_template("commit_message_xml");
408
409 let _ = (source, has_user);
411
412 println!(
413 "{}Initializing user templates directory...{}",
414 colors.bold(),
415 colors.reset()
416 );
417 println!(
418 " Location: {}{}{}",
419 colors.cyan(),
420 templates_dir.display(),
421 colors.reset()
422 );
423 println!();
424
425 if templates_dir.exists() {
427 if force {
428 println!(
429 "{}Warning: {}Directory already exists. Overwriting...{}",
430 colors.yellow(),
431 colors.reset(),
432 colors.reset()
433 );
434 } else {
435 println!(
436 "{}Error: {}Directory already exists. Use --force to overwrite.{}",
437 colors.red(),
438 colors.reset(),
439 colors.reset()
440 );
441 println!();
442 println!("To reinitialize with defaults, run:");
443 println!(" ralph --template-init --force");
444 return Err(anyhow::anyhow!("Templates directory already exists"));
445 }
446 }
447
448 fs::create_dir_all(&templates_dir)?;
450
451 let shared_dir = templates_dir.join("shared");
452 fs::create_dir_all(&shared_dir)?;
453
454 let reviewer_dir = templates_dir.join("reviewer");
455 fs::create_dir_all(&reviewer_dir)?;
456
457 let templates = get_all_templates();
459 let mut copied = 0;
460 let mut skipped = 0;
461
462 for (name, (content, _)) in &templates {
463 let target_path = if name.starts_with("reviewer/") {
464 let parts: Vec<&str> = name.split('/').collect();
465 if parts.len() == 2 {
466 templates_dir
467 .join("reviewer")
468 .join(format!("{}.txt", parts[1]))
469 } else {
470 continue;
471 }
472 } else {
473 templates_dir.join(format!("{name}.txt"))
474 };
475
476 if target_path.exists() && !force {
478 skipped += 1;
479 continue;
480 }
481
482 fs::write(&target_path, content)?;
483 copied += 1;
484 }
485
486 let partials = get_shared_partials();
488 for (name, content) in &partials {
489 let target_path = templates_dir.join(format!("{name}.txt"));
490 if target_path.exists() && !force {
491 skipped += 1;
492 continue;
493 }
494 fs::write(&target_path, content)?;
495 copied += 1;
496 }
497
498 println!(
499 "{}Successfully initialized user templates!{}",
500 colors.green(),
501 colors.reset()
502 );
503 println!();
504 println!(" {copied} templates copied");
505 if skipped > 0 {
506 println!(" {skipped} templates skipped (already exists)");
507 }
508 println!();
509 println!("You can now edit templates in:");
510 println!(" {}", templates_dir.display());
511 println!();
512 println!("Changes to user templates will override the built-in templates.");
513
514 Ok(())
515}
516
517pub fn handle_template_commands(commands: &TemplateCommands, colors: Colors) -> anyhow::Result<()> {
519 if commands.init_templates_enabled() {
520 handle_template_init(commands.force, colors)?;
521 } else if commands.validate {
522 handle_template_validate(colors);
523 } else if let Some(ref name) = commands.show {
524 handle_template_show(name, colors)?;
525 } else if commands.list {
526 handle_template_list(colors);
527 } else if let Some(ref name) = commands.variables {
528 handle_template_variables(name, colors)?;
529 } else if let Some(ref name) = commands.render {
530 handle_template_render(name, colors)?;
531 }
532
533 Ok(())
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539
540 #[test]
541 fn test_get_all_templates_not_empty() {
542 let templates = get_all_templates();
543 assert!(!templates.is_empty());
544 assert!(templates.contains_key("developer_iteration"));
545 assert!(templates.contains_key("commit_message_xml"));
546 }
547
548 #[test]
549 fn test_template_show_valid() {
550 let colors = Colors::new();
551 let result = handle_template_show("developer_iteration", colors);
552 assert!(result.is_ok());
553 }
554
555 #[test]
556 fn test_template_show_invalid() {
557 let colors = Colors::new();
558 let result = handle_template_show("nonexistent", colors);
559 assert!(result.is_err());
560 }
561
562 #[test]
563 fn test_template_variables() {
564 let colors = Colors::new();
565 let result = handle_template_variables("developer_iteration", colors);
566 assert!(result.is_ok());
567 }
568}