Skip to main content

ralph_workflow/cli/init/
config_generation.rs

1// Configuration file generation logic.
2//
3// This file is included via include!() macro from the parent init.rs module.
4// Contains handlers for creating config files and PROMPT.md.
5
6/// Handle the `--init-global` flag with a custom path resolver.
7///
8/// Creates a unified config file at the path determined by the resolver.
9/// This is the recommended way to configure Ralph globally.
10///
11/// # Arguments
12///
13/// * `colors` - Terminal color configuration for output
14/// * `resolver` - Path resolver for determining config file location
15///
16/// # Returns
17///
18/// Returns `Ok(true)` if the flag was handled (program should exit after),
19/// or an error if config creation failed.
20pub fn handle_init_global_with<R: ConfigEnvironment>(
21    colors: Colors,
22    env: &R,
23) -> anyhow::Result<bool> {
24    let global_path = env
25        .unified_config_path()
26        .ok_or_else(|| anyhow::anyhow!("Cannot determine config directory (no home directory)"))?;
27
28    // Check if config already exists using the environment
29    if env.file_exists(&global_path) {
30        println!(
31            "{}Unified config already exists:{} {}",
32            colors.yellow(),
33            colors.reset(),
34            global_path.display()
35        );
36        println!("Edit the file to customize, or delete it to regenerate from defaults.");
37        println!();
38        println!("Next steps:");
39        println!("  1. Create a PROMPT.md for your task:");
40        println!("       ralph --init <work-guide>");
41        println!("       ralph --list-work-guides  # Show all Work Guides");
42        println!("  2. Or run ralph directly with default settings:");
43        println!("       ralph \"your commit message\"");
44        return Ok(true);
45    }
46
47    // Create config using the environment's file operations
48    env.write_file(&global_path, crate::config::unified::DEFAULT_UNIFIED_CONFIG)
49        .map_err(|e| {
50            anyhow::anyhow!(
51                "Failed to create config file {}: {}",
52                global_path.display(),
53                e
54            )
55        })?;
56
57    println!(
58        "{}Created unified config: {}{}{}\n",
59        colors.green(),
60        colors.bold(),
61        global_path.display(),
62        colors.reset()
63    );
64    println!("This is the primary configuration file for Ralph.");
65    println!();
66    println!("Features:");
67    println!("  - General settings (verbosity, iterations, etc.)");
68    println!("  - CCS aliases for Claude Code Switch integration");
69    println!("  - Custom agent definitions");
70    println!("  - Agent chain configuration with fallbacks");
71    println!();
72    println!("Environment variables (RALPH_*) override these settings.");
73    println!();
74    println!("Next steps:");
75    println!("  1. Create a PROMPT.md for your task:");
76    println!("       ralph --init <work-guide>");
77    println!("       ralph --list-work-guides  # Show all Work Guides");
78    println!("  2. Or run ralph directly with default settings:");
79    println!("       ralph \"your commit message\"");
80    Ok(true)
81}
82
83/// Handle the `--init-global` flag using the default path resolver.
84///
85/// Creates a unified config file at `~/.config/ralph-workflow.toml` if it doesn't exist.
86/// This is a convenience wrapper that uses [`RealConfigEnvironment`] internally.
87///
88/// # Arguments
89///
90/// * `colors` - Terminal color configuration for output
91///
92/// # Returns
93///
94/// Returns `Ok(true)` if the flag was handled (program should exit after),
95/// or an error if config creation failed.
96pub fn handle_init_global(colors: Colors) -> anyhow::Result<bool> {
97    handle_init_global_with(colors, &RealConfigEnvironment)
98}
99
100/// Create a minimal default PROMPT.md content.
101fn create_minimal_prompt_md() -> String {
102    "# Task Description
103
104Describe what you want the AI agents to implement.
105
106## Example
107
108\"Fix the typo in the README file\"
109
110## Context
111
112Provide any relevant context about the task:
113- What problem are you trying to solve?
114- What are the acceptance criteria?
115- Are there any specific requirements or constraints?
116
117## Notes
118
119- This is a minimal PROMPT.md created by `ralph --init`
120- You can edit this file directly or use `ralph --init <work-guide>` to start from a Work Guide
121- Run `ralph --list-work-guides` to see all available Work Guides
122"
123    .to_string()
124}
125
126// ============================================================================
127// Environment-aware versions of init handlers
128// ============================================================================
129// These versions accept a ConfigEnvironment for dependency injection,
130// enabling tests to use in-memory file storage instead of real filesystem.
131
132/// Create PROMPT.md from a template at the specified path.
133fn create_prompt_from_template<R: ConfigEnvironment>(
134    template_name: &str,
135    prompt_path: &Path,
136    force: bool,
137    colors: Colors,
138    env: &R,
139) -> anyhow::Result<bool> {
140    // Validate the template exists first, before any file operations
141    let Some(template) = get_template(template_name) else {
142        println!(
143            "{}Unknown Work Guide: '{}'{}",
144            colors.red(),
145            template_name,
146            colors.reset()
147        );
148        println!();
149        let similar = find_similar_templates(template_name);
150        if !similar.is_empty() {
151            println!("{}Did you mean?{}", colors.yellow(), colors.reset());
152            for (name, score) in similar {
153                println!(
154                    "  {}{}{}  ({}% similar)",
155                    colors.cyan(),
156                    name,
157                    colors.reset(),
158                    score
159                );
160            }
161            println!();
162        }
163        println!("Commonly used Work Guides:");
164        print_common_work_guides(colors);
165        println!("Usage: ralph --init <work-guide>");
166        return Ok(true);
167    };
168
169    let content = template.content();
170
171    // Check if file exists using the environment
172    let file_exists = env.file_exists(prompt_path);
173
174    if force || !file_exists {
175        // Write file using the environment
176        env.write_file(prompt_path, content)?;
177    } else {
178        // File exists and not forcing - check if we can prompt
179        if can_prompt_user() {
180            if !prompt_overwrite_confirmation(prompt_path, colors)? {
181                return Ok(true);
182            }
183            env.write_file(prompt_path, content)?;
184        } else {
185            return Err(anyhow::anyhow!(
186                "PROMPT.md already exists: {}\nRefusing to overwrite in non-interactive mode. Use --force-overwrite to overwrite, or delete/backup the existing file.",
187                prompt_path.display()
188            ));
189        }
190    }
191
192    println!(
193        "{}Created PROMPT.md from template: {}{}{}",
194        colors.green(),
195        colors.bold(),
196        template_name,
197        colors.reset()
198    );
199    println!();
200    println!(
201        "Template: {}{}{}  {}",
202        colors.cyan(),
203        template.name(),
204        colors.reset(),
205        template.description()
206    );
207    println!();
208    println!("Next steps:");
209    println!("  1. Edit PROMPT.md with your task details");
210    println!("  2. Run: ralph \"your commit message\"");
211    println!();
212    println!("Tip: Use --list-work-guides to see all available Work Guides.");
213
214    Ok(true)
215}
216
217/// Handle --init with template argument using the provided environment.
218fn handle_init_template_arg_at_path_with_env<R: ConfigEnvironment>(
219    template_name: &str,
220    prompt_path: &Path,
221    force: bool,
222    colors: Colors,
223    env: &R,
224) -> anyhow::Result<bool> {
225    if get_template(template_name).is_some() {
226        return create_prompt_from_template(template_name, prompt_path, force, colors, env);
227    }
228
229    // Unknown value - show helpful error with suggestions
230    println!(
231        "{}Unknown Work Guide: '{}'{}",
232        colors.red(),
233        template_name,
234        colors.reset()
235    );
236    println!();
237
238    // Try to find similar template names
239    let similar = find_similar_templates(template_name);
240    if !similar.is_empty() {
241        println!("{}Did you mean?{}", colors.yellow(), colors.reset());
242        for (name, score) in similar {
243            println!(
244                "  {}{}{}  ({}% similar)",
245                colors.cyan(),
246                name,
247                colors.reset(),
248                score
249            );
250        }
251        println!();
252    }
253
254    println!("Commonly used Work Guides:");
255    print_common_work_guides(colors);
256    println!("Usage: ralph --init=<work-guide>");
257    println!("       ralph --init            # Smart init (infers intent)");
258    Ok(true)
259}
260
261/// Handle --init with smart inference using the provided environment.
262fn handle_init_state_inference_with_env<R: ConfigEnvironment>(
263    config_path: &std::path::Path,
264    prompt_path: &Path,
265    config_exists: bool,
266    prompt_exists: bool,
267    force: bool,
268    colors: Colors,
269    env: &R,
270) -> anyhow::Result<bool> {
271    match (config_exists, prompt_exists) {
272        (false, false) => handle_init_none_exist_with_env(config_path, colors, env),
273        (true, false) => Ok(handle_init_only_config_exists_with_env(
274            config_path,
275            prompt_path,
276            force,
277            colors,
278            env,
279        )),
280        (false, true) => handle_init_only_prompt_exists_with_env(colors, env),
281        (true, true) => Ok(handle_init_both_exist(
282            config_path,
283            prompt_path,
284            force,
285            colors,
286        )),
287    }
288}
289
290/// Handle --init when neither config nor PROMPT.md exists, using the provided environment.
291fn handle_init_none_exist_with_env<R: ConfigEnvironment>(
292    _config_path: &std::path::Path,
293    colors: Colors,
294    env: &R,
295) -> anyhow::Result<bool> {
296    println!(
297        "{}No config found. Creating unified config...{}",
298        colors.dim(),
299        colors.reset()
300    );
301    println!();
302    handle_init_global_with(colors, env)?;
303    Ok(true)
304}
305
306/// Handle --init when only config exists (no PROMPT.md), using the provided environment.
307fn handle_init_only_config_exists_with_env<R: ConfigEnvironment>(
308    config_path: &std::path::Path,
309    prompt_path: &Path,
310    force: bool,
311    colors: Colors,
312    env: &R,
313) -> bool {
314    println!(
315        "{}Config found at:{} {}",
316        colors.green(),
317        colors.reset(),
318        config_path.display()
319    );
320    println!(
321        "{}PROMPT.md not found in current directory.{}",
322        colors.yellow(),
323        colors.reset()
324    );
325    println!();
326
327    // Show common Work Guides inline
328    print_common_work_guides(colors);
329
330    // Check if we're in a TTY for interactive prompting
331    if can_prompt_user() {
332        // Interactive mode: prompt for template selection
333        if let Some(template_name) = prompt_for_template(colors) {
334            match create_prompt_from_template(&template_name, prompt_path, force, colors, env) {
335                Ok(_) => return true,
336                Err(e) => {
337                    println!(
338                        "{}Failed to create PROMPT.md: {}{}",
339                        colors.red(),
340                        e,
341                        colors.reset()
342                    );
343                    return true;
344                }
345            }
346        }
347        // User declined or entered invalid input, fall through to show usage
348    } else {
349        // Non-interactive mode: create a minimal default PROMPT.md
350        let default_content = create_minimal_prompt_md();
351
352        // Check if file exists using the environment
353        if env.file_exists(prompt_path) {
354            println!(
355                "{}PROMPT.md already exists:{} {}",
356                colors.yellow(),
357                colors.reset(),
358                prompt_path.display()
359            );
360            println!("Use --force-overwrite to overwrite, or delete/backup the existing file.");
361            return true;
362        }
363
364        // Write file using the environment
365        match env.write_file(prompt_path, &default_content) {
366            Ok(()) => {
367                println!(
368                    "{}Created minimal PROMPT.md{}",
369                    colors.green(),
370                    colors.reset()
371                );
372                println!();
373                println!("Next steps:");
374                println!("  1. Edit PROMPT.md with your task details");
375                println!("  2. Run: ralph \"your commit message\"");
376                println!();
377                println!("Tip: Use ralph --list-work-guides to see all available Work Guides.");
378                return true;
379            }
380            Err(e) => {
381                println!(
382                    "{}Failed to create PROMPT.md: {}{}",
383                    colors.red(),
384                    e,
385                    colors.reset()
386                );
387                return true;
388            }
389        }
390    }
391
392    // Show template list if we didn't create PROMPT.md
393    println!("Create a PROMPT.md from a Work Guide to get started:");
394    println!();
395
396    for (name, description) in list_templates() {
397        println!(
398            "  {}{}{}  {}{}{}",
399            colors.cyan(),
400            name,
401            colors.reset(),
402            colors.dim(),
403            description,
404            colors.reset()
405        );
406    }
407
408    println!();
409    println!("Usage: ralph --init <work-guide>");
410    println!();
411    println!("Example:");
412    println!("  ralph --init bug-fix");
413    println!("  ralph --init feature-spec");
414    true
415}
416
417/// Handle --init when only PROMPT.md exists (no config), using the provided environment.
418fn handle_init_only_prompt_exists_with_env<R: ConfigEnvironment>(
419    colors: Colors,
420    env: &R,
421) -> anyhow::Result<bool> {
422    println!(
423        "{}PROMPT.md found in current directory.{}",
424        colors.green(),
425        colors.reset()
426    );
427    println!(
428        "{}No config found. Creating unified config...{}",
429        colors.dim(),
430        colors.reset()
431    );
432    println!();
433    handle_init_global_with(colors, env)?;
434    Ok(true)
435}