Skip to main content

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