ralph-workflow 0.7.18

PROMPT-driven multi-agent orchestrator for git repos
Documentation
//! I/O boundary for config generation.
//!
//! This module contains CLI handlers that perform console I/O.
//! According to the Boundary-First Architecture pattern, all I/O
//! operations (including console output) should live in boundary modules.
//!
//! See `docs/plans/2026-03-16-functional-rust-refactoring-plan.md` for details.

use crate::config::ConfigEnvironment;
use crate::logger::Colors;
use crate::templates::{get_template, list_templates};
use std::io::Write;
use std::path::Path;

use super::global::handle_init_global_with;

use crate::cli::init::find_similar_templates;
use crate::cli::init::print_common_work_guides;
use crate::cli::init::{can_prompt_user, prompt_for_template, prompt_overwrite_confirmation};

fn create_minimal_prompt_md() -> String {
    "# Task Description\n\nDescribe what you want the AI agents to implement.\n\n## Example\n\n\"Fix the typo in the README file\"\n\n## Context\n\nProvide any relevant context about the task:\n- What problem are you trying to solve?\n- What are the acceptance criteria?\n- Are there any specific requirements or constraints?\n\n## Notes\n\n- This is a minimal PROMPT.md created by `ralph --init`\n- You can edit this file directly or use `ralph --init <work-guide>` to start from a Work Guide\n- Run `ralph --list-work-guides` to see all available Work Guides\n"
    .to_string()
}

fn print_unknown_template_error(template_name: &str, colors: Colors) {
    let _ = writeln!(
        std::io::stdout(),
        "{}Unknown Work Guide: '{}'{}",
        colors.red(),
        template_name,
        colors.reset()
    );
    let similar = find_similar_templates(template_name);
    if !similar.is_empty() {
        let _ = writeln!(
            std::io::stdout(),
            "{}Did you mean:?{}",
            colors.yellow(),
            colors.reset()
        );
        similar.into_iter().for_each(|(name, score)| {
            let _ = writeln!(std::io::stdout(), "  - {} ({}% match)", name, score);
        });
    }
}

fn print_aborted(colors: Colors) {
    let _ = writeln!(
        std::io::stdout(),
        "{}Aborted.{}",
        colors.yellow(),
        colors.reset()
    );
}

fn prompt_overwrite_if_exists<R: ConfigEnvironment>(
    prompt_path: &Path,
    colors: Colors,
    _env: &R,
) -> anyhow::Result<bool> {
    let response = prompt_overwrite_confirmation(prompt_path, colors)?;
    if !response {
        print_aborted(colors);
    }
    Ok(response)
}

fn check_overwrite_if_needed<R: ConfigEnvironment>(
    prompt_path: &Path,
    force: bool,
    colors: Colors,
    env: &R,
) -> anyhow::Result<bool> {
    if env.file_exists(prompt_path) && !force {
        return prompt_overwrite_if_exists(prompt_path, colors, env);
    }
    Ok(true)
}

fn write_template_and_notify<R: ConfigEnvironment>(
    template_name: &str,
    prompt_path: &Path,
    colors: Colors,
    env: &R,
) -> anyhow::Result<()> {
    let template = get_template(template_name)
        .ok_or_else(|| anyhow::anyhow!("template not found: {}", template_name))?;
    env.write_file(prompt_path, template.content())?;
    let _ = writeln!(
        std::io::stdout(),
        "{}Created PROMPT.md from '{}' work guide{}",
        colors.green(),
        template.name(),
        colors.reset()
    );
    Ok(())
}

fn validate_or_report_template(template_name: &str, colors: Colors) -> bool {
    use crate::cli::diagnostics_domain::{validate_template_name, TemplateValidation};
    if matches!(
        validate_template_name(template_name),
        TemplateValidation::Unknown
    ) {
        print_unknown_template_error(template_name, colors);
        return false;
    }
    true
}

pub fn create_prompt_from_template<R: ConfigEnvironment>(
    template_name: &str,
    prompt_path: &Path,
    force: bool,
    colors: Colors,
    env: &R,
) -> anyhow::Result<bool> {
    if !validate_or_report_template(template_name, colors) {
        return Ok(true);
    }

    if !check_overwrite_if_needed(prompt_path, force, colors, env)? {
        return Ok(false);
    }

    write_template_and_notify(template_name, prompt_path, colors, env)?;
    Ok(true)
}

pub fn handle_init_state_inference_with_env<R: ConfigEnvironment>(
    config_path: &Path,
    prompt_path: &Path,
    template_arg: Option<&str>,
    colors: Colors,
    env: &R,
) -> anyhow::Result<bool> {
    let has_config = env.file_exists(config_path);
    let has_prompt = env.file_exists(prompt_path);
    let action =
        crate::cli::diagnostics_domain::determine_init_action(has_config, has_prompt, template_arg);

    match action {
        crate::cli::diagnostics_domain::InitFileState::BothExist => {
            handle_both_exists(prompt_path, colors, env)
        }
        crate::cli::diagnostics_domain::InitFileState::ConfigOnly => {
            handle_config_only(prompt_path, colors, env)
        }
        crate::cli::diagnostics_domain::InitFileState::PromptOnly => {
            handle_prompt_only(colors, env)
        }
        crate::cli::diagnostics_domain::InitFileState::NeitherExists => {
            handle_neither_exists(prompt_path, colors, env)
        }
    }
}

