Skip to main content

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