git_iris/
commands.rs

1use crate::ProviderConfig;
2use crate::common::CommonParams;
3use crate::config::Config;
4use crate::instruction_presets::{
5    PresetType, get_instruction_preset_library, list_presets_formatted_by_type,
6};
7use crate::llm::get_available_provider_names;
8use crate::log_debug;
9use crate::mcp::config::{MCPServerConfig, MCPTransportType};
10use crate::mcp::server;
11use crate::ui;
12use anyhow::Context;
13use anyhow::{Result, anyhow};
14use colored::Colorize;
15use std::collections::HashMap;
16
17/// Apply common configuration changes to a config object
18/// Returns true if any changes were made
19///
20/// This centralized function handles changes to configuration objects, used by both
21/// personal and project configuration commands.
22///
23/// # Arguments
24///
25/// * `config` - The configuration object to modify
26/// * `common` - Common parameters from command line
27/// * `model` - Optional model to set for the selected provider
28/// * `token_limit` - Optional token limit to set
29/// * `param` - Optional additional parameters to set
30/// * `api_key` - Optional API key to set (ignored in project configs)
31///
32/// # Returns
33///
34/// Boolean indicating if any changes were made to the configuration
35fn apply_config_changes(
36    config: &mut Config,
37    common: &CommonParams,
38    model: Option<String>,
39    token_limit: Option<usize>,
40    param: Option<Vec<String>>,
41    api_key: Option<String>,
42) -> anyhow::Result<bool> {
43    let mut changes_made = false;
44
45    // Apply common parameters to the config
46    common.apply_to_config(config)?;
47
48    // Handle provider change
49    if let Some(provider) = &common.provider {
50        if !get_available_provider_names().iter().any(|p| p == provider) {
51            return Err(anyhow!("Invalid provider: {}", provider));
52        }
53        if config.default_provider != *provider {
54            config.default_provider.clone_from(provider);
55            changes_made = true;
56        }
57        if !config.providers.contains_key(provider) {
58            config
59                .providers
60                .insert(provider.clone(), ProviderConfig::default());
61            changes_made = true;
62        }
63    }
64
65    let provider_config = config
66        .providers
67        .get_mut(&config.default_provider)
68        .context("Could not get default provider")?;
69
70    // Apply API key if provided
71    if let Some(key) = api_key {
72        if provider_config.api_key != key {
73            provider_config.api_key = key;
74            changes_made = true;
75        }
76    }
77
78    // Apply model change
79    if let Some(model) = model {
80        if provider_config.model != model {
81            provider_config.model = model;
82            changes_made = true;
83        }
84    }
85
86    // Apply parameter changes
87    if let Some(params) = param {
88        let additional_params = parse_additional_params(&params);
89        if provider_config.additional_params != additional_params {
90            provider_config.additional_params = additional_params;
91            changes_made = true;
92        }
93    }
94
95    // Apply gitmoji setting
96    if let Some(use_gitmoji) = common.gitmoji {
97        if config.use_gitmoji != use_gitmoji {
98            config.use_gitmoji = use_gitmoji;
99            changes_made = true;
100        }
101    }
102
103    // Apply instructions
104    if let Some(instr) = &common.instructions {
105        if config.instructions != *instr {
106            config.instructions.clone_from(instr);
107            changes_made = true;
108        }
109    }
110
111    // Apply token limit
112    if let Some(limit) = token_limit {
113        if provider_config.token_limit != Some(limit) {
114            provider_config.token_limit = Some(limit);
115            changes_made = true;
116        }
117    }
118
119    // Apply preset
120    if let Some(preset) = &common.preset {
121        let preset_library = get_instruction_preset_library();
122        if preset_library.get_preset(preset).is_some() {
123            if config.instruction_preset != *preset {
124                config.instruction_preset.clone_from(preset);
125                changes_made = true;
126            }
127        } else {
128            return Err(anyhow!("Invalid preset: {}", preset));
129        }
130    }
131
132    Ok(changes_made)
133}
134
135/// Handle the 'config' command
136#[allow(clippy::too_many_lines)]
137pub fn handle_config_command(
138    common: &CommonParams,
139    api_key: Option<String>,
140    model: Option<String>,
141    token_limit: Option<usize>,
142    param: Option<Vec<String>>,
143) -> anyhow::Result<()> {
144    log_debug!(
145        "Starting 'config' command with common: {:?}, api_key: {:?}, model: {:?}, token_limit: {:?}, param: {:?}",
146        common,
147        api_key,
148        model,
149        token_limit,
150        param
151    );
152
153    let mut config = Config::load()?;
154
155    // Apply configuration changes
156    let changes_made =
157        apply_config_changes(&mut config, common, model, token_limit, param, api_key)?;
158
159    if changes_made {
160        config.save()?;
161        ui::print_success("Configuration updated successfully.");
162        println!();
163    }
164
165    // Print the configuration with beautiful styling
166    print_configuration(&config);
167
168    Ok(())
169}
170
171/// Process and apply configuration changes to a config object for project configs
172///
173/// This is a specialized wrapper around the `apply_config_changes` function that ensures
174/// API keys are never passed to project configuration files.
175///
176/// # Arguments
177///
178/// * `config` - The configuration object to modify
179/// * `common` - Common parameters from command line
180/// * `model` - Optional model to set for the selected provider
181/// * `token_limit` - Optional token limit to set
182/// * `param` - Optional additional parameters to set
183///
184/// # Returns
185///
186/// Boolean indicating if any changes were made to the configuration
187fn apply_project_config_changes(
188    config: &mut Config,
189    common: &CommonParams,
190    model: Option<String>,
191    token_limit: Option<usize>,
192    param: Option<Vec<String>>,
193) -> anyhow::Result<bool> {
194    // Use the shared function but don't pass an API key (never stored in project configs)
195    apply_config_changes(config, common, model, token_limit, param, None)
196}
197
198/// Handle printing current project configuration
199///
200/// Loads and displays the current project configuration if it exists,
201/// or shows a message if no project configuration is found.
202fn print_project_config() {
203    if let Ok(project_config) = Config::load_project_config() {
204        println!(
205            "\n{}",
206            "Current project configuration:".bright_cyan().bold()
207        );
208        print_configuration(&project_config);
209    } else {
210        println!("\n{}", "No project configuration file found.".yellow());
211        println!("You can create one with the project-config command.");
212    }
213}
214
215/// Handle the 'project-config' command
216///
217/// Creates or updates a project-specific configuration file (.irisconfig)
218/// in the repository root. Project configurations allow teams to share
219/// common settings without sharing sensitive data like API keys.
220///
221/// # Security
222///
223/// API keys are never stored in project configuration files, ensuring that
224/// sensitive credentials are not accidentally committed to version control.
225///
226/// # Arguments
227///
228/// * `common` - Common parameters from command line
229/// * `model` - Optional model to set for the selected provider
230/// * `token_limit` - Optional token limit to set  
231/// * `param` - Optional additional parameters to set
232/// * `print` - Whether to just print the current project config
233///
234/// # Returns
235///
236/// Result indicating success or an error
237pub fn handle_project_config_command(
238    common: &CommonParams,
239    model: Option<String>,
240    token_limit: Option<usize>,
241    param: Option<Vec<String>>,
242    print: bool,
243) -> anyhow::Result<()> {
244    log_debug!(
245        "Starting 'project-config' command with common: {:?}, model: {:?}, token_limit: {:?}, param: {:?}, print: {}",
246        common,
247        model,
248        token_limit,
249        param,
250        print
251    );
252
253    // Load the global config first
254    let mut config = Config::load()?;
255
256    // Set up a header to explain what's happening
257    println!("\n{}", "✨ Project Configuration".bright_magenta().bold());
258
259    // If print-only mode, just display the current project config if it exists
260    if print {
261        print_project_config();
262        return Ok(());
263    }
264
265    // Apply changes and track if any were made
266    let changes_made =
267        apply_project_config_changes(&mut config, common, model, token_limit, param)?;
268
269    if changes_made {
270        // Save to project config file
271        config.save_as_project_config()?;
272        ui::print_success("Project configuration created/updated successfully.");
273        println!();
274
275        // Print a notice about API keys not being stored in project config
276        println!(
277            "{}",
278            "Note: API keys are never stored in project configuration files."
279                .yellow()
280                .italic()
281        );
282        println!();
283
284        // Print the newly created/updated config
285        println!("{}", "Current project configuration:".bright_cyan().bold());
286        print_configuration(&config);
287    } else {
288        println!("{}", "No changes made to project configuration.".yellow());
289        println!();
290
291        // Check if a project config exists and show it if found
292        if let Ok(project_config) = Config::load_project_config() {
293            println!("{}", "Current project configuration:".bright_cyan().bold());
294            print_configuration(&project_config);
295        } else {
296            println!("{}", "No project configuration exists yet.".bright_yellow());
297            println!(
298                "{}",
299                "Use this command with options like --model or --provider to create one."
300                    .bright_white()
301            );
302        }
303    }
304
305    Ok(())
306}
307
308/// Display the configuration with beautiful styling and colors
309fn print_configuration(config: &Config) {
310    // Create a title with gradient
311    println!(
312        "\n{}",
313        ui::create_gradient_text("🔮 Git-Iris Configuration 🔮").bold()
314    );
315    println!();
316
317    // Global settings section
318    println!("{}", "Global Settings".bright_magenta().bold().underline());
319    println!();
320
321    let provider_label = "Default Provider:".bright_cyan().bold();
322    let provider_value = config.default_provider.bright_white();
323    println!("  {} {} {}", "🔹".cyan(), provider_label, provider_value);
324
325    let gitmoji_label = "Use Gitmoji:".bright_cyan().bold();
326    let gitmoji_value = if config.use_gitmoji {
327        "Yes".bright_green()
328    } else {
329        "No".bright_red()
330    };
331    println!("  {} {} {}", "🔹".cyan(), gitmoji_label, gitmoji_value);
332
333    let preset_label = "Instruction Preset:".bright_cyan().bold();
334    let preset_value = config.instruction_preset.bright_yellow();
335    println!("  {} {} {}", "🔹".cyan(), preset_label, preset_value);
336
337    println!();
338
339    // Instructions section (if any)
340    if !config.instructions.is_empty() {
341        println!("{}", "Custom Instructions".bright_blue().bold().underline());
342        println!();
343
344        // Display full instructions, preserving newlines
345        config.instructions.lines().for_each(|line| {
346            println!("  {}", line.bright_white().italic());
347        });
348
349        println!();
350    }
351
352    // Provider configurations
353    for (provider, provider_config) in &config.providers {
354        println!(
355            "{}",
356            format!("Provider: {provider}")
357                .bright_green()
358                .bold()
359                .underline()
360        );
361        println!();
362
363        // API Key status with lock emoji
364        let api_key_label = "API Key:".yellow().bold();
365        let api_key_value = if provider_config.api_key.is_empty() {
366            "Not set".bright_red().italic()
367        } else {
368            "Set ✓".bright_green()
369        };
370        println!("  {} {} {}", "🔒".yellow(), api_key_label, api_key_value);
371
372        // Model with sparkle emoji
373        let model_label = "Model:".yellow().bold();
374        let model_value = provider_config.model.bright_cyan();
375        println!("  {} {} {}", "✨".yellow(), model_label, model_value);
376
377        // Token limit with gauge emoji
378        let token_limit_label = "Token Limit:".yellow().bold();
379        let token_limit_value = provider_config
380            .token_limit
381            .map_or("Default".bright_yellow(), |limit| {
382                limit.to_string().bright_white()
383            });
384        println!(
385            "  {} {} {}",
386            "🔢".yellow(),
387            token_limit_label,
388            token_limit_value
389        );
390
391        // Additional parameters if any
392        if !provider_config.additional_params.is_empty() {
393            let params_label = "Additional Parameters:".yellow().bold();
394            println!("  {} {}", "🔧".yellow(), params_label);
395
396            for (key, value) in &provider_config.additional_params {
397                println!("    - {}: {}", key.bright_blue(), value.bright_white());
398            }
399        }
400
401        println!();
402    }
403}
404
405/// Parse additional parameters from the command line
406fn parse_additional_params(params: &[String]) -> HashMap<String, String> {
407    params
408        .iter()
409        .filter_map(|param| {
410            let parts: Vec<&str> = param.splitn(2, '=').collect();
411            if parts.len() == 2 {
412                Some((parts[0].to_string(), parts[1].to_string()))
413            } else {
414                None
415            }
416        })
417        .collect()
418}
419
420/// Handle the '`list_presets`' command
421pub fn handle_list_presets_command() -> Result<()> {
422    let library = get_instruction_preset_library();
423
424    // Get different categories of presets
425    let both_presets = list_presets_formatted_by_type(&library, Some(PresetType::Both));
426    let commit_only_presets = list_presets_formatted_by_type(&library, Some(PresetType::Commit));
427    let review_only_presets = list_presets_formatted_by_type(&library, Some(PresetType::Review));
428
429    println!(
430        "{}",
431        "\nGit-Iris Instruction Presets\n".bright_magenta().bold()
432    );
433
434    println!(
435        "{}",
436        "General Presets (usable for both commit and review):"
437            .bright_cyan()
438            .bold()
439    );
440    println!("{both_presets}\n");
441
442    if !commit_only_presets.is_empty() {
443        println!("{}", "Commit-specific Presets:".bright_green().bold());
444        println!("{commit_only_presets}\n");
445    }
446
447    if !review_only_presets.is_empty() {
448        println!("{}", "Review-specific Presets:".bright_blue().bold());
449        println!("{review_only_presets}\n");
450    }
451
452    println!("{}", "Usage:".bright_yellow().bold());
453    println!("  git-iris gen --preset <preset-key>");
454    println!("  git-iris review --preset <preset-key>");
455    println!("\nPreset types: [B] = Both commands, [C] = Commit only, [R] = Review only");
456
457    Ok(())
458}
459
460/// Handle the 'serve' command to start an MCP server
461pub async fn handle_serve_command(
462    dev: bool,
463    transport: String,
464    port: Option<u16>,
465    listen_address: Option<String>,
466) -> anyhow::Result<()> {
467    log_debug!(
468        "Starting 'serve' command with dev: {}, transport: {}, port: {:?}, listen_address: {:?}",
469        dev,
470        transport,
471        port,
472        listen_address
473    );
474
475    // Create MCP server configuration
476    let mut config = MCPServerConfig::default();
477
478    // Set development mode
479    if dev {
480        config = config.with_dev_mode();
481    }
482
483    // Set transport type
484    let transport_type = match transport.to_lowercase().as_str() {
485        "stdio" => MCPTransportType::StdIO,
486        "sse" => MCPTransportType::SSE,
487        _ => {
488            return Err(anyhow::anyhow!(
489                "Invalid transport type: {}. Valid options are: stdio, sse",
490                transport
491            ));
492        }
493    };
494    config = config.with_transport(transport_type);
495
496    // Set port if provided
497    if let Some(p) = port {
498        config = config.with_port(p);
499    }
500
501    // Set listen address if provided
502    if let Some(addr) = listen_address {
503        config = config.with_listen_address(addr);
504    }
505
506    // Start the server - all UI output is now handled inside serve implementation
507    server::serve(config).await
508}