ralph_workflow/cli/
init.rs

1//! Configuration initialization handlers.
2//!
3//! This module handles the `--init`, `--init-global`, legacy `--init-legacy` flags,
4//! and `--init-prompt` flag for creating default agent configuration files
5//! and PROMPT.md from templates.
6
7use 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::fs::OpenOptions;
13use std::io::IsTerminal;
14use std::path::Path;
15
16/// Minimum similarity threshold for suggesting alternatives (0-100 percentage).
17const MIN_SIMILARITY_PERCENT: u32 = 40;
18
19/// Handle the `--init-global` flag.
20///
21/// Creates a unified config file at `~/.config/ralph-workflow.toml` if it doesn't exist.
22/// This is the recommended way to configure Ralph globally.
23///
24/// # Arguments
25///
26/// * `colors` - Terminal color configuration for output
27///
28/// # Returns
29///
30/// Returns `Ok(true)` if the flag was handled (program should exit after),
31/// or an error if config creation failed.
32pub fn handle_init_global(colors: Colors) -> anyhow::Result<bool> {
33    let global_path = unified_config_path()
34        .ok_or_else(|| anyhow::anyhow!("Cannot determine config directory (no home directory)"))?;
35
36    match UnifiedConfig::ensure_config_exists() {
37        Ok(UnifiedConfigInitResult::Created) => {
38            println!(
39                "{}Created unified config: {}{}{}\n",
40                colors.green(),
41                colors.bold(),
42                global_path.display(),
43                colors.reset()
44            );
45            println!("This is the primary configuration file for Ralph.");
46            println!();
47            println!("Features:");
48            println!("  - General settings (verbosity, iterations, etc.)");
49            println!("  - CCS aliases for Claude Code Switch integration");
50            println!("  - Custom agent definitions");
51            println!("  - Agent chain configuration with fallbacks");
52            println!();
53            println!("Environment variables (RALPH_*) override these settings.");
54            println!();
55            println!("Next steps:");
56            println!("  1. Create a PROMPT.md for your task:");
57            println!("       ralph --init <work-guide>");
58            println!("       ralph --list-work-guides  # Show all Work Guides");
59            println!("  2. Or run ralph directly with default settings:");
60            println!("       ralph \"your commit message\"");
61            Ok(true)
62        }
63        Ok(UnifiedConfigInitResult::AlreadyExists) => {
64            println!(
65                "{}Unified config already exists:{} {}",
66                colors.yellow(),
67                colors.reset(),
68                global_path.display()
69            );
70            println!("Edit the file to customize, or delete it to regenerate from defaults.");
71            println!();
72            println!("Next steps:");
73            println!("  1. Create a PROMPT.md for your task:");
74            println!("       ralph --init <work-guide>");
75            println!("       ralph --list-work-guides  # Show all Work Guides");
76            println!("  2. Or run ralph directly with default settings:");
77            println!("       ralph \"your commit message\"");
78            Ok(true)
79        }
80        Err(e) => Err(anyhow::anyhow!(
81            "Failed to create config file {}: {}",
82            global_path.display(),
83            e
84        )),
85    }
86}
87
88/// Handle the legacy `--init-legacy` flag.
89///
90/// Creates a local agents.toml file at the specified path if it doesn't exist.
91pub fn handle_init_legacy(colors: Colors, agents_config_path: &Path) -> anyhow::Result<bool> {
92    match AgentsConfigFile::ensure_config_exists(agents_config_path) {
93        Ok(ConfigInitResult::Created) => {
94            println!(
95                "{}Created {}{}{}\n",
96                colors.green(),
97                colors.bold(),
98                agents_config_path.display(),
99                colors.reset()
100            );
101            println!("Edit the file to customize agent configurations, then run ralph again.");
102            println!("Or run ralph now to use the default settings.");
103            Ok(true)
104        }
105        Ok(ConfigInitResult::AlreadyExists) => {
106            println!(
107                "{}Config file already exists:{} {}",
108                colors.yellow(),
109                colors.reset(),
110                agents_config_path.display()
111            );
112            println!("Edit the file to customize, or delete it to regenerate from defaults.");
113            Ok(true)
114        }
115        Err(e) => Err(anyhow::anyhow!(
116            "Failed to create config file {}: {}",
117            agents_config_path.display(),
118            e
119        )),
120    }
121}
122
123// NOTE: legacy per-repo agents.toml creation is handled by `--init-legacy` only.
124
125/// Prompt the user to confirm overwriting an existing PROMPT.md.
126///
127/// Returns `true` if the user confirms, `false` otherwise.
128///
129/// Requires stdin to be a terminal and at least one output stream (stdout/stderr)
130/// to be a terminal so prompts are visible.
131fn can_prompt_user() -> bool {
132    prompt_output_target().is_some()
133}
134
135#[derive(Clone, Copy)]
136enum PromptOutputTarget {
137    Stdout,
138    Stderr,
139}
140
141fn prompt_output_target() -> Option<PromptOutputTarget> {
142    if !std::io::stdin().is_terminal() {
143        return None;
144    }
145
146    if std::io::stdout().is_terminal() {
147        return Some(PromptOutputTarget::Stdout);
148    }
149    if std::io::stderr().is_terminal() {
150        return Some(PromptOutputTarget::Stderr);
151    }
152
153    None
154}
155
156fn with_prompt_writer<T>(
157    target: PromptOutputTarget,
158    f: impl FnOnce(&mut dyn std::io::Write) -> anyhow::Result<T>,
159) -> anyhow::Result<T> {
160    use std::io;
161
162    match target {
163        PromptOutputTarget::Stdout => {
164            let mut out = io::stdout().lock();
165            f(&mut out)
166        }
167        PromptOutputTarget::Stderr => {
168            let mut err = io::stderr().lock();
169            f(&mut err)
170        }
171    }
172}
173
174fn prompt_overwrite_confirmation(prompt_path: &Path, colors: Colors) -> anyhow::Result<bool> {
175    use std::io;
176
177    let Some(target) = prompt_output_target() else {
178        return Ok(false);
179    };
180
181    with_prompt_writer(target, |w| {
182        writeln!(
183            w,
184            "{}PROMPT.md already exists:{} {}",
185            colors.yellow(),
186            colors.reset(),
187            prompt_path.display()
188        )?;
189        write!(w, "Do you want to overwrite it? [y/N]: ")?;
190        w.flush()?;
191        Ok(())
192    })?;
193
194    let mut input = String::new();
195    match io::stdin().read_line(&mut input) {
196        Ok(0) => return Ok(false),
197        Ok(_) => {}
198        Err(_) => return Ok(false),
199    }
200
201    let response = input.trim().to_lowercase();
202    Ok(response == "y" || response == "yes")
203}
204
205/// Handle the `--init-prompt` flag.
206///
207/// Creates a PROMPT.md file from the specified template.
208///
209/// # Arguments
210///
211/// * `template_name` - The name of the template to use
212/// * `force` - If true, overwrite existing PROMPT.md without prompting
213/// * `colors` - Terminal color configuration for output
214///
215/// # Returns
216///
217/// Returns `Ok(true)` if the flag was handled (program should exit after),
218/// or an error if template creation failed.
219pub fn handle_init_prompt(
220    template_name: &str,
221    force: bool,
222    colors: Colors,
223) -> anyhow::Result<bool> {
224    handle_init_prompt_at_path(template_name, Path::new("PROMPT.md"), force, colors)
225}
226
227fn handle_init_prompt_at_path(
228    template_name: &str,
229    prompt_path: &Path,
230    force: bool,
231    colors: Colors,
232) -> anyhow::Result<bool> {
233    // Validate the template exists first, before any file operations
234    let Some(template) = get_template(template_name) else {
235        println!(
236            "{}Unknown Work Guide: '{}'{}",
237            colors.red(),
238            template_name,
239            colors.reset()
240        );
241        println!();
242        let similar = find_similar_templates(template_name);
243        if !similar.is_empty() {
244            println!("{}Did you mean?{}", colors.yellow(), colors.reset());
245            for (name, score) in similar {
246                println!(
247                    "  {}{}{}  ({}% similar)",
248                    colors.cyan(),
249                    name,
250                    colors.reset(),
251                    score
252                );
253            }
254            println!();
255        }
256        println!("Commonly used Work Guides:");
257        print_common_work_guides(colors);
258        println!("Usage: ralph --init-prompt <work-guide>");
259        return Ok(true);
260    };
261
262    let content = template.content();
263    if force {
264        fs::write(prompt_path, content)?;
265    } else {
266        match OpenOptions::new()
267            .write(true)
268            .create_new(true)
269            .open(prompt_path)
270        {
271            Ok(mut file) => {
272                use std::io::Write;
273                file.write_all(content.as_bytes())?;
274            }
275            Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
276                if can_prompt_user() {
277                    if !prompt_overwrite_confirmation(prompt_path, colors)? {
278                        return Ok(true);
279                    }
280                    fs::write(prompt_path, content)?;
281                } else {
282                    return Err(anyhow::anyhow!(
283                        "PROMPT.md already exists: {}\nRefusing to overwrite in non-interactive mode. Use --force-overwrite to overwrite, or delete/backup the existing file.",
284                        prompt_path.display()
285                    ));
286                }
287            }
288            Err(err) => return Err(err.into()),
289        }
290    }
291
292    println!(
293        "{}Created PROMPT.md from template: {}{}{}",
294        colors.green(),
295        colors.bold(),
296        template_name,
297        colors.reset()
298    );
299    println!();
300    println!(
301        "Template: {}{}{}  {}",
302        colors.cyan(),
303        template.name(),
304        colors.reset(),
305        template.description()
306    );
307    println!();
308    println!("Next steps:");
309    println!("  1. Edit PROMPT.md with your task details");
310    println!("  2. Run: ralph \"your commit message\"");
311    println!();
312    println!("Tip: Use --list-work-guides to see all available Work Guides.");
313
314    Ok(true)
315}
316
317/// Print a short list of common Work Guides.
318///
319/// Shows the most commonly used Work Guides with a note to use --list-work-guides for more.
320fn print_common_work_guides(colors: Colors) {
321    println!("{}Common Work Guides:{}", colors.bold(), colors.reset());
322    println!(
323        "  {}quick{}           Quick/small changes (typos, minor fixes)",
324        colors.cyan(),
325        colors.reset()
326    );
327    println!(
328        "  {}bug-fix{}         Bug fix with investigation guidance",
329        colors.cyan(),
330        colors.reset()
331    );
332    println!(
333        "  {}feature-spec{}    Comprehensive product specification",
334        colors.cyan(),
335        colors.reset()
336    );
337    println!(
338        "  {}refactor{}        Code refactoring with behavior preservation",
339        colors.cyan(),
340        colors.reset()
341    );
342    println!();
343    println!(
344        "Use {}--list-work-guides{} for the complete list of Work Guides.",
345        colors.cyan(),
346        colors.reset()
347    );
348    println!();
349}
350
351/// Print a template category section.
352///
353/// Helper function to reduce the length of `handle_list_work_guides`.
354fn print_template_category(category_name: &str, templates: &[(&str, &str)], colors: Colors) {
355    println!("{}{}:{}", colors.bold(), category_name, colors.reset());
356    for (name, description) in templates {
357        println!(
358            "  {}{}{}  {}",
359            colors.cyan(),
360            name,
361            colors.reset(),
362            description
363        );
364    }
365    println!();
366}
367
368/// Handle the `--list-work-guides` (or `--list-templates`) flag.
369///
370/// Lists all available PROMPT.md Work Guides with descriptions, organized by category.
371///
372/// # Arguments
373///
374/// * `colors` - Terminal color configuration for output
375///
376/// # Returns
377///
378/// Returns `true` if the flag was handled (program should exit after).
379pub fn handle_list_work_guides(colors: Colors) -> bool {
380    println!("PROMPT.md Work Guides (use: ralph --init <work-guide>)");
381    println!();
382
383    // Common templates (most frequently used)
384    print_template_category(
385        "Common Templates",
386        &[
387            ("quick", "Quick/small changes (typos, minor fixes)"),
388            ("bug-fix", "Bug fix with investigation guidance"),
389            ("feature-spec", "Comprehensive product specification"),
390            ("refactor", "Code refactoring with behavior preservation"),
391        ],
392        colors,
393    );
394
395    // Testing and documentation
396    print_template_category(
397        "Testing & Documentation",
398        &[
399            ("test", "Test writing with edge case considerations"),
400            ("docs", "Documentation update with completeness checklist"),
401            ("code-review", "Structured code review for pull requests"),
402        ],
403        colors,
404    );
405
406    // Specialized development
407    print_template_category(
408        "Specialized Development",
409        &[
410            ("cli-tool", "CLI tool with argument parsing and completion"),
411            ("web-api", "REST/HTTP API with error handling"),
412            (
413                "ui-component",
414                "UI component with accessibility and responsive design",
415            ),
416            ("onboarding", "Learn a new codebase efficiently"),
417        ],
418        colors,
419    );
420
421    // Advanced/Infrastructure
422    print_template_category(
423        "Advanced & Infrastructure",
424        &[
425            (
426                "performance-optimization",
427                "Performance optimization with benchmarking",
428            ),
429            (
430                "security-audit",
431                "Security audit with OWASP Top 10 coverage",
432            ),
433            (
434                "api-integration",
435                "API integration with retry logic and resilience",
436            ),
437            (
438                "database-migration",
439                "Database migration with zero-downtime strategies",
440            ),
441            (
442                "dependency-update",
443                "Dependency update with breaking change handling",
444            ),
445            ("data-pipeline", "Data pipeline with ETL and monitoring"),
446        ],
447        colors,
448    );
449
450    // Maintenance
451    print_template_category(
452        "Maintenance & Operations",
453        &[
454            (
455                "debug-triage",
456                "Systematic issue investigation and diagnosis",
457            ),
458            (
459                "tech-debt",
460                "Technical debt refactoring with prioritization",
461            ),
462            (
463                "release",
464                "Release preparation with versioning and changelog",
465            ),
466        ],
467        colors,
468    );
469
470    println!("Usage: ralph --init <work-guide>");
471    println!("       ralph --init-prompt <work-guide>");
472    println!();
473    println!("Example:");
474    println!("  ralph --init bug-fix              # Create bug fix Work Guide");
475    println!("  ralph --init feature-spec         # Create feature spec Work Guide");
476    println!("  ralph --init quick                # Create quick change Work Guide");
477    println!();
478    println!("{}Tip:{}", colors.yellow(), colors.reset());
479    println!("  Use --init without a value to auto-detect what you need.");
480    println!("  Use --force-overwrite to overwrite an existing PROMPT.md.");
481    println!("  Run ralph --extended-help to learn about Work Guides vs Agent Prompts.");
482
483    true
484}
485
486/// Handle the smart `--init` flag.
487///
488/// This function intelligently determines what the user wants to initialize:
489/// - If a value is provided and matches a known template name → create PROMPT.md
490/// - If config doesn't exist and no template specified → create config
491/// - If config exists but PROMPT.md doesn't → prompt to create PROMPT.md
492/// - If both exist → show helpful message about what's already set up
493///
494/// # Arguments
495///
496/// * `template_arg` - Optional template name from `--init=TEMPLATE`
497/// * `force` - If true, overwrite existing PROMPT.md without prompting
498/// * `colors` - Terminal color configuration for output
499///
500/// # Returns
501///
502/// Returns `Ok(true)` if the flag was handled (program should exit after),
503/// or `Ok(false)` if not handled, or an error if initialization failed.
504pub fn handle_smart_init(
505    template_arg: Option<&str>,
506    force: bool,
507    colors: Colors,
508) -> anyhow::Result<bool> {
509    let config_path = crate::config::unified_config_path()
510        .ok_or_else(|| anyhow::anyhow!("Cannot determine config directory (no home directory)"))?;
511    handle_smart_init_at_paths(
512        template_arg,
513        force,
514        colors,
515        &config_path,
516        Path::new("PROMPT.md"),
517    )
518}
519
520fn handle_smart_init_at_paths(
521    template_arg: Option<&str>,
522    force: bool,
523    colors: Colors,
524    config_path: &std::path::Path,
525    prompt_path: &Path,
526) -> anyhow::Result<bool> {
527    let config_exists = config_path.exists();
528    let prompt_exists = prompt_path.exists();
529
530    // If a template name is provided (non-empty), treat it as --init-prompt
531    if let Some(template_name) = template_arg {
532        if !template_name.is_empty() {
533            return handle_init_template_arg_at_path(template_name, prompt_path, force, colors);
534        }
535        // Empty string means --init was used without a value, fall through to smart inference
536    }
537
538    // No template provided - use smart inference based on current state
539    handle_init_state_inference(
540        config_path,
541        prompt_path,
542        config_exists,
543        prompt_exists,
544        force,
545        colors,
546    )
547}
548
549/// Calculate Levenshtein distance between two strings.
550///
551/// Returns the minimum number of single-character edits (insertions, deletions,
552/// or substitutions) required to change one string into the other.
553fn levenshtein_distance(a: &str, b: &str) -> usize {
554    let a_chars: Vec<char> = a.chars().collect();
555    let b_chars: Vec<char> = b.chars().collect();
556    let b_len = b_chars.len();
557
558    // Use two rows to save memory
559    let mut prev_row: Vec<usize> = (0..=b_len).collect();
560    let mut curr_row = vec![0; b_len + 1];
561
562    for (i, a_char) in a_chars.iter().enumerate() {
563        curr_row[0] = i + 1;
564
565        for (j, b_char) in b_chars.iter().enumerate() {
566            let cost = usize::from(a_char != b_char);
567            curr_row[j + 1] = std::cmp::min(
568                std::cmp::min(
569                    curr_row[j] + 1,     // deletion
570                    prev_row[j + 1] + 1, // insertion
571                ),
572                prev_row[j] + cost, // substitution
573            );
574        }
575
576        std::mem::swap(&mut prev_row, &mut curr_row);
577    }
578
579    prev_row[b_len]
580}
581
582/// Calculate similarity score as a percentage (0-100).
583///
584/// This avoids floating point comparison issues in tests.
585fn similarity_percentage(a: &str, b: &str) -> u32 {
586    if a == b {
587        return 100;
588    }
589    if a.is_empty() || b.is_empty() {
590        return 0;
591    }
592
593    let max_len = a.len().max(b.len());
594    let distance = levenshtein_distance(a, b);
595
596    if max_len == 0 {
597        return 100;
598    }
599
600    // Calculate percentage without floating point
601    // (100 * (max_len - distance)) / max_len
602    let diff = max_len.saturating_sub(distance);
603    // The division result is guaranteed to fit in u32 since it's ≤ 100
604    u32::try_from((100 * diff) / max_len).unwrap_or(0)
605}
606
607/// Find the best matching template names using fuzzy matching.
608///
609/// Returns templates that are similar to the input within the threshold.
610fn find_similar_templates(input: &str) -> Vec<(&'static str, u32)> {
611    let input_lower = input.to_lowercase();
612    let mut matches: Vec<(&'static str, u32)> = ALL_TEMPLATES
613        .iter()
614        .map(|t| {
615            let name = t.name();
616            let sim = similarity_percentage(&input_lower, &name.to_lowercase());
617            (name, sim)
618        })
619        .filter(|(_, sim)| *sim >= MIN_SIMILARITY_PERCENT)
620        .collect();
621
622    // Sort by similarity (highest first)
623    matches.sort_by(|a, b| b.1.cmp(&a.1));
624
625    // Return top 3 matches
626    matches.truncate(3);
627    matches
628}
629
630fn handle_init_template_arg_at_path(
631    template_name: &str,
632    prompt_path: &Path,
633    force: bool,
634    colors: Colors,
635) -> anyhow::Result<bool> {
636    if get_template(template_name).is_some() {
637        return handle_init_prompt_at_path(template_name, prompt_path, force, colors);
638    }
639
640    // Unknown value - show helpful error with suggestions
641    println!(
642        "{}Unknown Work Guide: '{}'{}",
643        colors.red(),
644        template_name,
645        colors.reset()
646    );
647    println!();
648
649    // Try to find similar template names
650    let similar = find_similar_templates(template_name);
651    if !similar.is_empty() {
652        println!("{}Did you mean?{}", colors.yellow(), colors.reset());
653        for (name, score) in similar {
654            println!(
655                "  {}{}{}  ({}% similar)",
656                colors.cyan(),
657                name,
658                colors.reset(),
659                score
660            );
661        }
662        println!();
663    }
664
665    println!("Commonly used Work Guides:");
666    print_common_work_guides(colors);
667    println!("Usage: ralph --init=<work-guide>");
668    println!("       ralph --init            # Smart init (infers intent)");
669    Ok(true)
670}
671
672/// Handle --init with smart inference based on current state.
673fn handle_init_state_inference(
674    config_path: &std::path::Path,
675    prompt_path: &Path,
676    config_exists: bool,
677    prompt_exists: bool,
678    force: bool,
679    colors: Colors,
680) -> anyhow::Result<bool> {
681    match (config_exists, prompt_exists) {
682        (false, false) => handle_init_none_exist(config_path, colors),
683        (true, false) => Ok(handle_init_only_config_exists(config_path, force, colors)),
684        (false, true) => handle_init_only_prompt_exists(colors),
685        (true, true) => Ok(handle_init_both_exist(
686            config_path,
687            prompt_path,
688            force,
689            colors,
690        )),
691    }
692}
693
694/// Handle --init when neither config nor PROMPT.md exists.
695fn handle_init_none_exist(_config_path: &std::path::Path, colors: Colors) -> anyhow::Result<bool> {
696    println!(
697        "{}No config found. Creating unified config...{}",
698        colors.dim(),
699        colors.reset()
700    );
701    println!();
702    handle_init_global(colors)?;
703    Ok(true)
704}
705
706/// Handle --init when only config exists (no PROMPT.md).
707///
708/// When in a TTY, prompts for template selection.
709/// When not in a TTY, creates a minimal default PROMPT.md.
710fn handle_init_only_config_exists(
711    config_path: &std::path::Path,
712    force: bool,
713    colors: Colors,
714) -> bool {
715    println!(
716        "{}Config found at:{} {}",
717        colors.green(),
718        colors.reset(),
719        config_path.display()
720    );
721    println!(
722        "{}PROMPT.md not found in current directory.{}",
723        colors.yellow(),
724        colors.reset()
725    );
726    println!();
727
728    // Show common Work Guides inline
729    print_common_work_guides(colors);
730
731    // Check if we're in a TTY for interactive prompting
732    if can_prompt_user() {
733        // Interactive mode: prompt for template selection
734        if let Some(template_name) = prompt_for_template(colors) {
735            match handle_init_prompt(&template_name, force, colors) {
736                Ok(_) => return true,
737                Err(e) => {
738                    println!(
739                        "{}Failed to create PROMPT.md: {}{}",
740                        colors.red(),
741                        e,
742                        colors.reset()
743                    );
744                    return true;
745                }
746            }
747        }
748        // User declined or entered invalid input, fall through to show usage
749    } else {
750        // Non-interactive mode: create a minimal default PROMPT.md
751        let default_content = create_minimal_prompt_md();
752        let prompt_path = Path::new("PROMPT.md");
753
754        match OpenOptions::new()
755            .write(true)
756            .create_new(true)
757            .open(prompt_path)
758        {
759            Ok(mut file) => {
760                use std::io::Write;
761                if let Err(e) = file.write_all(default_content.as_bytes()) {
762                    println!(
763                        "{}Failed to create PROMPT.md: {}{}",
764                        colors.red(),
765                        e,
766                        colors.reset()
767                    );
768                    return true;
769                }
770                println!(
771                    "{}Created minimal PROMPT.md{}",
772                    colors.green(),
773                    colors.reset()
774                );
775                println!();
776                println!("Next steps:");
777                println!("  1. Edit PROMPT.md with your task details");
778                println!("  2. Run: ralph \"your commit message\"");
779                println!();
780                println!("Tip: Use ralph --list-work-guides to see all available Work Guides.");
781                return true;
782            }
783            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
784                println!(
785                    "{}PROMPT.md already exists:{} {}",
786                    colors.yellow(),
787                    colors.reset(),
788                    prompt_path.display()
789                );
790                println!("Use --force-overwrite to overwrite, or delete/backup the existing file.");
791                return true;
792            }
793            Err(e) => {
794                println!(
795                    "{}Failed to create PROMPT.md: {}{}",
796                    colors.red(),
797                    e,
798                    colors.reset()
799                );
800                return true;
801            }
802        }
803    }
804
805    // Show template list if we didn't create PROMPT.md
806    println!("Create a PROMPT.md from a Work Guide to get started:");
807    println!();
808
809    for (name, description) in list_templates() {
810        println!(
811            "  {}{}{}  {}{}{}",
812            colors.cyan(),
813            name,
814            colors.reset(),
815            colors.dim(),
816            description,
817            colors.reset()
818        );
819    }
820
821    println!();
822    println!("Usage: ralph --init <work-guide>");
823    println!("       ralph --init-prompt <work-guide>");
824    println!();
825    println!("Example:");
826    println!("  ralph --init bug-fix");
827    println!("  ralph --init feature-spec");
828    true
829}
830
831/// Prompt the user to select a template interactively.
832///
833/// Returns `Some(template_name)` if the user selected a template,
834/// or `None` if the user declined or entered invalid input.
835fn prompt_for_template(colors: Colors) -> Option<String> {
836    use std::io;
837
838    let target = prompt_output_target()?;
839    if with_prompt_writer(target, |w| {
840        let _ = writeln!(
841            w,
842            "PROMPT.md contains your task specification for the AI agents."
843        );
844        let _ = write!(w, "Would you like to create one from a Work Guide? [Y/n]: ");
845        w.flush()?;
846        Ok(())
847    })
848    .is_err()
849    {
850        return None;
851    };
852
853    let mut input = String::new();
854    match io::stdin().read_line(&mut input) {
855        Ok(0) | Err(_) => return None,
856        Ok(_) => {}
857    }
858
859    let response = input.trim().to_lowercase();
860    if response == "n" || response == "no" || response == "skip" {
861        return None;
862    }
863
864    // Show available templates
865    let templates: Vec<(&str, &str)> = list_templates();
866    if with_prompt_writer(target, |w| {
867        let _ = writeln!(w);
868        let _ = writeln!(w, "Available Work Guides:");
869
870        for (i, (name, description)) in templates.iter().enumerate() {
871            let _ = writeln!(
872                w,
873                "  {}{}{}  {}{}{}",
874                colors.cyan(),
875                name,
876                colors.reset(),
877                colors.dim(),
878                description,
879                colors.reset()
880            );
881            if (i + 1) % 5 == 0 {
882                let _ = writeln!(w); // Group templates in sets of 5 for readability
883            }
884        }
885
886        let _ = writeln!(w);
887        let _ = writeln!(w, "Common choices:");
888        let _ = writeln!(
889            w,
890            "  {}quick{}           - Quick/small changes (typos, minor fixes)",
891            colors.cyan(),
892            colors.reset()
893        );
894        let _ = writeln!(
895            w,
896            "  {}bug-fix{}         - Bug fix with investigation guidance",
897            colors.cyan(),
898            colors.reset()
899        );
900        let _ = writeln!(
901            w,
902            "  {}feature-spec{}    - Product specification",
903            colors.cyan(),
904            colors.reset()
905        );
906        let _ = writeln!(w);
907        let _ = write!(w, "Enter Work Guide name (or press Enter to use 'quick'): ");
908        w.flush()?;
909        Ok(())
910    })
911    .is_err()
912    {
913        return None;
914    };
915
916    let mut template_input = String::new();
917    match io::stdin().read_line(&mut template_input) {
918        Ok(0) | Err(_) => return None,
919        Ok(_) => {}
920    }
921
922    let template_name = template_input.trim();
923    if template_name.is_empty() {
924        // Default to 'quick' template
925        return Some("quick".to_string());
926    }
927
928    // Validate the template exists
929    if get_template(template_name).is_some() {
930        Some(template_name.to_string())
931    } else {
932        let _ = with_prompt_writer(target, |w| {
933            writeln!(
934                w,
935                "{}Unknown Work Guide: '{}'{}",
936                colors.red(),
937                template_name,
938                colors.reset()
939            )?;
940            writeln!(
941                w,
942                "Run 'ralph --list-work-guides' to see all available Work Guides."
943            )?;
944            Ok(())
945        });
946        None
947    }
948}
949
950/// Create a minimal default PROMPT.md content.
951fn create_minimal_prompt_md() -> String {
952    "# Task Description
953
954Describe what you want the AI agents to implement.
955
956## Example
957
958\"Fix the typo in the README file\"
959
960## Context
961
962Provide any relevant context about the task:
963- What problem are you trying to solve?
964- What are the acceptance criteria?
965- Are there any specific requirements or constraints?
966
967## Notes
968
969- This is a minimal PROMPT.md created by `ralph --init`
970- You can edit this file directly or use `ralph --init <work-guide>` to start from a Work Guide
971- Run `ralph --list-work-guides` to see all available Work Guides
972"
973    .to_string()
974}
975
976/// Handle --init when only PROMPT.md exists (no config).
977fn handle_init_only_prompt_exists(colors: Colors) -> anyhow::Result<bool> {
978    println!(
979        "{}PROMPT.md found in current directory.{}",
980        colors.green(),
981        colors.reset()
982    );
983    println!(
984        "{}No config found. Creating unified config...{}",
985        colors.dim(),
986        colors.reset()
987    );
988    println!();
989    handle_init_global(colors)?;
990    Ok(true)
991}
992
993/// Handle --init when both config and PROMPT.md exist.
994fn handle_init_both_exist(
995    config_path: &std::path::Path,
996    prompt_path: &Path,
997    force: bool,
998    colors: Colors,
999) -> bool {
1000    // If force is set, show that they can use --force-overwrite to overwrite
1001    if force {
1002        println!(
1003            "{}Note:{} --force-overwrite has no effect when not specifying a Work Guide.",
1004            colors.yellow(),
1005            colors.reset()
1006        );
1007        println!("Use: ralph --init <work-guide> --force-overwrite  to overwrite PROMPT.md");
1008        println!();
1009    }
1010
1011    println!("{}Setup complete!{}", colors.green(), colors.reset());
1012    println!();
1013    println!(
1014        "  Config: {}{}{}",
1015        colors.dim(),
1016        config_path.display(),
1017        colors.reset()
1018    );
1019    println!(
1020        "  PROMPT: {}{}{}",
1021        colors.dim(),
1022        prompt_path.display(),
1023        colors.reset()
1024    );
1025    println!();
1026    println!("You're ready to run Ralph:");
1027    println!("  ralph \"your commit message\"");
1028    println!();
1029    println!("Other commands:");
1030    println!("  ralph --list-work-guides   # Show all Work Guides");
1031    println!("  ralph --init <work-guide> --force-overwrite  # Overwrite PROMPT.md");
1032    true
1033}
1034
1035/// Handle the `--extended-help` / `--man` flag.
1036///
1037/// Displays comprehensive help including shell completion, all presets,
1038/// troubleshooting information, and the difference between Work Guides and Agent Prompts.
1039pub fn handle_extended_help() {
1040    println!(
1041        r#"RALPH EXTENDED HELP
1042═══════════════════════════════════════════════════════════════════════════════
1043
1044Ralph is a PROMPT-driven multi-agent orchestrator for git repos. It runs a
1045developer agent for code implementation, then a reviewer agent for quality
1046assurance, automatically staging and committing the final result.
1047
1048═══════════════════════════════════════════════════════════════════════════════
1049GETTING STARTED
1050═══════════════════════════════════════════════════════════════════════════════
1051
1052  1. Initialize config:
1053       ralph --init                      # Smart init (infers what you need)
1054
1055  2. Create a PROMPT.md from a Work Guide:
1056       ralph --init feature-spec         # Or: bug-fix, refactor, quick, etc.
1057
1058  3. Edit PROMPT.md with your task details
1059
1060  4. Run Ralph:
1061       ralph "fix: my bug description"   # Commit message for the final commit
1062
1063═══════════════════════════════════════════════════════════════════════════════
1064WORK GUIDES VS AGENT PROMPTS
1065═══════════════════════════════════════════════════════════════════════════════
1066
1067  Ralph has two types of templates - understanding the difference is key:
1068
1069  1. WORK GUIDES (for PROMPT.md - YOUR task descriptions)
1070     ─────────────────────────────────────────────────────
1071     These are templates for describing YOUR work to the AI.
1072     You fill them in with your specific task requirements.
1073
1074     Examples: quick, bug-fix, feature-spec, refactor, test, docs
1075
1076     Commands:
1077       ralph --init <work-guide>      Create PROMPT.md from a Work Guide
1078       ralph --list-work-guides       Show all available Work Guides
1079       ralph --init-prompt <name>     Same as --init (legacy alias)
1080
1081  2. AGENT PROMPTS (backend AI behavior configuration)
1082     ─────────────────────────────────────────────────────
1083     These configure HOW the AI agents behave (internal system prompts).
1084     You probably don't need to touch these unless customizing agent behavior.
1085
1086     Commands:
1087       ralph --init-system-prompts    Create default Agent Prompts
1088       ralph --list                   Show Agent Prompt templates
1089       ralph --show <name>            Show a specific Agent Prompt
1090
1091═══════════════════════════════════════════════════════════════════════════════
1092PRESET MODES
1093═══════════════════════════════════════════════════════════════════════════════
1094
1095  Pick how thorough the AI should be:
1096
1097    -Q  Quick:      1 dev iteration  + 1 review   (typos, small fixes)
1098    -U  Rapid:      2 dev iterations + 1 review   (minor changes)
1099    -S  Standard:   5 dev iterations + 2 reviews  (default for most tasks)
1100    -T  Thorough:  10 dev iterations + 5 reviews  (complex features)
1101    -L  Long:      15 dev iterations + 10 reviews (most thorough)
1102
1103  Custom iterations:
1104    ralph -D 3 -R 2 "feat: feature"   # 3 dev iterations, 2 review cycles
1105    ralph -D 10 -R 0 "feat: no review"  # Skip review phase entirely
1106
1107═══════════════════════════════════════════════════════════════════════════════
1108COMMON OPTIONS
1109═══════════════════════════════════════════════════════════════════════════════
1110
1111  Iterations:
1112    -D N, --developer-iters N   Set developer iterations
1113    -R N, --reviewer-reviews N  Set review cycles (0 = skip review)
1114
1115  Agents:
1116    -a AGENT, --developer-agent AGENT   Pick developer agent
1117    -r AGENT, --reviewer-agent AGENT    Pick reviewer agent
1118
1119  Verbosity:
1120    -q, --quiet          Quiet mode (minimal output)
1121    -f, --full           Full output (no truncation)
1122    -v N, --verbosity N  Set verbosity (0-4)
1123
1124  Other:
1125    -d, --diagnose       Show system info and agent status
1126
1127═══════════════════════════════════════════════════════════════════════════════
1128ADVANCED OPTIONS
1129═══════════════════════════════════════════════════════════════════════════════
1130
1131  These options are hidden from the main --help to reduce clutter.
1132
1133  Initialization:
1134    --force-overwrite            Overwrite PROMPT.md without prompting
1135    --init-prompt <name>         Create PROMPT.md (legacy, use --init instead)
1136    -i, --interactive            Prompt for PROMPT.md if missing
1137
1138  Git Control:
1139    --skip-rebase                Skip automatic rebase to main branch
1140    --rebase-only                Only rebase, then exit (no pipeline)
1141    --git-user-name <name>       Override git user name for commits
1142    --git-user-email <email>     Override git user email for commits
1143
1144  Recovery:
1145    --resume                     Resume from last checkpoint
1146    --dry-run                    Validate setup without running agents
1147
1148  Agent Prompt Management:
1149    --init-system-prompts        Create default Agent Prompt templates
1150    --list                       List all Agent Prompt templates
1151    --show <name>                Show Agent Prompt content
1152    --validate                   Validate Agent Prompt templates
1153    --variables <name>           Extract variables from template
1154    --render <name>              Test render a template
1155
1156  Debugging:
1157    --show-streaming-metrics     Show JSON streaming quality metrics
1158    -c PATH, --config PATH       Use specific config file
1159
1160═══════════════════════════════════════════════════════════════════════════════
1161SHELL COMPLETION
1162═══════════════════════════════════════════════════════════════════════════════
1163
1164  Enable tab-completion for faster command entry:
1165
1166    Bash:
1167      ralph --generate-completion=bash > ~/.local/share/bash-completion/completions/ralph
1168
1169    Zsh:
1170      ralph --generate-completion=zsh > ~/.zsh/completion/_ralph
1171
1172    Fish:
1173      ralph --generate-completion=fish > ~/.config/fish/completions/ralph.fish
1174
1175  Then restart your shell or source the file.
1176
1177═══════════════════════════════════════════════════════════════════════════════
1178TROUBLESHOOTING
1179═══════════════════════════════════════════════════════════════════════════════
1180
1181  Common issues:
1182
1183    "PROMPT.md not found"
1184      → Run: ralph --init <work-guide>  (e.g., ralph --init bug-fix)
1185
1186    "No agents available"
1187      → Run: ralph -d  (diagnose) to check agent status
1188      → Ensure at least one agent is installed (claude, codex, opencode)
1189
1190    "Config file not found"
1191      → Run: ralph --init  to create ~/.config/ralph-workflow.toml
1192
1193    Resume after interruption:
1194      → Run: ralph --resume  to continue from last checkpoint
1195
1196    Validate setup without running:
1197      → Run: ralph --dry-run
1198
1199═══════════════════════════════════════════════════════════════════════════════
1200EXAMPLES
1201═══════════════════════════════════════════════════════════════════════════════
1202
1203    ralph "fix: typo"                 Run with default settings
1204    ralph -Q "fix: small bug"         Quick mode for tiny fixes
1205    ralph -U "feat: add button"       Rapid mode for minor features
1206    ralph -a claude "fix: bug"        Use specific agent
1207    ralph --list-work-guides          See all Work Guides
1208    ralph --init bug-fix              Create PROMPT.md from a Work Guide
1209    ralph --init bug-fix --force-overwrite  Overwrite existing PROMPT.md
1210
1211═══════════════════════════════════════════════════════════════════════════════
1212"#
1213    );
1214}
1215
1216#[cfg(test)]
1217mod tests {
1218    use super::*;
1219    use std::path::PathBuf;
1220    use std::sync::atomic::{AtomicUsize, Ordering};
1221
1222    struct TempDir {
1223        dir: PathBuf,
1224    }
1225
1226    impl TempDir {
1227        fn new() -> Self {
1228            static COUNTER: AtomicUsize = AtomicUsize::new(0);
1229
1230            let mut dir = std::env::temp_dir();
1231            let pid = std::process::id();
1232            let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1233            dir.push(format!("ralph-workflow-init-tests-{pid}-{n}"));
1234            std::fs::create_dir_all(&dir).unwrap();
1235            Self { dir }
1236        }
1237    }
1238
1239    impl Drop for TempDir {
1240        fn drop(&mut self) {
1241            let _ = std::fs::remove_dir_all(&self.dir);
1242        }
1243    }
1244
1245    #[test]
1246    fn test_handle_smart_init_with_valid_template_creates_prompt_md() {
1247        let dir = TempDir::new();
1248
1249        let colors = Colors::new();
1250        let prompt_path = dir.dir.join("PROMPT.md");
1251        let config_path = dir.dir.join("ralph-workflow.toml");
1252
1253        let result =
1254            handle_smart_init_at_paths(Some("quick"), false, colors, &config_path, &prompt_path)
1255                .unwrap();
1256        assert!(result);
1257        assert!(prompt_path.exists());
1258
1259        let template = get_template("quick").unwrap();
1260        let content = std::fs::read_to_string(prompt_path).unwrap();
1261        assert_eq!(content, template.content());
1262    }
1263
1264    #[test]
1265    fn test_handle_smart_init_with_invalid_template_does_not_create_prompt_md() {
1266        let dir = TempDir::new();
1267
1268        let colors = Colors::new();
1269        let prompt_path = dir.dir.join("PROMPT.md");
1270        let config_path = dir.dir.join("ralph-workflow.toml");
1271        let result = handle_smart_init_at_paths(
1272            Some("nonexistent-template"),
1273            false,
1274            colors,
1275            &config_path,
1276            &prompt_path,
1277        )
1278        .unwrap();
1279        assert!(result);
1280        assert!(!prompt_path.exists());
1281    }
1282
1283    #[test]
1284    fn test_handle_init_prompt_does_not_overwrite_existing_prompt_without_force() {
1285        let dir = TempDir::new();
1286        let prompt_path = dir.dir.join("PROMPT.md");
1287        std::fs::write(&prompt_path, "original").unwrap();
1288
1289        let colors = Colors::new();
1290        let err = handle_init_prompt_at_path("quick", &prompt_path, false, colors).unwrap_err();
1291        assert!(err
1292            .to_string()
1293            .contains("Refusing to overwrite in non-interactive mode"));
1294        let content = std::fs::read_to_string(prompt_path).unwrap();
1295        assert_eq!(content, "original");
1296    }
1297
1298    #[test]
1299    fn test_handle_init_prompt_overwrites_existing_prompt_with_force() {
1300        let dir = TempDir::new();
1301        let prompt_path = dir.dir.join("PROMPT.md");
1302        std::fs::write(&prompt_path, "original").unwrap();
1303
1304        let colors = Colors::new();
1305        let result = handle_init_prompt_at_path("quick", &prompt_path, true, colors).unwrap();
1306        assert!(result);
1307
1308        let template = get_template("quick").unwrap();
1309        let content = std::fs::read_to_string(prompt_path).unwrap();
1310        assert_eq!(content, template.content());
1311    }
1312
1313    #[test]
1314    fn test_template_name_validation() {
1315        // Test that we can validate template names
1316        assert!(get_template("bug-fix").is_some());
1317        assert!(get_template("feature-spec").is_some());
1318        assert!(get_template("refactor").is_some());
1319        assert!(get_template("test").is_some());
1320        assert!(get_template("docs").is_some());
1321        assert!(get_template("quick").is_some());
1322
1323        // Invalid template names
1324        assert!(get_template("invalid").is_none());
1325        assert!(get_template("").is_none());
1326    }
1327
1328    #[test]
1329    fn test_levenshtein_distance() {
1330        // Exact match
1331        assert_eq!(levenshtein_distance("test", "test"), 0);
1332
1333        // One edit
1334        assert_eq!(levenshtein_distance("test", "tast"), 1);
1335        assert_eq!(levenshtein_distance("test", "tests"), 1);
1336        assert_eq!(levenshtein_distance("test", "est"), 1);
1337
1338        // Two edits
1339        assert_eq!(levenshtein_distance("test", "taste"), 2);
1340        assert_eq!(levenshtein_distance("test", "best"), 1);
1341
1342        // Completely different
1343        assert_eq!(levenshtein_distance("abc", "xyz"), 3);
1344    }
1345
1346    #[test]
1347    fn test_similarity() {
1348        // Exact match
1349        assert_eq!(similarity_percentage("test", "test"), 100);
1350
1351        // Similar strings - should be high similarity
1352        assert!(similarity_percentage("bug-fix", "bugfix") > 80);
1353        assert!(similarity_percentage("feature-spec", "feature") > 50);
1354
1355        // Different strings - should be low similarity
1356        assert!(similarity_percentage("test", "xyz") < 50);
1357
1358        // Empty strings
1359        assert_eq!(similarity_percentage("", ""), 100);
1360        assert_eq!(similarity_percentage("test", ""), 0);
1361        assert_eq!(similarity_percentage("", "test"), 0);
1362    }
1363
1364    #[test]
1365    fn test_find_similar_templates() {
1366        // Find similar to "bugfix" (missing hyphen)
1367        let similar = find_similar_templates("bugfix");
1368        assert!(!similar.is_empty());
1369        assert!(similar.iter().any(|(name, _)| *name == "bug-fix"));
1370
1371        // Find similar to "feature" (should match feature-spec)
1372        let similar = find_similar_templates("feature");
1373        assert!(!similar.is_empty());
1374        assert!(similar.iter().any(|(name, _)| name.contains("feature")));
1375
1376        // Very different string should return empty or low similarity
1377        let similar = find_similar_templates("xyzabc");
1378        // Either empty or all matches have low similarity
1379        assert!(similar.is_empty() || similar.iter().all(|(_, sim)| *sim < 50));
1380    }
1381}