fn handle_both_exists<R: ConfigEnvironment>(
    prompt_path: &Path,
    colors: Colors,
    env: &R,
) -> anyhow::Result<bool> {
    let _ = writeln!(
        std::io::stdout(),
        "{}Found existing config and PROMPT.md{}",
        colors.yellow(),
        colors.reset()
    );
    let response = prompt_overwrite_confirmation(Path::new("Reinitialize both?"), colors)?;
    if response {
        handle_init_global_with(colors, env)?;
        create_prompt_from_template("blank", prompt_path, true, colors, env)?;
        return Ok(true);
    }
    Ok(false)
}

fn create_minimal_and_notify<R: ConfigEnvironment>(
    prompt_path: &Path,
    colors: Colors,
    env: &R,
) -> anyhow::Result<bool> {
    let content = create_minimal_prompt_md();
    env.write_file(prompt_path, &content)?;
    let _ = writeln!(
        std::io::stdout(),
        "{}Created minimal PROMPT.md (non-interactive mode){}",
        colors.green(),
        colors.reset()
    );
    Ok(false)
}

fn maybe_prompt_for_template(can_prompt: bool, colors: Colors) -> Option<String> {
    if can_prompt {
        prompt_for_template(colors)
    } else {
        None
    }
}

fn dispatch_config_only_action<R: ConfigEnvironment>(
    prompt_path: &Path,
    colors: Colors,
    env: &R,
) -> anyhow::Result<bool> {
    use crate::cli::diagnostics_domain::ConfigOnlyNextAction;
    let can_prompt = can_prompt_user();
    let template_name = maybe_prompt_for_template(can_prompt, colors);
    match crate::cli::diagnostics_domain::determine_config_only_next_action(
        can_prompt,
        template_name,
    ) {
        ConfigOnlyNextAction::CreateFromTemplate(name) => {
            create_prompt_from_template(&name, prompt_path, false, colors, env)
        }
        ConfigOnlyNextAction::CreateMinimal => create_minimal_and_notify(prompt_path, colors, env),
        ConfigOnlyNextAction::Skip => Ok(false),
    }
}

fn handle_config_only<R: ConfigEnvironment>(
    prompt_path: &Path,
    colors: Colors,
    env: &R,
) -> anyhow::Result<bool> {
    let _ = writeln!(
        std::io::stdout(),
        "{}Found existing config but no PROMPT.md{}",
        colors.yellow(),
        colors.reset()
    );
    let response = prompt_overwrite_confirmation(Path::new("Create PROMPT.md?"), colors)?;
    if !response {
        return Ok(false);
    }
    dispatch_config_only_action(prompt_path, colors, env)
}

fn handle_prompt_only<R: ConfigEnvironment>(colors: Colors, env: &R) -> anyhow::Result<bool> {
    let _ = writeln!(
        std::io::stdout(),
        "{}Found existing PROMPT.md but no config{}",
        colors.yellow(),
        colors.reset()
    );
    let response = prompt_overwrite_confirmation(Path::new("Create config?"), colors)?;
    if response {
        handle_init_global_with(colors, env)?;
        return Ok(true);
    }
    Ok(false)
}

fn print_available_work_guides(colors: Colors) {
    let _ = writeln!(
        std::io::stdout(),
        "\n{}Available Work Guides:{}",
        colors.blue(),
        colors.reset()
    );
    let _ = list_templates();
    let _ = writeln!(std::io::stdout());
    print_common_work_guides(colors);
}

fn create_minimal_with_help<R: ConfigEnvironment>(
    prompt_path: &Path,
    colors: Colors,
    env: &R,
) -> anyhow::Result<bool> {
    let content = create_minimal_prompt_md();
    env.write_file(prompt_path, &content)?;
    let _ = writeln!(
        std::io::stdout(),
        "{}Created minimal PROMPT.md (non-interactive mode){}",
        colors.green(),
        colors.reset()
    );
    crate::cli::handle_extended_help();
    Ok(true)
}

fn dispatch_neither_exists_action<R: ConfigEnvironment>(
    prompt_path: &Path,
    colors: Colors,
    env: &R,
) -> anyhow::Result<bool> {
    use crate::cli::diagnostics_domain::NeitherExistsNextAction;
    let can_prompt = can_prompt_user();
    let template_name = maybe_prompt_for_template(can_prompt, colors);
    match crate::cli::diagnostics_domain::determine_neither_exists_next_action(
        can_prompt,
        template_name,
    ) {
        NeitherExistsNextAction::CreateFromTemplate(name) => {
            create_prompt_from_template(&name, prompt_path, false, colors, env)
        }
        NeitherExistsNextAction::CreateMinimal => {
            create_minimal_with_help(prompt_path, colors, env)
        }
        NeitherExistsNextAction::Skip => Ok(true),
    }
}

fn handle_neither_exists<R: ConfigEnvironment>(
    prompt_path: &Path,
    colors: Colors,
    env: &R,
) -> anyhow::Result<bool> {
    handle_init_global_with(colors, env)?;
    print_available_work_guides(colors);
    dispatch_neither_exists_action(prompt_path, colors, env)
}

pub fn handle_init_template_arg_at_path_with_env<R: ConfigEnvironment>(
    template_name: &str,
    prompt_path: &Path,
    colors: Colors,
    env: &R,
) -> anyhow::Result<bool> {
    create_prompt_from_template(template_name, prompt_path, false, colors, env)
}