1use crate::agents::{AgentsConfigFile, ConfigInitResult};
8use crate::config::{unified_config_path, UnifiedConfig, UnifiedConfigInitResult};
9use crate::logger::Colors;
10use crate::templates::{get_template, list_templates, ALL_TEMPLATES};
11use std::fs;
12use std::io::IsTerminal;
13use std::path::Path;
14
15const MIN_SIMILARITY_PERCENT: u32 = 40;
17
18pub fn handle_init_global(colors: Colors) -> anyhow::Result<bool> {
32 let global_path = unified_config_path()
33 .ok_or_else(|| anyhow::anyhow!("Cannot determine config directory (no home directory)"))?;
34
35 match UnifiedConfig::ensure_config_exists() {
36 Ok(UnifiedConfigInitResult::Created) => {
37 println!(
38 "{}Created unified config: {}{}{}\n",
39 colors.green(),
40 colors.bold(),
41 global_path.display(),
42 colors.reset()
43 );
44 println!("This is the primary configuration file for Ralph.");
45 println!();
46 println!("Features:");
47 println!(" - General settings (verbosity, iterations, etc.)");
48 println!(" - CCS aliases for Claude Code Switch integration");
49 println!(" - Custom agent definitions");
50 println!(" - Agent chain configuration with fallbacks");
51 println!();
52 println!("Environment variables (RALPH_*) override these settings.");
53 println!();
54 println!("Next steps:");
55 println!(" 1. Create a PROMPT.md for your task:");
56 println!(" ralph --init <work-guide>");
57 println!(" ralph --list-templates # Show all Work Guides");
58 println!(" 2. Or run ralph directly with default settings:");
59 println!(" ralph \"your commit message\"");
60 Ok(true)
61 }
62 Ok(UnifiedConfigInitResult::AlreadyExists) => {
63 println!(
64 "{}Unified config already exists:{} {}",
65 colors.yellow(),
66 colors.reset(),
67 global_path.display()
68 );
69 println!("Edit the file to customize, or delete it to regenerate from defaults.");
70 println!();
71 println!("Next steps:");
72 println!(" 1. Create a PROMPT.md for your task:");
73 println!(" ralph --init <work-guide>");
74 println!(" ralph --list-templates # Show all Work Guides");
75 println!(" 2. Or run ralph directly with default settings:");
76 println!(" ralph \"your commit message\"");
77 Ok(true)
78 }
79 Err(e) => Err(anyhow::anyhow!(
80 "Failed to create config file {}: {}",
81 global_path.display(),
82 e
83 )),
84 }
85}
86
87pub fn handle_init_legacy(colors: Colors, agents_config_path: &Path) -> anyhow::Result<bool> {
91 match AgentsConfigFile::ensure_config_exists(agents_config_path) {
92 Ok(ConfigInitResult::Created) => {
93 println!(
94 "{}Created {}{}{}\n",
95 colors.green(),
96 colors.bold(),
97 agents_config_path.display(),
98 colors.reset()
99 );
100 println!("Edit the file to customize agent configurations, then run ralph again.");
101 println!("Or run ralph now to use the default settings.");
102 Ok(true)
103 }
104 Ok(ConfigInitResult::AlreadyExists) => {
105 println!(
106 "{}Config file already exists:{} {}",
107 colors.yellow(),
108 colors.reset(),
109 agents_config_path.display()
110 );
111 println!("Edit the file to customize, or delete it to regenerate from defaults.");
112 Ok(true)
113 }
114 Err(e) => Err(anyhow::anyhow!(
115 "Failed to create config file {}: {}",
116 agents_config_path.display(),
117 e
118 )),
119 }
120}
121
122pub fn handle_init_prompt(template_name: &str, colors: Colors) -> anyhow::Result<bool> {
138 let prompt_path = Path::new("PROMPT.md");
139
140 if prompt_path.exists() {
142 println!(
143 "{}PROMPT.md already exists:{} {}",
144 colors.yellow(),
145 colors.reset(),
146 prompt_path.display()
147 );
148 println!("Delete or backup the existing file to create a new one from a template.");
149 return Ok(true);
150 }
151
152 let Some(template) = get_template(template_name) else {
154 println!(
155 "{}Unknown template: '{}'{}",
156 colors.red(),
157 template_name,
158 colors.reset()
159 );
160 println!();
161 println!("Available templates:");
162 for (name, description) in list_templates() {
163 println!(
164 " {}{}{} {}",
165 colors.cyan(),
166 name,
167 colors.reset(),
168 description
169 );
170 }
171 println!();
172 println!("Usage: ralph --init-prompt <template>");
173 println!(" ralph --list-templates");
174 return Ok(true);
175 };
176
177 let content = template.content();
179 fs::write(prompt_path, content)?;
180
181 println!(
182 "{}Created PROMPT.md from template: {}{}{}",
183 colors.green(),
184 colors.bold(),
185 template_name,
186 colors.reset()
187 );
188 println!();
189 println!(
190 "Template: {}{}{} {}",
191 colors.cyan(),
192 template.name(),
193 colors.reset(),
194 template.description()
195 );
196 println!();
197 println!("Next steps:");
198 println!(" 1. Edit PROMPT.md with your task details");
199 println!(" 2. Run: ralph \"your commit message\"");
200 println!();
201 println!("Tip: Use --list-templates to see all available templates.");
202
203 Ok(true)
204}
205
206fn print_template_category(category_name: &str, templates: &[(&str, &str)], colors: Colors) {
210 println!("{}{}:{}", colors.bold(), category_name, colors.reset());
211 for (name, description) in templates {
212 println!(
213 " {}{}{} {}",
214 colors.cyan(),
215 name,
216 colors.reset(),
217 description
218 );
219 }
220 println!();
221}
222
223pub fn handle_list_templates(colors: Colors) -> bool {
235 println!("PROMPT.md Work Guides (use: ralph --init <template>)");
236 println!();
237
238 print_template_category(
240 "Common Templates",
241 &[
242 ("quick", "Quick/small changes (typos, minor fixes)"),
243 ("bug-fix", "Bug fix with investigation guidance"),
244 ("feature-spec", "Comprehensive product specification"),
245 ("refactor", "Code refactoring with behavior preservation"),
246 ],
247 colors,
248 );
249
250 print_template_category(
252 "Testing & Documentation",
253 &[
254 ("test", "Test writing with edge case considerations"),
255 ("docs", "Documentation update with completeness checklist"),
256 ("code-review", "Structured code review for pull requests"),
257 ],
258 colors,
259 );
260
261 print_template_category(
263 "Specialized Development",
264 &[
265 ("cli-tool", "CLI tool with argument parsing and completion"),
266 ("web-api", "REST/HTTP API with error handling"),
267 (
268 "ui-component",
269 "UI component with accessibility and responsive design",
270 ),
271 ("onboarding", "Learn a new codebase efficiently"),
272 ],
273 colors,
274 );
275
276 print_template_category(
278 "Advanced & Infrastructure",
279 &[
280 (
281 "performance-optimization",
282 "Performance optimization with benchmarking",
283 ),
284 (
285 "security-audit",
286 "Security audit with OWASP Top 10 coverage",
287 ),
288 (
289 "api-integration",
290 "API integration with retry logic and resilience",
291 ),
292 (
293 "database-migration",
294 "Database migration with zero-downtime strategies",
295 ),
296 (
297 "dependency-update",
298 "Dependency update with breaking change handling",
299 ),
300 ("data-pipeline", "Data pipeline with ETL and monitoring"),
301 ],
302 colors,
303 );
304
305 print_template_category(
307 "Maintenance & Operations",
308 &[
309 (
310 "debug-triage",
311 "Systematic issue investigation and diagnosis",
312 ),
313 (
314 "tech-debt",
315 "Technical debt refactoring with prioritization",
316 ),
317 (
318 "release",
319 "Release preparation with versioning and changelog",
320 ),
321 ],
322 colors,
323 );
324
325 println!("Usage: ralph --init <template>");
326 println!(" ralph --init-prompt <template>");
327 println!();
328 println!("Example:");
329 println!(" ralph --init bug-fix # Create bug fix template");
330 println!(" ralph --init feature-spec # Create feature spec template");
331 println!(" ralph --init quick # Create quick change template");
332 println!();
333 println!("{}Tip:{}", colors.yellow(), colors.reset());
334 println!(" Use --init without a value to auto-detect what you need.");
335 println!(" Run ralph --help to understand the difference between Task Templates");
336 println!(" (for PROMPT.md) and System Prompts (backend AI configuration).");
337
338 true
339}
340
341pub fn handle_smart_init(template_arg: Option<&str>, colors: Colors) -> anyhow::Result<bool> {
359 let config_path = crate::config::unified_config_path()
360 .ok_or_else(|| anyhow::anyhow!("Cannot determine config directory (no home directory)"))?;
361 let prompt_path = Path::new("PROMPT.md");
362
363 let config_exists = config_path.exists();
364 let prompt_exists = prompt_path.exists();
365
366 if let Some(template_name) = template_arg {
368 if !template_name.is_empty() {
369 return handle_init_template_arg(template_name, colors);
370 }
371 }
373
374 handle_init_state_inference(
376 &config_path,
377 prompt_path,
378 config_exists,
379 prompt_exists,
380 colors,
381 )
382}
383
384fn levenshtein_distance(a: &str, b: &str) -> usize {
389 let a_chars: Vec<char> = a.chars().collect();
390 let b_chars: Vec<char> = b.chars().collect();
391 let b_len = b_chars.len();
392
393 let mut prev_row: Vec<usize> = (0..=b_len).collect();
395 let mut curr_row = vec![0; b_len + 1];
396
397 for (i, a_char) in a_chars.iter().enumerate() {
398 curr_row[0] = i + 1;
399
400 for (j, b_char) in b_chars.iter().enumerate() {
401 let cost = usize::from(a_char != b_char);
402 curr_row[j + 1] = std::cmp::min(
403 std::cmp::min(
404 curr_row[j] + 1, prev_row[j + 1] + 1, ),
407 prev_row[j] + cost, );
409 }
410
411 std::mem::swap(&mut prev_row, &mut curr_row);
412 }
413
414 prev_row[b_len]
415}
416
417fn similarity_percentage(a: &str, b: &str) -> u32 {
421 if a == b {
422 return 100;
423 }
424 if a.is_empty() || b.is_empty() {
425 return 0;
426 }
427
428 let max_len = a.len().max(b.len());
429 let distance = levenshtein_distance(a, b);
430
431 if max_len == 0 {
432 return 100;
433 }
434
435 let diff = max_len.saturating_sub(distance);
438 u32::try_from((100 * diff) / max_len).unwrap_or(0)
440}
441
442fn find_similar_templates(input: &str) -> Vec<(&'static str, u32)> {
446 let input_lower = input.to_lowercase();
447 let mut matches: Vec<(&'static str, u32)> = ALL_TEMPLATES
448 .iter()
449 .map(|t| {
450 let name = t.name();
451 let sim = similarity_percentage(&input_lower, &name.to_lowercase());
452 (name, sim)
453 })
454 .filter(|(_, sim)| *sim >= MIN_SIMILARITY_PERCENT)
455 .collect();
456
457 matches.sort_by(|a, b| b.1.cmp(&a.1));
459
460 matches.truncate(3);
462 matches
463}
464
465fn handle_init_template_arg(template_name: &str, colors: Colors) -> anyhow::Result<bool> {
467 if get_template(template_name).is_some() {
468 return handle_init_prompt(template_name, colors);
469 }
470
471 println!(
473 "{}Unknown Work Guide: '{}'{}",
474 colors.red(),
475 template_name,
476 colors.reset()
477 );
478 println!();
479
480 let similar = find_similar_templates(template_name);
482 if !similar.is_empty() {
483 println!("{}Did you mean?{}", colors.yellow(), colors.reset());
484 for (name, score) in similar {
485 println!(
486 " {}{}{} ({}% similar)",
487 colors.cyan(),
488 name,
489 colors.reset(),
490 score
491 );
492 }
493 println!();
494 }
495
496 println!("Available Work Guides:");
497 for (name, description) in list_templates() {
498 println!(
499 " {}{}{} {}{}{}",
500 colors.cyan(),
501 name,
502 colors.reset(),
503 colors.dim(),
504 description,
505 colors.reset()
506 );
507 }
508 println!();
509 println!("Usage: ralph --init=<work-guide>");
510 println!(" ralph --init # Smart init (infers intent)");
511 Ok(true)
512}
513
514fn handle_init_state_inference(
516 config_path: &std::path::Path,
517 prompt_path: &Path,
518 config_exists: bool,
519 prompt_exists: bool,
520 colors: Colors,
521) -> anyhow::Result<bool> {
522 match (config_exists, prompt_exists) {
523 (false, false) => handle_init_none_exist(config_path, colors),
524 (true, false) => Ok(handle_init_only_config_exists(config_path, colors)),
525 (false, true) => handle_init_only_prompt_exists(colors),
526 (true, true) => Ok(handle_init_both_exist(config_path, prompt_path, colors)),
527 }
528}
529
530fn handle_init_none_exist(_config_path: &std::path::Path, colors: Colors) -> anyhow::Result<bool> {
532 println!(
533 "{}No config found. Creating unified config...{}",
534 colors.dim(),
535 colors.reset()
536 );
537 println!();
538 handle_init_global(colors)?;
539 Ok(true)
540}
541
542fn handle_init_only_config_exists(config_path: &std::path::Path, colors: Colors) -> bool {
547 println!(
548 "{}Config found at:{} {}",
549 colors.green(),
550 colors.reset(),
551 config_path.display()
552 );
553 println!(
554 "{}PROMPT.md not found in current directory.{}",
555 colors.yellow(),
556 colors.reset()
557 );
558 println!();
559
560 if std::io::stdin().is_terminal() && std::io::stdout().is_terminal() {
562 if let Some(template_name) = prompt_for_template(colors) {
564 match handle_init_prompt(&template_name, colors) {
565 Ok(_) => return true,
566 Err(e) => {
567 println!(
568 "{}Failed to create PROMPT.md: {}{}",
569 colors.red(),
570 e,
571 colors.reset()
572 );
573 return true;
574 }
575 }
576 }
577 } else {
579 let default_content = create_minimal_prompt_md();
581 let prompt_path = Path::new("PROMPT.md");
582
583 match fs::write(prompt_path, default_content) {
584 Ok(()) => {
585 println!(
586 "{}Created minimal PROMPT.md{}",
587 colors.green(),
588 colors.reset()
589 );
590 println!();
591 println!("Next steps:");
592 println!(" 1. Edit PROMPT.md with your task details");
593 println!(" 2. Run: ralph \"your commit message\"");
594 println!();
595 println!("Tip: Use ralph --list-templates to see all available Work Guides.");
596 return true;
597 }
598 Err(e) => {
599 println!(
600 "{}Failed to create PROMPT.md: {}{}",
601 colors.red(),
602 e,
603 colors.reset()
604 );
605 return true;
606 }
607 }
608 }
609
610 println!("Create a PROMPT.md from a Work Guide to get started:");
612 println!();
613
614 for (name, description) in list_templates() {
615 println!(
616 " {}{}{} {}{}{}",
617 colors.cyan(),
618 name,
619 colors.reset(),
620 colors.dim(),
621 description,
622 colors.reset()
623 );
624 }
625
626 println!();
627 println!("Usage: ralph --init <work-guide>");
628 println!(" ralph --init-prompt <work-guide>");
629 println!();
630 println!("Example:");
631 println!(" ralph --init bug-fix");
632 println!(" ralph --init feature-spec");
633 true
634}
635
636fn prompt_for_template(colors: Colors) -> Option<String> {
641 use std::io::{self, Write};
642
643 println!("PROMPT.md contains your task specification for the AI agents.");
644 print!("Would you like to create one from a template? [Y/n]: ");
645 if io::stdout().flush().is_err() {
646 return None;
647 }
648
649 let mut input = String::new();
650 match io::stdin().read_line(&mut input) {
651 Ok(0) | Err(_) => return None,
652 Ok(_) => {}
653 }
654
655 let response = input.trim().to_lowercase();
656 if response == "n" || response == "no" || response == "skip" {
657 return None;
658 }
659
660 println!();
662 println!("Available templates:");
663
664 let templates: Vec<(&str, &str)> = list_templates();
665 for (i, (name, description)) in templates.iter().enumerate() {
666 println!(
667 " {}{}{} {}{}{}",
668 colors.cyan(),
669 name,
670 colors.reset(),
671 colors.dim(),
672 description,
673 colors.reset()
674 );
675 if (i + 1) % 5 == 0 {
676 println!(); }
678 }
679
680 println!();
681 println!("Common choices:");
682 println!(
683 " {}quick{} - Quick/small changes (typos, minor fixes)",
684 colors.cyan(),
685 colors.reset()
686 );
687 println!(
688 " {}bug-fix{} - Bug fix with investigation guidance",
689 colors.cyan(),
690 colors.reset()
691 );
692 println!(
693 " {}feature-spec{} - Product specification",
694 colors.cyan(),
695 colors.reset()
696 );
697 println!();
698 print!("Enter template name (or press Enter to use 'quick'): ");
699 if io::stdout().flush().is_err() {
700 return None;
701 }
702
703 let mut template_input = String::new();
704 match io::stdin().read_line(&mut template_input) {
705 Ok(0) | Err(_) => return None,
706 Ok(_) => {}
707 }
708
709 let template_name = template_input.trim();
710 if template_name.is_empty() {
711 return Some("quick".to_string());
713 }
714
715 if get_template(template_name).is_some() {
717 Some(template_name.to_string())
718 } else {
719 println!(
720 "{}Unknown template: '{}'{}",
721 colors.red(),
722 template_name,
723 colors.reset()
724 );
725 println!("Run 'ralph --list-templates' to see all available templates.");
726 None
727 }
728}
729
730fn create_minimal_prompt_md() -> String {
732 "# Task Description
733
734Describe what you want the AI agents to implement.
735
736## Example
737
738\"Fix the typo in the README file\"
739
740## Context
741
742Provide any relevant context about the task:
743- What problem are you trying to solve?
744- What are the acceptance criteria?
745- Are there any specific requirements or constraints?
746
747## Notes
748
749- This is a minimal PROMPT.md created by `ralph --init`
750- You can edit this file directly or use `ralph --init <work-guide>` to start from a Work Guide
751- Run `ralph --list-templates` to see all available Work Guides
752"
753 .to_string()
754}
755
756fn handle_init_only_prompt_exists(colors: Colors) -> anyhow::Result<bool> {
758 println!(
759 "{}PROMPT.md found in current directory.{}",
760 colors.green(),
761 colors.reset()
762 );
763 println!(
764 "{}No config found. Creating unified config...{}",
765 colors.dim(),
766 colors.reset()
767 );
768 println!();
769 handle_init_global(colors)?;
770 Ok(true)
771}
772
773fn handle_init_both_exist(
775 config_path: &std::path::Path,
776 prompt_path: &Path,
777 colors: Colors,
778) -> bool {
779 println!("{}Setup complete!{}", colors.green(), colors.reset());
780 println!();
781 println!(
782 " Config: {}{}{}",
783 colors.dim(),
784 config_path.display(),
785 colors.reset()
786 );
787 println!(
788 " PROMPT: {}{}{}",
789 colors.dim(),
790 prompt_path.display(),
791 colors.reset()
792 );
793 println!();
794 println!("You're ready to run Ralph:");
795 println!(" ralph \"your commit message\"");
796 println!();
797 println!("Other commands:");
798 println!(" ralph --list-templates # Show all Work Guides");
799 println!(" ralph --init=<template> # Create new PROMPT.md from Work Guide");
800 true
801}
802
803#[cfg(test)]
804mod tests {
805 use super::*;
806
807 #[test]
808 fn test_handle_smart_init_with_valid_template() {
809 let colors = Colors::new();
811 let result = handle_smart_init(Some("bug-fix"), colors);
812
813 assert!(result.is_ok());
816 }
817
818 #[test]
819 fn test_handle_smart_init_with_invalid_template() {
820 let colors = Colors::new();
822 let result = handle_smart_init(Some("nonexistent-template"), colors);
823
824 assert!(result.is_ok());
826 }
827
828 #[test]
829 fn test_handle_smart_init_no_arg() {
830 let colors = Colors::new();
832 let result = handle_smart_init(None, colors);
833
834 assert!(result.is_ok());
836 }
837
838 #[test]
839 fn test_template_name_validation() {
840 assert!(get_template("bug-fix").is_some());
842 assert!(get_template("feature-spec").is_some());
843 assert!(get_template("refactor").is_some());
844 assert!(get_template("test").is_some());
845 assert!(get_template("docs").is_some());
846 assert!(get_template("quick").is_some());
847
848 assert!(get_template("invalid").is_none());
850 assert!(get_template("").is_none());
851 }
852
853 #[test]
854 fn test_levenshtein_distance() {
855 assert_eq!(levenshtein_distance("test", "test"), 0);
857
858 assert_eq!(levenshtein_distance("test", "tast"), 1);
860 assert_eq!(levenshtein_distance("test", "tests"), 1);
861 assert_eq!(levenshtein_distance("test", "est"), 1);
862
863 assert_eq!(levenshtein_distance("test", "taste"), 2);
865 assert_eq!(levenshtein_distance("test", "best"), 1);
866
867 assert_eq!(levenshtein_distance("abc", "xyz"), 3);
869 }
870
871 #[test]
872 fn test_similarity() {
873 assert_eq!(similarity_percentage("test", "test"), 100);
875
876 assert!(similarity_percentage("bug-fix", "bugfix") > 80);
878 assert!(similarity_percentage("feature-spec", "feature") > 50);
879
880 assert!(similarity_percentage("test", "xyz") < 50);
882
883 assert_eq!(similarity_percentage("", ""), 100);
885 assert_eq!(similarity_percentage("test", ""), 0);
886 assert_eq!(similarity_percentage("", "test"), 0);
887 }
888
889 #[test]
890 fn test_find_similar_templates() {
891 let similar = find_similar_templates("bugfix");
893 assert!(!similar.is_empty());
894 assert!(similar.iter().any(|(name, _)| *name == "bug-fix"));
895
896 let similar = find_similar_templates("feature");
898 assert!(!similar.is_empty());
899 assert!(similar.iter().any(|(name, _)| name.contains("feature")));
900
901 let similar = find_similar_templates("xyzabc");
903 assert!(similar.is_empty() || similar.iter().all(|(_, sim)| *sim < 50));
905 }
906}