git_iris/
commands.rs

1use crate::common::CommonParams;
2use crate::config::Config;
3use crate::instruction_presets::{
4    PresetType, get_instruction_preset_library, list_presets_formatted_by_type,
5};
6use crate::log_debug;
7use crate::providers::{Provider, ProviderConfig};
8use crate::ui;
9use anyhow::Context;
10use anyhow::{Result, anyhow};
11use colored::Colorize;
12use std::collections::HashMap;
13
14/// Helper to get themed colors for terminal output
15mod colors {
16    use crate::theme;
17
18    pub fn accent_primary() -> (u8, u8, u8) {
19        let c = theme::current().color("accent.primary");
20        (c.r, c.g, c.b)
21    }
22
23    pub fn accent_secondary() -> (u8, u8, u8) {
24        let c = theme::current().color("accent.secondary");
25        (c.r, c.g, c.b)
26    }
27
28    pub fn accent_tertiary() -> (u8, u8, u8) {
29        let c = theme::current().color("accent.tertiary");
30        (c.r, c.g, c.b)
31    }
32
33    pub fn warning() -> (u8, u8, u8) {
34        let c = theme::current().color("warning");
35        (c.r, c.g, c.b)
36    }
37
38    pub fn success() -> (u8, u8, u8) {
39        let c = theme::current().color("success");
40        (c.r, c.g, c.b)
41    }
42
43    pub fn text_secondary() -> (u8, u8, u8) {
44        let c = theme::current().color("text.secondary");
45        (c.r, c.g, c.b)
46    }
47
48    pub fn text_dim() -> (u8, u8, u8) {
49        let c = theme::current().color("text.dim");
50        (c.r, c.g, c.b)
51    }
52}
53
54/// Apply common configuration changes to a config object
55/// Returns true if any changes were made
56///
57/// This centralized function handles changes to configuration objects, used by both
58/// personal and project configuration commands.
59///
60/// # Arguments
61///
62/// * `config` - The configuration object to modify
63/// * `common` - Common parameters from command line
64/// * `model` - Optional model to set for the selected provider
65/// * `token_limit` - Optional token limit to set
66/// * `param` - Optional additional parameters to set
67/// * `api_key` - Optional API key to set (ignored in project configs)
68///
69/// # Returns
70///
71/// Boolean indicating if any changes were made to the configuration
72fn apply_config_changes(
73    config: &mut Config,
74    common: &CommonParams,
75    model: Option<String>,
76    fast_model: Option<String>,
77    token_limit: Option<usize>,
78    param: Option<Vec<String>>,
79    api_key: Option<String>,
80    subagent_timeout: Option<u64>,
81) -> anyhow::Result<bool> {
82    let mut changes_made = false;
83
84    // Apply common parameters to the config and track if changes were made
85    let common_changes = common.apply_to_config(config)?;
86    changes_made |= common_changes;
87
88    // Handle provider change - validate and insert if needed
89    if let Some(provider_str) = &common.provider {
90        let provider: Provider = provider_str.parse().map_err(|_| {
91            anyhow!(
92                "Invalid provider: {}. Available: {}",
93                provider_str,
94                Provider::all_names().join(", ")
95            )
96        })?;
97
98        // Only check for provider insertion if it wasn't already handled
99        if !config.providers.contains_key(provider.name()) {
100            config.providers.insert(
101                provider.name().to_string(),
102                ProviderConfig::with_defaults(provider),
103            );
104            changes_made = true;
105        }
106    }
107
108    let provider_config = config
109        .providers
110        .get_mut(&config.default_provider)
111        .context("Could not get default provider")?;
112
113    // Apply API key if provided
114    if let Some(key) = api_key
115        && provider_config.api_key != key
116    {
117        provider_config.api_key = key;
118        changes_made = true;
119    }
120
121    // Apply model change
122    if let Some(model) = model
123        && provider_config.model != model
124    {
125        provider_config.model = model;
126        changes_made = true;
127    }
128
129    // Apply fast model change
130    if let Some(fast_model) = fast_model
131        && provider_config.fast_model != Some(fast_model.clone())
132    {
133        provider_config.fast_model = Some(fast_model);
134        changes_made = true;
135    }
136
137    // Apply parameter changes
138    if let Some(params) = param {
139        let additional_params = parse_additional_params(&params);
140        if provider_config.additional_params != additional_params {
141            provider_config.additional_params = additional_params;
142            changes_made = true;
143        }
144    }
145
146    // Apply gitmoji setting
147    if let Some(use_gitmoji) = common.resolved_gitmoji()
148        && config.use_gitmoji != use_gitmoji
149    {
150        config.use_gitmoji = use_gitmoji;
151        changes_made = true;
152    }
153
154    // Apply instructions
155    if let Some(instr) = &common.instructions
156        && config.instructions != *instr
157    {
158        config.instructions.clone_from(instr);
159        changes_made = true;
160    }
161
162    // Apply token limit
163    if let Some(limit) = token_limit
164        && provider_config.token_limit != Some(limit)
165    {
166        provider_config.token_limit = Some(limit);
167        changes_made = true;
168    }
169
170    // Apply preset
171    if let Some(preset) = &common.preset {
172        let preset_library = get_instruction_preset_library();
173        if preset_library.get_preset(preset).is_some() {
174            if config.instruction_preset != *preset {
175                config.instruction_preset.clone_from(preset);
176                changes_made = true;
177            }
178        } else {
179            return Err(anyhow!("Invalid preset: {}", preset));
180        }
181    }
182
183    // Apply subagent timeout
184    if let Some(timeout) = subagent_timeout
185        && config.subagent_timeout_secs != timeout
186    {
187        config.subagent_timeout_secs = timeout;
188        changes_made = true;
189    }
190
191    Ok(changes_made)
192}
193
194/// Handle the 'config' command
195#[allow(clippy::too_many_lines)]
196pub fn handle_config_command(
197    common: &CommonParams,
198    api_key: Option<String>,
199    model: Option<String>,
200    fast_model: Option<String>,
201    token_limit: Option<usize>,
202    param: Option<Vec<String>>,
203    subagent_timeout: Option<u64>,
204) -> anyhow::Result<()> {
205    log_debug!(
206        "Starting 'config' command with common: {:?}, api_key: {:?}, model: {:?}, token_limit: {:?}, param: {:?}, subagent_timeout: {:?}",
207        common,
208        api_key,
209        model,
210        token_limit,
211        param,
212        subagent_timeout
213    );
214
215    let mut config = Config::load()?;
216
217    // Apply configuration changes
218    let changes_made = apply_config_changes(
219        &mut config,
220        common,
221        model,
222        fast_model,
223        token_limit,
224        param,
225        api_key,
226        subagent_timeout,
227    )?;
228
229    if changes_made {
230        config.save()?;
231        ui::print_success("Configuration updated successfully.");
232        ui::print_newline();
233    }
234
235    // Print the configuration with beautiful styling
236    print_configuration(&config);
237
238    Ok(())
239}
240
241/// Handle printing current project configuration
242///
243/// Loads and displays the current project configuration if it exists,
244/// or shows a message if no project configuration is found.
245fn print_project_config() {
246    if let Ok(project_config) = Config::load_project_config() {
247        ui::print_message(&format!(
248            "\n{}",
249            "Current project configuration:".bright_cyan().bold()
250        ));
251        print_configuration(&project_config);
252    } else {
253        ui::print_message(&format!(
254            "\n{}",
255            "No project configuration file found.".yellow()
256        ));
257        ui::print_message("You can create one with the project-config command.");
258    }
259}
260
261/// Handle the 'project-config' command
262///
263/// Creates or updates a project-specific configuration file (.irisconfig)
264/// in the repository root. Project configurations allow teams to share
265/// common settings without sharing sensitive data like API keys.
266///
267/// # Security
268///
269/// API keys are never stored in project configuration files, ensuring that
270/// sensitive credentials are not accidentally committed to version control.
271///
272/// # Arguments
273///
274/// * `common` - Common parameters from command line
275/// * `model` - Optional model to set for the selected provider
276/// * `token_limit` - Optional token limit to set
277/// * `param` - Optional additional parameters to set
278/// * `print` - Whether to just print the current project config
279///
280/// # Returns
281///
282/// Result indicating success or an error
283pub fn handle_project_config_command(
284    common: &CommonParams,
285    model: Option<String>,
286    fast_model: Option<String>,
287    token_limit: Option<usize>,
288    param: Option<Vec<String>>,
289    subagent_timeout: Option<u64>,
290    print: bool,
291) -> anyhow::Result<()> {
292    log_debug!(
293        "Starting 'project-config' command with common: {:?}, model: {:?}, token_limit: {:?}, param: {:?}, subagent_timeout: {:?}, print: {}",
294        common,
295        model,
296        token_limit,
297        param,
298        subagent_timeout,
299        print
300    );
301
302    println!("\n{}", "✨ Project Configuration".bright_magenta().bold());
303
304    if print {
305        print_project_config();
306        return Ok(());
307    }
308
309    let mut config = Config::load_project_config().unwrap_or_else(|_| Config {
310        default_provider: String::new(),
311        providers: HashMap::new(),
312        use_gitmoji: true,
313        instructions: String::new(),
314        instruction_preset: String::new(),
315        theme: String::new(),
316        subagent_timeout_secs: 120,
317        temp_instructions: None,
318        temp_preset: None,
319        is_project_config: true,
320        gitmoji_override: None,
321    });
322
323    let mut changes_made = false;
324
325    // Apply provider settings
326    let provider_name = apply_provider_settings(
327        &mut config,
328        common,
329        model,
330        fast_model,
331        token_limit,
332        param,
333        &mut changes_made,
334    )?;
335
336    // Apply common settings
337    apply_common_settings(&mut config, common, subagent_timeout, &mut changes_made)?;
338
339    // Display result
340    display_project_config_result(&config, changes_made, &provider_name)?;
341
342    Ok(())
343}
344
345/// Apply provider-related settings to config
346fn apply_provider_settings(
347    config: &mut Config,
348    common: &CommonParams,
349    model: Option<String>,
350    fast_model: Option<String>,
351    token_limit: Option<usize>,
352    param: Option<Vec<String>>,
353    changes_made: &mut bool,
354) -> anyhow::Result<String> {
355    // Apply provider change
356    if let Some(provider_str) = &common.provider {
357        let provider: Provider = provider_str.parse().map_err(|_| {
358            anyhow!(
359                "Invalid provider: {}. Available: {}",
360                provider_str,
361                Provider::all_names().join(", ")
362            )
363        })?;
364
365        if config.default_provider != provider.name() {
366            config.default_provider = provider.name().to_string();
367            config
368                .providers
369                .entry(provider.name().to_string())
370                .or_default();
371            *changes_made = true;
372        }
373    }
374
375    // Get provider name to use
376    let provider_name = common
377        .provider
378        .clone()
379        .or_else(|| {
380            if config.default_provider.is_empty() {
381                None
382            } else {
383                Some(config.default_provider.clone())
384            }
385        })
386        .unwrap_or_else(|| Provider::default().name().to_string());
387
388    // Ensure provider config entry exists if setting model options
389    if model.is_some() || fast_model.is_some() || token_limit.is_some() || param.is_some() {
390        config.providers.entry(provider_name.clone()).or_default();
391    }
392
393    // Apply model settings
394    if let Some(m) = model
395        && let Some(pc) = config.providers.get_mut(&provider_name)
396        && pc.model != m
397    {
398        pc.model = m;
399        *changes_made = true;
400    }
401
402    if let Some(fm) = fast_model
403        && let Some(pc) = config.providers.get_mut(&provider_name)
404        && pc.fast_model != Some(fm.clone())
405    {
406        pc.fast_model = Some(fm);
407        *changes_made = true;
408    }
409
410    if let Some(limit) = token_limit
411        && let Some(pc) = config.providers.get_mut(&provider_name)
412        && pc.token_limit != Some(limit)
413    {
414        pc.token_limit = Some(limit);
415        *changes_made = true;
416    }
417
418    if let Some(params) = param
419        && let Some(pc) = config.providers.get_mut(&provider_name)
420    {
421        let additional_params = parse_additional_params(&params);
422        if pc.additional_params != additional_params {
423            pc.additional_params = additional_params;
424            *changes_made = true;
425        }
426    }
427
428    Ok(provider_name)
429}
430
431/// Apply common settings (gitmoji, instructions, preset, timeout)
432fn apply_common_settings(
433    config: &mut Config,
434    common: &CommonParams,
435    subagent_timeout: Option<u64>,
436    changes_made: &mut bool,
437) -> anyhow::Result<()> {
438    if let Some(use_gitmoji) = common.resolved_gitmoji()
439        && config.use_gitmoji != use_gitmoji
440    {
441        config.use_gitmoji = use_gitmoji;
442        *changes_made = true;
443    }
444
445    if let Some(instr) = &common.instructions
446        && config.instructions != *instr
447    {
448        config.instructions.clone_from(instr);
449        *changes_made = true;
450    }
451
452    if let Some(preset) = &common.preset {
453        let preset_library = get_instruction_preset_library();
454        if preset_library.get_preset(preset).is_some() {
455            if config.instruction_preset != *preset {
456                config.instruction_preset.clone_from(preset);
457                *changes_made = true;
458            }
459        } else {
460            return Err(anyhow!("Invalid preset: {}", preset));
461        }
462    }
463
464    if let Some(timeout) = subagent_timeout
465        && config.subagent_timeout_secs != timeout
466    {
467        config.subagent_timeout_secs = timeout;
468        *changes_made = true;
469    }
470
471    Ok(())
472}
473
474/// Display the result of project config command
475fn display_project_config_result(
476    config: &Config,
477    changes_made: bool,
478    _provider_name: &str,
479) -> anyhow::Result<()> {
480    if changes_made {
481        config.save_as_project_config()?;
482        ui::print_success("Project configuration created/updated successfully.");
483        println!();
484        println!(
485            "{}",
486            "Note: API keys are never stored in project configuration files."
487                .yellow()
488                .italic()
489        );
490        println!();
491        println!("{}", "Current project configuration:".bright_cyan().bold());
492        print_configuration(config);
493    } else {
494        println!("{}", "No changes made to project configuration.".yellow());
495        println!();
496
497        if let Ok(project_config) = Config::load_project_config() {
498            println!("{}", "Current project configuration:".bright_cyan().bold());
499            print_configuration(&project_config);
500        } else {
501            println!("{}", "No project configuration exists yet.".bright_yellow());
502            println!(
503                "{}",
504                "Use this command with options like --model or --provider to create one."
505                    .bright_white()
506            );
507        }
508    }
509    Ok(())
510}
511
512/// Display the configuration with `SilkCircuit` styling
513fn print_configuration(config: &Config) {
514    let purple = colors::accent_primary();
515    let cyan = colors::accent_secondary();
516    let coral = colors::accent_tertiary();
517    let yellow = colors::warning();
518    let green = colors::success();
519    let dim = colors::text_secondary();
520    let dim_sep = colors::text_dim();
521
522    println!();
523    println!(
524        "{}  {}  {}",
525        "━━━".truecolor(purple.0, purple.1, purple.2),
526        "IRIS CONFIGURATION"
527            .truecolor(cyan.0, cyan.1, cyan.2)
528            .bold(),
529        "━━━".truecolor(purple.0, purple.1, purple.2)
530    );
531    println!();
532
533    // Global Settings
534    print_section_header("GLOBAL");
535
536    print_config_row("Provider", &config.default_provider, cyan, true);
537    print_config_row(
538        "Gitmoji",
539        if config.use_gitmoji {
540            "enabled"
541        } else {
542            "disabled"
543        },
544        if config.use_gitmoji { green } else { dim },
545        false,
546    );
547    print_config_row("Preset", &config.instruction_preset, yellow, false);
548    print_config_row(
549        "Subagent Timeout",
550        &format!("{}s", config.subagent_timeout_secs),
551        coral,
552        false,
553    );
554
555    // Custom Instructions (if any)
556    if !config.instructions.is_empty() {
557        println!();
558        print_section_header("INSTRUCTIONS");
559        for line in config.instructions.lines() {
560            println!("  {}", line.truecolor(dim.0, dim.1, dim.2).italic());
561        }
562    }
563
564    // Show all configured providers
565    // For personal configs: show only those with API keys
566    // For project configs: show all providers (they never have API keys)
567    let mut providers: Vec<_> = config
568        .providers
569        .iter()
570        .filter(|(_, cfg)| config.is_project_config || !cfg.api_key.is_empty())
571        .collect();
572    providers.sort_by_key(|(name, _)| name.as_str());
573
574    for (provider_name, provider_config) in providers {
575        println!();
576        let is_active = provider_name == &config.default_provider;
577        let header = if is_active {
578            format!("{} ✦", provider_name.to_uppercase())
579        } else {
580            provider_name.to_uppercase()
581        };
582        print_section_header(&header);
583
584        // Model
585        print_config_row("Model", &provider_config.model, cyan, true);
586
587        // Fast Model
588        let fast_model = provider_config.fast_model.as_deref().unwrap_or("(default)");
589        print_config_row("Fast Model", fast_model, cyan, false);
590
591        // Token Limit
592        if let Some(limit) = provider_config.token_limit {
593            print_config_row("Token Limit", &limit.to_string(), coral, false);
594        }
595
596        // Additional Parameters
597        if !provider_config.additional_params.is_empty() {
598            println!(
599                "  {} {}",
600                "Params".truecolor(dim.0, dim.1, dim.2),
601                "─".truecolor(dim_sep.0, dim_sep.1, dim_sep.2)
602            );
603            for (key, value) in &provider_config.additional_params {
604                println!(
605                    "    {} {} {}",
606                    key.truecolor(cyan.0, cyan.1, cyan.2),
607                    "→".truecolor(dim_sep.0, dim_sep.1, dim_sep.2),
608                    value.truecolor(dim.0, dim.1, dim.2)
609                );
610            }
611        }
612    }
613
614    println!();
615    println!(
616        "{}",
617        "─".repeat(40).truecolor(dim_sep.0, dim_sep.1, dim_sep.2)
618    );
619    println!();
620}
621
622/// Print a section header in `SilkCircuit` style
623fn print_section_header(name: &str) {
624    let purple = colors::accent_primary();
625    let dim_sep = colors::text_dim();
626    println!(
627        "{} {} {}",
628        "─".truecolor(purple.0, purple.1, purple.2),
629        name.truecolor(purple.0, purple.1, purple.2).bold(),
630        "─"
631            .repeat(30 - name.len().min(28))
632            .truecolor(dim_sep.0, dim_sep.1, dim_sep.2)
633    );
634}
635
636/// Print a config row with label and value
637fn print_config_row(label: &str, value: &str, value_color: (u8, u8, u8), highlight: bool) {
638    let dim = colors::text_secondary();
639    let label_styled = format!("{label:>12}").truecolor(dim.0, dim.1, dim.2);
640
641    let value_styled = if highlight {
642        value
643            .truecolor(value_color.0, value_color.1, value_color.2)
644            .bold()
645    } else {
646        value.truecolor(value_color.0, value_color.1, value_color.2)
647    };
648
649    println!("{label_styled}  {value_styled}");
650}
651
652/// Parse additional parameters from the command line
653fn parse_additional_params(params: &[String]) -> HashMap<String, String> {
654    params
655        .iter()
656        .filter_map(|param| {
657            let parts: Vec<&str> = param.splitn(2, '=').collect();
658            if parts.len() == 2 {
659                Some((parts[0].to_string(), parts[1].to_string()))
660            } else {
661                None
662            }
663        })
664        .collect()
665}
666
667/// Handle the '`list_presets`' command
668pub fn handle_list_presets_command() -> Result<()> {
669    let library = get_instruction_preset_library();
670
671    // Get different categories of presets
672    let both_presets = list_presets_formatted_by_type(&library, Some(PresetType::Both));
673    let commit_only_presets = list_presets_formatted_by_type(&library, Some(PresetType::Commit));
674    let review_only_presets = list_presets_formatted_by_type(&library, Some(PresetType::Review));
675
676    println!(
677        "{}",
678        "\nGit-Iris Instruction Presets\n".bright_magenta().bold()
679    );
680
681    println!(
682        "{}",
683        "General Presets (usable for both commit and review):"
684            .bright_cyan()
685            .bold()
686    );
687    println!("{both_presets}\n");
688
689    if !commit_only_presets.is_empty() {
690        println!("{}", "Commit-specific Presets:".bright_green().bold());
691        println!("{commit_only_presets}\n");
692    }
693
694    if !review_only_presets.is_empty() {
695        println!("{}", "Review-specific Presets:".bright_blue().bold());
696        println!("{review_only_presets}\n");
697    }
698
699    println!("{}", "Usage:".bright_yellow().bold());
700    println!("  git-iris gen --preset <preset-key>");
701    println!("  git-iris review --preset <preset-key>");
702    println!("\nPreset types: [B] = Both commands, [C] = Commit only, [R] = Review only");
703
704    Ok(())
705}