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::io::IsTerminal;
13use std::path::Path;
14
15/// Minimum similarity threshold for suggesting alternatives (0-100 percentage).
16const MIN_SIMILARITY_PERCENT: u32 = 40;
17
18/// Handle the `--init-global` flag.
19///
20/// Creates a unified config file at `~/.config/ralph-workflow.toml` if it doesn't exist.
21/// This is the recommended way to configure Ralph globally.
22///
23/// # Arguments
24///
25/// * `colors` - Terminal color configuration for output
26///
27/// # Returns
28///
29/// Returns `Ok(true)` if the flag was handled (program should exit after),
30/// or an error if config creation failed.
31pub fn handle_init_global(colors: Colors) -> anyhow::Result<bool> {
32    let global_path = unified_config_path()
33        .ok_or_else(|| anyhow::anyhow!("Cannot determine config directory (no home directory)"))?;
34
35    match UnifiedConfig::ensure_config_exists() {
36        Ok(UnifiedConfigInitResult::Created) => {
37            println!(
38                "{}Created unified config: {}{}{}\n",
39                colors.green(),
40                colors.bold(),
41                global_path.display(),
42                colors.reset()
43            );
44            println!("This is the primary configuration file for Ralph.");
45            println!();
46            println!("Features:");
47            println!("  - General settings (verbosity, iterations, etc.)");
48            println!("  - CCS aliases for Claude Code Switch integration");
49            println!("  - Custom agent definitions");
50            println!("  - Agent chain configuration with fallbacks");
51            println!();
52            println!("Environment variables (RALPH_*) override these settings.");
53            println!();
54            println!("Next steps:");
55            println!("  1. Create a PROMPT.md for your task:");
56            println!("       ralph --init <work-guide>");
57            println!("       ralph --list-templates  # Show all Work Guides");
58            println!("  2. Or run ralph directly with default settings:");
59            println!("       ralph \"your commit message\"");
60            Ok(true)
61        }
62        Ok(UnifiedConfigInitResult::AlreadyExists) => {
63            println!(
64                "{}Unified config already exists:{} {}",
65                colors.yellow(),
66                colors.reset(),
67                global_path.display()
68            );
69            println!("Edit the file to customize, or delete it to regenerate from defaults.");
70            println!();
71            println!("Next steps:");
72            println!("  1. Create a PROMPT.md for your task:");
73            println!("       ralph --init <work-guide>");
74            println!("       ralph --list-templates  # Show all Work Guides");
75            println!("  2. Or run ralph directly with default settings:");
76            println!("       ralph \"your commit message\"");
77            Ok(true)
78        }
79        Err(e) => Err(anyhow::anyhow!(
80            "Failed to create config file {}: {}",
81            global_path.display(),
82            e
83        )),
84    }
85}
86
87/// Handle the legacy `--init-legacy` flag.
88///
89/// Creates a local agents.toml file at the specified path if it doesn't exist.
90pub fn handle_init_legacy(colors: Colors, agents_config_path: &Path) -> anyhow::Result<bool> {
91    match AgentsConfigFile::ensure_config_exists(agents_config_path) {
92        Ok(ConfigInitResult::Created) => {
93            println!(
94                "{}Created {}{}{}\n",
95                colors.green(),
96                colors.bold(),
97                agents_config_path.display(),
98                colors.reset()
99            );
100            println!("Edit the file to customize agent configurations, then run ralph again.");
101            println!("Or run ralph now to use the default settings.");
102            Ok(true)
103        }
104        Ok(ConfigInitResult::AlreadyExists) => {
105            println!(
106                "{}Config file already exists:{} {}",
107                colors.yellow(),
108                colors.reset(),
109                agents_config_path.display()
110            );
111            println!("Edit the file to customize, or delete it to regenerate from defaults.");
112            Ok(true)
113        }
114        Err(e) => Err(anyhow::anyhow!(
115            "Failed to create config file {}: {}",
116            agents_config_path.display(),
117            e
118        )),
119    }
120}
121
122// NOTE: legacy per-repo agents.toml creation is handled by `--init-legacy` only.
123
124/// Handle the `--init-prompt` flag.
125///
126/// Creates a PROMPT.md file from the specified template.
127///
128/// # Arguments
129///
130/// * `template_name` - The name of the template to use
131/// * `colors` - Terminal color configuration for output
132///
133/// # Returns
134///
135/// Returns `Ok(true)` if the flag was handled (program should exit after),
136/// or an error if template creation failed.
137pub fn handle_init_prompt(template_name: &str, colors: Colors) -> anyhow::Result<bool> {
138    let prompt_path = Path::new("PROMPT.md");
139
140    // Check if PROMPT.md already exists
141    if prompt_path.exists() {
142        println!(
143            "{}PROMPT.md already exists:{} {}",
144            colors.yellow(),
145            colors.reset(),
146            prompt_path.display()
147        );
148        println!("Delete or backup the existing file to create a new one from a template.");
149        return Ok(true);
150    }
151
152    // Validate the template exists
153    let Some(template) = get_template(template_name) else {
154        println!(
155            "{}Unknown template: '{}'{}",
156            colors.red(),
157            template_name,
158            colors.reset()
159        );
160        println!();
161        println!("Available templates:");
162        for (name, description) in list_templates() {
163            println!(
164                "  {}{}{}  {}",
165                colors.cyan(),
166                name,
167                colors.reset(),
168                description
169            );
170        }
171        println!();
172        println!("Usage: ralph --init-prompt <template>");
173        println!("       ralph --list-templates");
174        return Ok(true);
175    };
176
177    // Write the template content to PROMPT.md
178    let content = template.content();
179    fs::write(prompt_path, content)?;
180
181    println!(
182        "{}Created PROMPT.md from template: {}{}{}",
183        colors.green(),
184        colors.bold(),
185        template_name,
186        colors.reset()
187    );
188    println!();
189    println!(
190        "Template: {}{}{}  {}",
191        colors.cyan(),
192        template.name(),
193        colors.reset(),
194        template.description()
195    );
196    println!();
197    println!("Next steps:");
198    println!("  1. Edit PROMPT.md with your task details");
199    println!("  2. Run: ralph \"your commit message\"");
200    println!();
201    println!("Tip: Use --list-templates to see all available templates.");
202
203    Ok(true)
204}
205
206/// Print a template category section.
207///
208/// Helper function to reduce the length of `handle_list_templates`.
209fn print_template_category(category_name: &str, templates: &[(&str, &str)], colors: Colors) {
210    println!("{}{}:{}", colors.bold(), category_name, colors.reset());
211    for (name, description) in templates {
212        println!(
213            "  {}{}{}  {}",
214            colors.cyan(),
215            name,
216            colors.reset(),
217            description
218        );
219    }
220    println!();
221}
222
223/// Handle the `--list-templates` flag.
224///
225/// Lists all available PROMPT.md templates with descriptions, organized by category.
226///
227/// # Arguments
228///
229/// * `colors` - Terminal color configuration for output
230///
231/// # Returns
232///
233/// Returns `true` if the flag was handled (program should exit after).
234pub fn handle_list_templates(colors: Colors) -> bool {
235    println!("PROMPT.md Work Guides (use: ralph --init <template>)");
236    println!();
237
238    // Common templates (most frequently used)
239    print_template_category(
240        "Common Templates",
241        &[
242            ("quick", "Quick/small changes (typos, minor fixes)"),
243            ("bug-fix", "Bug fix with investigation guidance"),
244            ("feature-spec", "Comprehensive product specification"),
245            ("refactor", "Code refactoring with behavior preservation"),
246        ],
247        colors,
248    );
249
250    // Testing and documentation
251    print_template_category(
252        "Testing & Documentation",
253        &[
254            ("test", "Test writing with edge case considerations"),
255            ("docs", "Documentation update with completeness checklist"),
256            ("code-review", "Structured code review for pull requests"),
257        ],
258        colors,
259    );
260
261    // Specialized development
262    print_template_category(
263        "Specialized Development",
264        &[
265            ("cli-tool", "CLI tool with argument parsing and completion"),
266            ("web-api", "REST/HTTP API with error handling"),
267            (
268                "ui-component",
269                "UI component with accessibility and responsive design",
270            ),
271            ("onboarding", "Learn a new codebase efficiently"),
272        ],
273        colors,
274    );
275
276    // Advanced/Infrastructure
277    print_template_category(
278        "Advanced & Infrastructure",
279        &[
280            (
281                "performance-optimization",
282                "Performance optimization with benchmarking",
283            ),
284            (
285                "security-audit",
286                "Security audit with OWASP Top 10 coverage",
287            ),
288            (
289                "api-integration",
290                "API integration with retry logic and resilience",
291            ),
292            (
293                "database-migration",
294                "Database migration with zero-downtime strategies",
295            ),
296            (
297                "dependency-update",
298                "Dependency update with breaking change handling",
299            ),
300            ("data-pipeline", "Data pipeline with ETL and monitoring"),
301        ],
302        colors,
303    );
304
305    // Maintenance
306    print_template_category(
307        "Maintenance & Operations",
308        &[
309            (
310                "debug-triage",
311                "Systematic issue investigation and diagnosis",
312            ),
313            (
314                "tech-debt",
315                "Technical debt refactoring with prioritization",
316            ),
317            (
318                "release",
319                "Release preparation with versioning and changelog",
320            ),
321        ],
322        colors,
323    );
324
325    println!("Usage: ralph --init <template>");
326    println!("       ralph --init-prompt <template>");
327    println!();
328    println!("Example:");
329    println!("  ralph --init bug-fix              # Create bug fix template");
330    println!("  ralph --init feature-spec         # Create feature spec template");
331    println!("  ralph --init quick                # Create quick change template");
332    println!();
333    println!("{}Tip:{}", colors.yellow(), colors.reset());
334    println!("  Use --init without a value to auto-detect what you need.");
335    println!("  Run ralph --help to understand the difference between Task Templates");
336    println!("  (for PROMPT.md) and System Prompts (backend AI configuration).");
337
338    true
339}
340
341/// Handle the smart `--init` flag.
342///
343/// This function intelligently determines what the user wants to initialize:
344/// - If a value is provided and matches a known template name → create PROMPT.md
345/// - If config doesn't exist and no template specified → create config
346/// - If config exists but PROMPT.md doesn't → prompt to create PROMPT.md
347/// - If both exist → show helpful message about what's already set up
348///
349/// # Arguments
350///
351/// * `template_arg` - Optional template name from `--init=TEMPLATE`
352/// * `colors` - Terminal color configuration for output
353///
354/// # Returns
355///
356/// Returns `Ok(true)` if the flag was handled (program should exit after),
357/// or `Ok(false)` if not handled, or an error if initialization failed.
358pub fn handle_smart_init(template_arg: Option<&str>, colors: Colors) -> anyhow::Result<bool> {
359    let config_path = crate::config::unified_config_path()
360        .ok_or_else(|| anyhow::anyhow!("Cannot determine config directory (no home directory)"))?;
361    let prompt_path = Path::new("PROMPT.md");
362
363    let config_exists = config_path.exists();
364    let prompt_exists = prompt_path.exists();
365
366    // If a template name is provided (non-empty), treat it as --init-prompt
367    if let Some(template_name) = template_arg {
368        if !template_name.is_empty() {
369            return handle_init_template_arg(template_name, colors);
370        }
371        // Empty string means --init was used without a value, fall through to smart inference
372    }
373
374    // No template provided - use smart inference based on current state
375    handle_init_state_inference(
376        &config_path,
377        prompt_path,
378        config_exists,
379        prompt_exists,
380        colors,
381    )
382}
383
384/// Calculate Levenshtein distance between two strings.
385///
386/// Returns the minimum number of single-character edits (insertions, deletions,
387/// or substitutions) required to change one string into the other.
388fn levenshtein_distance(a: &str, b: &str) -> usize {
389    let a_chars: Vec<char> = a.chars().collect();
390    let b_chars: Vec<char> = b.chars().collect();
391    let b_len = b_chars.len();
392
393    // Use two rows to save memory
394    let mut prev_row: Vec<usize> = (0..=b_len).collect();
395    let mut curr_row = vec![0; b_len + 1];
396
397    for (i, a_char) in a_chars.iter().enumerate() {
398        curr_row[0] = i + 1;
399
400        for (j, b_char) in b_chars.iter().enumerate() {
401            let cost = usize::from(a_char != b_char);
402            curr_row[j + 1] = std::cmp::min(
403                std::cmp::min(
404                    curr_row[j] + 1,     // deletion
405                    prev_row[j + 1] + 1, // insertion
406                ),
407                prev_row[j] + cost, // substitution
408            );
409        }
410
411        std::mem::swap(&mut prev_row, &mut curr_row);
412    }
413
414    prev_row[b_len]
415}
416
417/// Calculate similarity score as a percentage (0-100).
418///
419/// This avoids floating point comparison issues in tests.
420fn similarity_percentage(a: &str, b: &str) -> u32 {
421    if a == b {
422        return 100;
423    }
424    if a.is_empty() || b.is_empty() {
425        return 0;
426    }
427
428    let max_len = a.len().max(b.len());
429    let distance = levenshtein_distance(a, b);
430
431    if max_len == 0 {
432        return 100;
433    }
434
435    // Calculate percentage without floating point
436    // (100 * (max_len - distance)) / max_len
437    let diff = max_len.saturating_sub(distance);
438    // The division result is guaranteed to fit in u32 since it's ≤ 100
439    u32::try_from((100 * diff) / max_len).unwrap_or(0)
440}
441
442/// Find the best matching template names using fuzzy matching.
443///
444/// Returns templates that are similar to the input within the threshold.
445fn find_similar_templates(input: &str) -> Vec<(&'static str, u32)> {
446    let input_lower = input.to_lowercase();
447    let mut matches: Vec<(&'static str, u32)> = ALL_TEMPLATES
448        .iter()
449        .map(|t| {
450            let name = t.name();
451            let sim = similarity_percentage(&input_lower, &name.to_lowercase());
452            (name, sim)
453        })
454        .filter(|(_, sim)| *sim >= MIN_SIMILARITY_PERCENT)
455        .collect();
456
457    // Sort by similarity (highest first)
458    matches.sort_by(|a, b| b.1.cmp(&a.1));
459
460    // Return top 3 matches
461    matches.truncate(3);
462    matches
463}
464
465/// Handle --init when a template name is provided.
466fn handle_init_template_arg(template_name: &str, colors: Colors) -> anyhow::Result<bool> {
467    if get_template(template_name).is_some() {
468        return handle_init_prompt(template_name, colors);
469    }
470
471    // Unknown value - show helpful error with suggestions
472    println!(
473        "{}Unknown Work Guide: '{}'{}",
474        colors.red(),
475        template_name,
476        colors.reset()
477    );
478    println!();
479
480    // Try to find similar template names
481    let similar = find_similar_templates(template_name);
482    if !similar.is_empty() {
483        println!("{}Did you mean?{}", colors.yellow(), colors.reset());
484        for (name, score) in similar {
485            println!(
486                "  {}{}{}  ({}% similar)",
487                colors.cyan(),
488                name,
489                colors.reset(),
490                score
491            );
492        }
493        println!();
494    }
495
496    println!("Available Work Guides:");
497    for (name, description) in list_templates() {
498        println!(
499            "  {}{}{}  {}{}{}",
500            colors.cyan(),
501            name,
502            colors.reset(),
503            colors.dim(),
504            description,
505            colors.reset()
506        );
507    }
508    println!();
509    println!("Usage: ralph --init=<work-guide>");
510    println!("       ralph --init            # Smart init (infers intent)");
511    Ok(true)
512}
513
514/// Handle --init with smart inference based on current state.
515fn handle_init_state_inference(
516    config_path: &std::path::Path,
517    prompt_path: &Path,
518    config_exists: bool,
519    prompt_exists: bool,
520    colors: Colors,
521) -> anyhow::Result<bool> {
522    match (config_exists, prompt_exists) {
523        (false, false) => handle_init_none_exist(config_path, colors),
524        (true, false) => Ok(handle_init_only_config_exists(config_path, colors)),
525        (false, true) => handle_init_only_prompt_exists(colors),
526        (true, true) => Ok(handle_init_both_exist(config_path, prompt_path, colors)),
527    }
528}
529
530/// Handle --init when neither config nor PROMPT.md exists.
531fn handle_init_none_exist(_config_path: &std::path::Path, colors: Colors) -> anyhow::Result<bool> {
532    println!(
533        "{}No config found. Creating unified config...{}",
534        colors.dim(),
535        colors.reset()
536    );
537    println!();
538    handle_init_global(colors)?;
539    Ok(true)
540}
541
542/// Handle --init when only config exists (no PROMPT.md).
543///
544/// When in a TTY, prompts for template selection.
545/// When not in a TTY, creates a minimal default PROMPT.md.
546fn handle_init_only_config_exists(config_path: &std::path::Path, colors: Colors) -> bool {
547    println!(
548        "{}Config found at:{} {}",
549        colors.green(),
550        colors.reset(),
551        config_path.display()
552    );
553    println!(
554        "{}PROMPT.md not found in current directory.{}",
555        colors.yellow(),
556        colors.reset()
557    );
558    println!();
559
560    // Check if we're in a TTY for interactive prompting
561    if std::io::stdin().is_terminal() && std::io::stdout().is_terminal() {
562        // Interactive mode: prompt for template selection
563        if let Some(template_name) = prompt_for_template(colors) {
564            match handle_init_prompt(&template_name, colors) {
565                Ok(_) => return true,
566                Err(e) => {
567                    println!(
568                        "{}Failed to create PROMPT.md: {}{}",
569                        colors.red(),
570                        e,
571                        colors.reset()
572                    );
573                    return true;
574                }
575            }
576        }
577        // User declined or entered invalid input, fall through to show usage
578    } else {
579        // Non-interactive mode: create a minimal default PROMPT.md
580        let default_content = create_minimal_prompt_md();
581        let prompt_path = Path::new("PROMPT.md");
582
583        match fs::write(prompt_path, default_content) {
584            Ok(()) => {
585                println!(
586                    "{}Created minimal PROMPT.md{}",
587                    colors.green(),
588                    colors.reset()
589                );
590                println!();
591                println!("Next steps:");
592                println!("  1. Edit PROMPT.md with your task details");
593                println!("  2. Run: ralph \"your commit message\"");
594                println!();
595                println!("Tip: Use ralph --list-templates to see all available Work Guides.");
596                return true;
597            }
598            Err(e) => {
599                println!(
600                    "{}Failed to create PROMPT.md: {}{}",
601                    colors.red(),
602                    e,
603                    colors.reset()
604                );
605                return true;
606            }
607        }
608    }
609
610    // Show template list if we didn't create PROMPT.md
611    println!("Create a PROMPT.md from a Work Guide to get started:");
612    println!();
613
614    for (name, description) in list_templates() {
615        println!(
616            "  {}{}{}  {}{}{}",
617            colors.cyan(),
618            name,
619            colors.reset(),
620            colors.dim(),
621            description,
622            colors.reset()
623        );
624    }
625
626    println!();
627    println!("Usage: ralph --init <work-guide>");
628    println!("       ralph --init-prompt <work-guide>");
629    println!();
630    println!("Example:");
631    println!("  ralph --init bug-fix");
632    println!("  ralph --init feature-spec");
633    true
634}
635
636/// Prompt the user to select a template interactively.
637///
638/// Returns `Some(template_name)` if the user selected a template,
639/// or `None` if the user declined or entered invalid input.
640fn prompt_for_template(colors: Colors) -> Option<String> {
641    use std::io::{self, Write};
642
643    println!("PROMPT.md contains your task specification for the AI agents.");
644    print!("Would you like to create one from a template? [Y/n]: ");
645    if io::stdout().flush().is_err() {
646        return None;
647    }
648
649    let mut input = String::new();
650    match io::stdin().read_line(&mut input) {
651        Ok(0) | Err(_) => return None,
652        Ok(_) => {}
653    }
654
655    let response = input.trim().to_lowercase();
656    if response == "n" || response == "no" || response == "skip" {
657        return None;
658    }
659
660    // Show available templates
661    println!();
662    println!("Available templates:");
663
664    let templates: Vec<(&str, &str)> = list_templates();
665    for (i, (name, description)) in templates.iter().enumerate() {
666        println!(
667            "  {}{}{}  {}{}{}",
668            colors.cyan(),
669            name,
670            colors.reset(),
671            colors.dim(),
672            description,
673            colors.reset()
674        );
675        if (i + 1) % 5 == 0 {
676            println!(); // Group templates in sets of 5 for readability
677        }
678    }
679
680    println!();
681    println!("Common choices:");
682    println!(
683        "  {}quick{}           - Quick/small changes (typos, minor fixes)",
684        colors.cyan(),
685        colors.reset()
686    );
687    println!(
688        "  {}bug-fix{}         - Bug fix with investigation guidance",
689        colors.cyan(),
690        colors.reset()
691    );
692    println!(
693        "  {}feature-spec{}    - Product specification",
694        colors.cyan(),
695        colors.reset()
696    );
697    println!();
698    print!("Enter template name (or press Enter to use 'quick'): ");
699    if io::stdout().flush().is_err() {
700        return None;
701    }
702
703    let mut template_input = String::new();
704    match io::stdin().read_line(&mut template_input) {
705        Ok(0) | Err(_) => return None,
706        Ok(_) => {}
707    }
708
709    let template_name = template_input.trim();
710    if template_name.is_empty() {
711        // Default to 'quick' template
712        return Some("quick".to_string());
713    }
714
715    // Validate the template exists
716    if get_template(template_name).is_some() {
717        Some(template_name.to_string())
718    } else {
719        println!(
720            "{}Unknown template: '{}'{}",
721            colors.red(),
722            template_name,
723            colors.reset()
724        );
725        println!("Run 'ralph --list-templates' to see all available templates.");
726        None
727    }
728}
729
730/// Create a minimal default PROMPT.md content.
731fn create_minimal_prompt_md() -> String {
732    "# Task Description
733
734Describe what you want the AI agents to implement.
735
736## Example
737
738\"Fix the typo in the README file\"
739
740## Context
741
742Provide any relevant context about the task:
743- What problem are you trying to solve?
744- What are the acceptance criteria?
745- Are there any specific requirements or constraints?
746
747## Notes
748
749- This is a minimal PROMPT.md created by `ralph --init`
750- You can edit this file directly or use `ralph --init <work-guide>` to start from a Work Guide
751- Run `ralph --list-templates` to see all available Work Guides
752"
753    .to_string()
754}
755
756/// Handle --init when only PROMPT.md exists (no config).
757fn handle_init_only_prompt_exists(colors: Colors) -> anyhow::Result<bool> {
758    println!(
759        "{}PROMPT.md found in current directory.{}",
760        colors.green(),
761        colors.reset()
762    );
763    println!(
764        "{}No config found. Creating unified config...{}",
765        colors.dim(),
766        colors.reset()
767    );
768    println!();
769    handle_init_global(colors)?;
770    Ok(true)
771}
772
773/// Handle --init when both config and PROMPT.md exist.
774fn handle_init_both_exist(
775    config_path: &std::path::Path,
776    prompt_path: &Path,
777    colors: Colors,
778) -> bool {
779    println!("{}Setup complete!{}", colors.green(), colors.reset());
780    println!();
781    println!(
782        "  Config: {}{}{}",
783        colors.dim(),
784        config_path.display(),
785        colors.reset()
786    );
787    println!(
788        "  PROMPT: {}{}{}",
789        colors.dim(),
790        prompt_path.display(),
791        colors.reset()
792    );
793    println!();
794    println!("You're ready to run Ralph:");
795    println!("  ralph \"your commit message\"");
796    println!();
797    println!("Other commands:");
798    println!("  ralph --list-templates    # Show all Work Guides");
799    println!("  ralph --init=<template>    # Create new PROMPT.md from Work Guide");
800    true
801}
802
803#[cfg(test)]
804mod tests {
805    use super::*;
806
807    #[test]
808    fn test_handle_smart_init_with_valid_template() {
809        // When a valid template name is provided, it should delegate to handle_init_prompt
810        let colors = Colors::new();
811        let result = handle_smart_init(Some("bug-fix"), colors);
812
813        // We expect this to return Ok(true) since it handles the init
814        // The actual test would need to mock file system operations
815        assert!(result.is_ok());
816    }
817
818    #[test]
819    fn test_handle_smart_init_with_invalid_template() {
820        // When an invalid template name is provided, it should show an error
821        let colors = Colors::new();
822        let result = handle_smart_init(Some("nonexistent-template"), colors);
823
824        // Should still return Ok(true) since it handled the request (showed error)
825        assert!(result.is_ok());
826    }
827
828    #[test]
829    fn test_handle_smart_init_no_arg() {
830        // When no argument is provided, it should check the current state
831        let colors = Colors::new();
832        let result = handle_smart_init(None, colors);
833
834        // Should return Ok(something) depending on the state of config/PROMPT.md
835        assert!(result.is_ok());
836    }
837
838    #[test]
839    fn test_template_name_validation() {
840        // Test that we can validate template names
841        assert!(get_template("bug-fix").is_some());
842        assert!(get_template("feature-spec").is_some());
843        assert!(get_template("refactor").is_some());
844        assert!(get_template("test").is_some());
845        assert!(get_template("docs").is_some());
846        assert!(get_template("quick").is_some());
847
848        // Invalid template names
849        assert!(get_template("invalid").is_none());
850        assert!(get_template("").is_none());
851    }
852
853    #[test]
854    fn test_levenshtein_distance() {
855        // Exact match
856        assert_eq!(levenshtein_distance("test", "test"), 0);
857
858        // One edit
859        assert_eq!(levenshtein_distance("test", "tast"), 1);
860        assert_eq!(levenshtein_distance("test", "tests"), 1);
861        assert_eq!(levenshtein_distance("test", "est"), 1);
862
863        // Two edits
864        assert_eq!(levenshtein_distance("test", "taste"), 2);
865        assert_eq!(levenshtein_distance("test", "best"), 1);
866
867        // Completely different
868        assert_eq!(levenshtein_distance("abc", "xyz"), 3);
869    }
870
871    #[test]
872    fn test_similarity() {
873        // Exact match
874        assert_eq!(similarity_percentage("test", "test"), 100);
875
876        // Similar strings - should be high similarity
877        assert!(similarity_percentage("bug-fix", "bugfix") > 80);
878        assert!(similarity_percentage("feature-spec", "feature") > 50);
879
880        // Different strings - should be low similarity
881        assert!(similarity_percentage("test", "xyz") < 50);
882
883        // Empty strings
884        assert_eq!(similarity_percentage("", ""), 100);
885        assert_eq!(similarity_percentage("test", ""), 0);
886        assert_eq!(similarity_percentage("", "test"), 0);
887    }
888
889    #[test]
890    fn test_find_similar_templates() {
891        // Find similar to "bugfix" (missing hyphen)
892        let similar = find_similar_templates("bugfix");
893        assert!(!similar.is_empty());
894        assert!(similar.iter().any(|(name, _)| *name == "bug-fix"));
895
896        // Find similar to "feature" (should match feature-spec)
897        let similar = find_similar_templates("feature");
898        assert!(!similar.is_empty());
899        assert!(similar.iter().any(|(name, _)| name.contains("feature")));
900
901        // Very different string should return empty or low similarity
902        let similar = find_similar_templates("xyzabc");
903        // Either empty or all matches have low similarity
904        assert!(similar.is_empty() || similar.iter().all(|(_, sim)| *sim < 50));
905    }
906}