Skip to main content

git_iris/
cli.rs

1use crate::commands;
2use crate::common::CommonParams;
3use crate::log_debug;
4use crate::providers::Provider;
5use crate::theme;
6use crate::theme::names::tokens;
7use crate::ui;
8use clap::builder::{Styles, styling::AnsiColor};
9use clap::{CommandFactory, Parser, Subcommand, crate_version};
10use clap_complete::{Shell, generate};
11use colored::Colorize;
12use std::io;
13
14/// Default log file path for debug output
15pub const LOG_FILE: &str = "git-iris-debug.log";
16
17/// CLI structure defining the available commands and global arguments
18#[derive(Parser)]
19#[command(
20    author,
21    version = crate_version!(),
22    about = "Git-Iris: AI-powered Git workflow assistant",
23    long_about = "Git-Iris enhances your Git workflow with AI-assisted commit messages, code reviews, changelogs, and more.",
24    disable_version_flag = true,
25    after_help = get_dynamic_help(),
26    styles = get_styles(),
27)]
28#[allow(clippy::struct_excessive_bools)]
29pub struct Cli {
30    /// Subcommands available for the CLI
31    #[command(subcommand)]
32    pub command: Option<Commands>,
33
34    /// Log debug messages to a file
35    #[arg(
36        short = 'l',
37        long = "log",
38        global = true,
39        help = "Log debug messages to a file"
40    )]
41    pub log: bool,
42
43    /// Specify a custom log file path
44    #[arg(
45        long = "log-file",
46        global = true,
47        help = "Specify a custom log file path"
48    )]
49    pub log_file: Option<String>,
50
51    /// Suppress non-essential output (spinners, waiting messages, etc.)
52    #[arg(
53        short = 'q',
54        long = "quiet",
55        global = true,
56        help = "Suppress non-essential output"
57    )]
58    pub quiet: bool,
59
60    /// Display the version
61    #[arg(
62        short = 'v',
63        long = "version",
64        global = true,
65        help = "Display the version"
66    )]
67    pub version: bool,
68
69    /// Repository URL to use instead of local repository
70    #[arg(
71        short = 'r',
72        long = "repo",
73        global = true,
74        help = "Repository URL to use instead of local repository"
75    )]
76    pub repository_url: Option<String>,
77
78    /// Enable debug mode for detailed agent observability
79    #[arg(
80        long = "debug",
81        global = true,
82        help = "Enable debug mode with gorgeous color-coded output showing agent execution details"
83    )]
84    pub debug: bool,
85
86    /// Override the theme for this session
87    #[arg(
88        long = "theme",
89        global = true,
90        help = "Override theme for this session (use 'git-iris themes' to list available)"
91    )]
92    pub theme: Option<String>,
93}
94
95/// Enumeration of available subcommands
96#[derive(Subcommand)]
97#[command(subcommand_negates_reqs = true)]
98#[command(subcommand_precedence_over_arg = true)]
99pub enum Commands {
100    // Feature commands first
101    /// Generate a commit message using AI
102    #[command(
103        about = "Generate a commit message using AI",
104        long_about = "Generate a commit message using AI based on the current Git context.",
105        after_help = get_dynamic_help(),
106        visible_alias = "commit"
107    )]
108    Gen {
109        #[command(flatten)]
110        common: CommonParams,
111
112        /// Automatically commit with the generated message
113        #[arg(short, long, help = "Automatically commit with the generated message")]
114        auto_commit: bool,
115
116        /// Print the generated message to stdout and exit
117        #[arg(short, long, help = "Print the generated message to stdout and exit")]
118        print: bool,
119
120        /// Skip the verification step (pre/post commit hooks)
121        #[arg(long, help = "Skip verification steps (pre/post commit hooks)")]
122        no_verify: bool,
123
124        /// Amend the previous commit instead of creating a new one
125        #[arg(long, help = "Amend the previous commit with staged changes")]
126        amend: bool,
127    },
128
129    /// Review staged changes and provide feedback
130    #[command(
131        about = "Review staged changes using AI",
132        long_about = "Generate a comprehensive multi-dimensional code review of staged changes using AI. Analyzes code across 10 dimensions including complexity, security, performance, and more."
133    )]
134    Review {
135        #[command(flatten)]
136        common: CommonParams,
137
138        /// Print the generated review to stdout and exit
139        #[arg(short, long, help = "Print the generated review to stdout and exit")]
140        print: bool,
141
142        /// Output raw markdown without any console formatting
143        #[arg(long, help = "Output raw markdown without any console formatting")]
144        raw: bool,
145
146        /// Include unstaged changes in the review
147        #[arg(long, help = "Include unstaged changes in the review")]
148        include_unstaged: bool,
149
150        /// Review a specific commit by ID (hash, branch, or reference)
151        #[arg(
152            long,
153            help = "Review a specific commit by ID (hash, branch, or reference)"
154        )]
155        commit: Option<String>,
156
157        /// Starting branch for comparison (defaults to the repository's primary branch)
158        #[arg(
159            long,
160            help = "Starting branch for comparison (defaults to the repository's primary branch). Used with --to for explicit branch comparison reviews"
161        )]
162        from: Option<String>,
163
164        /// Target branch for comparison (e.g., 'feature-branch', 'pr-branch')
165        #[arg(
166            long,
167            help = "Target branch for comparison (e.g., 'feature-branch', 'pr-branch'). Used with --from for branch comparison reviews or on its own to compare from the repository's primary branch"
168        )]
169        to: Option<String>,
170    },
171
172    /// Generate a pull request description
173    #[command(
174        about = "Generate a pull request description using AI",
175        long_about = "Generate a comprehensive pull request description based on commit ranges, branch differences, or single commits. Analyzes the overall changeset as an atomic unit and creates professional PR descriptions with summaries, detailed explanations, and testing notes.\n\nUsage examples:\n• Single commit: --from abc1234 or --to abc1234\n• Single commitish: --from HEAD~1 or --to HEAD~2\n• Multiple commits: --from HEAD~3 (reviews last 3 commits)\n• Commit range: --from abc1234 --to def5678\n• Branch comparison: --from <default-branch> --to feature-branch\n• From repository primary branch to target branch: --to feature-branch\n\nSupported commitish syntax: HEAD~2, HEAD^, @~3, <default-branch>~1, origin/<default-branch>^, etc."
176    )]
177    Pr {
178        #[command(flatten)]
179        common: CommonParams,
180
181        /// Print the generated PR description to stdout and exit
182        #[arg(
183            short,
184            long,
185            help = "Print the generated PR description to stdout and exit"
186        )]
187        print: bool,
188
189        /// Output raw markdown without any console formatting
190        #[arg(long, help = "Output raw markdown without any console formatting")]
191        raw: bool,
192
193        /// Copy raw markdown to clipboard
194        #[arg(
195            short,
196            long,
197            help = "Copy raw markdown to clipboard (for pasting into GitHub/GitLab)"
198        )]
199        copy: bool,
200
201        /// Starting branch, commit, or commitish for comparison
202        #[arg(
203            long,
204            help = "Starting branch, commit, or commitish for comparison. For single commit analysis, specify just this parameter with a commit hash (e.g., --from abc1234). For reviewing multiple commits, use commitish syntax (e.g., --from HEAD~3 to review last 3 commits)"
205        )]
206        from: Option<String>,
207
208        /// Target branch, commit, or commitish for comparison
209        #[arg(
210            long,
211            help = "Target branch, commit, or commitish for comparison. For single commit analysis, specify just this parameter with a commit hash or commitish (e.g., --to HEAD~2)"
212        )]
213        to: Option<String>,
214    },
215
216    /// Generate a changelog
217    #[command(
218        about = "Generate a changelog",
219        long_about = "Generate a changelog between two specified Git references."
220    )]
221    Changelog {
222        #[command(flatten)]
223        common: CommonParams,
224
225        /// Starting Git reference (commit hash, tag, or branch name)
226        #[arg(long, required = true)]
227        from: String,
228
229        /// Ending Git reference (commit hash, tag, or branch name). Defaults to HEAD if not specified.
230        #[arg(long)]
231        to: Option<String>,
232
233        /// Output raw markdown without any console formatting
234        #[arg(long, help = "Output raw markdown without any console formatting")]
235        raw: bool,
236
237        /// Update the changelog file with the new changes
238        #[arg(long, help = "Update the changelog file with the new changes")]
239        update: bool,
240
241        /// Path to the changelog file
242        #[arg(long, help = "Path to the changelog file (defaults to CHANGELOG.md)")]
243        file: Option<String>,
244
245        /// Explicit version name to use in the changelog instead of getting it from Git
246        #[arg(long, help = "Explicit version name to use in the changelog")]
247        version_name: Option<String>,
248    },
249
250    /// Generate release notes
251    #[command(
252        about = "Generate release notes",
253        long_about = "Generate comprehensive release notes between two specified Git references."
254    )]
255    ReleaseNotes {
256        #[command(flatten)]
257        common: CommonParams,
258
259        /// Starting Git reference (commit hash, tag, or branch name)
260        #[arg(long, required = true)]
261        from: String,
262
263        /// Ending Git reference (commit hash, tag, or branch name). Defaults to HEAD if not specified.
264        #[arg(long)]
265        to: Option<String>,
266
267        /// Output raw markdown without any console formatting
268        #[arg(long, help = "Output raw markdown without any console formatting")]
269        raw: bool,
270
271        /// Update the release notes file with the new content
272        #[arg(long, help = "Update the release notes file with the new content")]
273        update: bool,
274
275        /// Path to the release notes file
276        #[arg(
277            long,
278            help = "Path to the release notes file (defaults to RELEASE_NOTES.md)"
279        )]
280        file: Option<String>,
281
282        /// Explicit version name to use in the release notes instead of getting it from Git
283        #[arg(long, help = "Explicit version name to use in the release notes")]
284        version_name: Option<String>,
285    },
286
287    /// Launch Iris Studio - unified TUI for all operations
288    #[command(
289        about = "Launch Iris Studio TUI",
290        long_about = "Launch Iris Studio, a unified terminal user interface for exploring code, generating commits, reviewing changes, and more. The interface adapts to your repository state."
291    )]
292    Studio {
293        #[command(flatten)]
294        common: CommonParams,
295
296        /// Initial mode to launch in
297        #[arg(
298            long,
299            value_name = "MODE",
300            help = "Initial mode: explore, commit, review, pr, changelog, release-notes"
301        )]
302        mode: Option<String>,
303
304        /// Starting ref for PR/changelog comparison (defaults to the repository's primary branch)
305        #[arg(long, value_name = "REF", help = "Starting ref for comparison")]
306        from: Option<String>,
307
308        /// Ending ref for PR/changelog comparison (defaults to HEAD)
309        #[arg(long, value_name = "REF", help = "Ending ref for comparison")]
310        to: Option<String>,
311    },
312
313    // Configuration and utility commands
314    /// Configure the AI-assisted Git commit message generator
315    #[command(about = "Configure Git-Iris settings and providers")]
316    Config {
317        #[command(flatten)]
318        common: CommonParams,
319
320        /// Set API key for the specified provider
321        #[arg(long, help = "Set API key for the specified provider")]
322        api_key: Option<String>,
323
324        /// Set fast model for the specified provider (used for status updates and simple tasks)
325        #[arg(
326            long,
327            help = "Set fast model for the specified provider (used for status updates and simple tasks)"
328        )]
329        fast_model: Option<String>,
330
331        /// Set token limit for the specified provider
332        #[arg(long, help = "Set token limit for the specified provider")]
333        token_limit: Option<usize>,
334
335        /// Set additional parameters for the specified provider
336        #[arg(
337            long,
338            help = "Set additional parameters for the specified provider (key=value)"
339        )]
340        param: Option<Vec<String>>,
341
342        /// Set timeout in seconds for parallel subagent tasks
343        #[arg(
344            long,
345            help = "Set timeout in seconds for parallel subagent tasks (default: 120)"
346        )]
347        subagent_timeout: Option<u64>,
348    },
349
350    /// Create or update a project-specific configuration file
351    #[command(
352        about = "Manage project-specific configuration",
353        long_about = "Create or update a project-specific .irisconfig file in the repository root."
354    )]
355    ProjectConfig {
356        #[command(flatten)]
357        common: CommonParams,
358
359        /// Set fast model for the specified provider (used for status updates and simple tasks)
360        #[arg(
361            long,
362            help = "Set fast model for the specified provider (used for status updates and simple tasks)"
363        )]
364        fast_model: Option<String>,
365
366        /// Set token limit for the specified provider
367        #[arg(long, help = "Set token limit for the specified provider")]
368        token_limit: Option<usize>,
369
370        /// Set additional parameters for the specified provider
371        #[arg(
372            long,
373            help = "Set additional parameters for the specified provider (key=value)"
374        )]
375        param: Option<Vec<String>>,
376
377        /// Set timeout in seconds for parallel subagent tasks
378        #[arg(
379            long,
380            help = "Set timeout in seconds for parallel subagent tasks (default: 120)"
381        )]
382        subagent_timeout: Option<u64>,
383
384        /// Print the current project configuration
385        #[arg(short, long, help = "Print the current project configuration")]
386        print: bool,
387    },
388
389    /// List available instruction presets
390    #[command(about = "List available instruction presets")]
391    ListPresets,
392
393    /// List available themes
394    #[command(about = "List available themes")]
395    Themes,
396
397    /// Generate shell completions
398    #[command(
399        about = "Generate shell completions",
400        long_about = "Generate shell completion scripts for bash, zsh, fish, elvish, or powershell.\n\nUsage examples:\n• Bash: git-iris completions bash >> ~/.bashrc\n• Zsh:  git-iris completions zsh >> ~/.zshrc\n• Fish: git-iris completions fish > ~/.config/fish/completions/git-iris.fish"
401    )]
402    Completions {
403        /// Shell to generate completions for
404        #[arg(value_enum)]
405        shell: Shell,
406    },
407
408    /// Manage Git hooks for automatic commit message generation
409    #[command(
410        about = "Install or uninstall the prepare-commit-msg Git hook",
411        long_about = "Install or uninstall a prepare-commit-msg Git hook that automatically generates commit messages using git-iris when you run 'git commit'."
412    )]
413    Hook {
414        /// Hook action to perform
415        #[command(subcommand)]
416        action: HookAction,
417    },
418}
419
420/// Hook management sub-commands
421#[derive(Subcommand)]
422pub enum HookAction {
423    /// Install the prepare-commit-msg hook
424    #[command(about = "Install the prepare-commit-msg hook")]
425    Install {
426        /// Overwrite an existing hook that wasn't installed by git-iris
427        #[arg(long, help = "Overwrite an existing hook not installed by git-iris")]
428        force: bool,
429    },
430    /// Uninstall the prepare-commit-msg hook
431    #[command(about = "Uninstall the prepare-commit-msg hook")]
432    Uninstall,
433}
434
435/// Define custom styles for Clap
436fn get_styles() -> Styles {
437    Styles::styled()
438        .header(AnsiColor::Magenta.on_default().bold())
439        .usage(AnsiColor::Cyan.on_default().bold())
440        .literal(AnsiColor::Green.on_default().bold())
441        .placeholder(AnsiColor::Yellow.on_default())
442        .valid(AnsiColor::Blue.on_default().bold())
443        .invalid(AnsiColor::Red.on_default().bold())
444        .error(AnsiColor::Red.on_default().bold())
445}
446
447/// Parse the command-line arguments
448#[must_use]
449pub fn parse_args() -> Cli {
450    Cli::parse()
451}
452
453/// Generate dynamic help including available LLM providers
454fn get_dynamic_help() -> String {
455    let providers_list = Provider::all_names()
456        .iter()
457        .map(|p| format!("{}", p.bold()))
458        .collect::<Vec<_>>()
459        .join(" • ");
460
461    format!("\nAvailable LLM Providers: {providers_list}")
462}
463
464/// Main function to parse arguments and handle the command
465///
466/// # Errors
467///
468/// Returns an error when command handling fails.
469pub async fn main() -> anyhow::Result<()> {
470    let cli = parse_args();
471
472    if cli.version {
473        ui::print_version(crate_version!());
474        return Ok(());
475    }
476
477    // Initialize logger with appropriate filter level — must happen before any log calls
478    if let Err(e) = crate::logger::init(cli.log) {
479        eprintln!("Warning: Failed to initialize logging: {e}");
480    }
481
482    if cli.log {
483        crate::logger::enable_logging();
484        crate::logger::set_log_to_stdout(true);
485        let log_file = cli.log_file.as_deref().unwrap_or(LOG_FILE);
486        crate::logger::set_log_file(log_file)?;
487        log_debug!("Debug logging enabled");
488    } else {
489        crate::logger::disable_logging();
490    }
491
492    // Set quiet mode in the UI module
493    if cli.quiet {
494        crate::ui::set_quiet_mode(true);
495    }
496
497    // Initialize theme
498    initialize_theme(cli.theme.as_deref());
499
500    // Enable debug mode if requested
501    if cli.debug {
502        crate::agents::debug::enable_debug_mode();
503        crate::agents::debug::debug_header("🔮 IRIS DEBUG MODE ACTIVATED 🔮");
504    }
505
506    if let Some(command) = cli.command {
507        handle_command(command, cli.repository_url).await
508    } else {
509        // Default: launch Studio with auto-detect mode
510        handle_studio(
511            CommonParams::default(),
512            None,
513            None,
514            None,
515            cli.repository_url,
516        )
517        .await
518    }
519}
520
521/// Initialize the theme from CLI flag or config
522fn initialize_theme(cli_theme: Option<&str>) {
523    use crate::config::Config;
524
525    // CLI flag takes precedence
526    let theme_name = if let Some(name) = cli_theme {
527        Some(name.to_string())
528    } else {
529        // Try to load from config
530        Config::load().ok().and_then(|c| {
531            if c.theme.is_empty() {
532                None
533            } else {
534                Some(c.theme)
535            }
536        })
537    };
538
539    // Load the theme if specified, otherwise default is already active
540    if let Some(name) = theme_name {
541        if let Err(e) = theme::load_theme_by_name(&name) {
542            ui::print_warning(&format!(
543                "Failed to load theme '{}': {}. Using default.",
544                name, e
545            ));
546        } else {
547            log_debug!("Loaded theme: {}", name);
548        }
549    }
550}
551
552/// Configuration for the Gen command
553#[allow(clippy::struct_excessive_bools)]
554struct GenConfig {
555    auto_commit: bool,
556    use_gitmoji: bool,
557    print_only: bool,
558    verify: bool,
559    amend: bool,
560}
561
562/// Handle the `Gen` command with agent framework and Studio integration
563#[allow(clippy::too_many_lines)]
564async fn handle_gen_with_agent(
565    common: CommonParams,
566    config: GenConfig,
567    repository_url: Option<String>,
568) -> anyhow::Result<()> {
569    use crate::agents::{IrisAgentService, StructuredResponse, TaskContext};
570    use crate::config::Config;
571    use crate::git::GitRepo;
572    use crate::instruction_presets::PresetType;
573    use crate::output::format_commit_result;
574    use crate::services::GitCommitService;
575    use crate::studio::{Mode, run_studio};
576    use crate::types::format_commit_message;
577    use anyhow::Context;
578    use std::sync::Arc;
579
580    // Check if the preset is appropriate for commit messages
581    if !common.is_valid_preset_for_type(PresetType::Commit) {
582        ui::print_warning(
583            "The specified preset may not be suitable for commit messages. Consider using a commit or general preset instead.",
584        );
585        ui::print_info("Run 'git-iris list-presets' to see available presets for commits.");
586    }
587
588    // Amend mode requires --print or --auto-commit (Studio amend support coming later)
589    if config.amend && !config.print_only && !config.auto_commit {
590        ui::print_warning("--amend requires --print or --auto-commit for now.");
591        ui::print_info("Example: git-iris gen --amend --auto-commit");
592        return Ok(());
593    }
594
595    let mut cfg = Config::load()?;
596    common.apply_to_config(&mut cfg)?;
597
598    // Create git repo and services
599    let repo_url = repository_url.clone().or(common.repository_url.clone());
600    let git_repo = Arc::new(GitRepo::new_from_url(repo_url).context("Failed to create GitRepo")?);
601    let use_gitmoji = config.use_gitmoji && cfg.use_gitmoji;
602
603    // Create GitCommitService for commit operations
604    let commit_service = Arc::new(GitCommitService::new(
605        git_repo.clone(),
606        use_gitmoji,
607        config.verify,
608    ));
609
610    // Create IrisAgentService for LLM operations
611    let agent_service = Arc::new(IrisAgentService::from_common_params(
612        &common,
613        repository_url.clone(),
614    )?);
615
616    // Get git info for staged files check
617    let git_info = git_repo.get_git_info(&cfg)?;
618
619    // For --print or --auto-commit, we need to generate the message first
620    if config.print_only || config.auto_commit {
621        // For amend mode, we allow empty staged changes (amending message only)
622        // For regular commits, we require staged changes
623        if git_info.staged_files.is_empty() && !config.amend {
624            ui::print_warning(
625                "No staged changes. Please stage your changes before generating a commit message.",
626            );
627            ui::print_info("You can stage changes using 'git add <file>' or 'git add .'");
628            return Ok(());
629        }
630
631        // Run pre-commit hook before we do anything else
632        if let Err(e) = commit_service.pre_commit() {
633            ui::print_error(&format!("Pre-commit failed: {e}"));
634            return Err(e);
635        }
636
637        // Create spinner for agent mode
638        let spinner_msg = if config.amend {
639            "Generating amended commit message..."
640        } else {
641            "Generating commit message..."
642        };
643        let spinner = ui::create_spinner(spinner_msg);
644
645        // Use IrisAgentService for commit message generation
646        // For amend, we pass the original message as context
647        let context = if config.amend {
648            let original_message = commit_service.get_head_commit_message().unwrap_or_default();
649            TaskContext::for_amend(original_message)
650        } else {
651            TaskContext::for_gen()
652        };
653        let response = agent_service.execute_task("commit", context).await?;
654
655        // Extract commit message from response
656        let StructuredResponse::CommitMessage(generated_message) = response else {
657            return Err(anyhow::anyhow!("Expected commit message response"));
658        };
659
660        // Finish spinner after agent completes
661        spinner.finish_and_clear();
662
663        if config.print_only {
664            println!("{}", format_commit_message(&generated_message));
665            return Ok(());
666        }
667
668        // Auto-commit/amend mode
669        if commit_service.is_remote() {
670            ui::print_error(
671                "Cannot automatically commit to a remote repository. Use --print instead.",
672            );
673            return Err(anyhow::anyhow!(
674                "Auto-commit not supported for remote repositories"
675            ));
676        }
677
678        let commit_result = if config.amend {
679            commit_service.perform_amend(&format_commit_message(&generated_message))
680        } else {
681            commit_service.perform_commit(&format_commit_message(&generated_message))
682        };
683
684        match commit_result {
685            Ok(result) => {
686                let output =
687                    format_commit_result(&result, &format_commit_message(&generated_message));
688                println!("{output}");
689            }
690            Err(e) => {
691                let action = if config.amend { "amend" } else { "commit" };
692                eprintln!("Failed to {action}: {e}");
693                return Err(e);
694            }
695        }
696        return Ok(());
697    }
698
699    // Interactive mode: launch Studio (it handles staged check and auto-generation)
700    if commit_service.is_remote() {
701        ui::print_warning(
702            "Interactive commit not available for remote repositories. Use --print instead.",
703        );
704        return Ok(());
705    }
706
707    // Launch Studio in Commit mode - it will auto-generate if there are staged changes
708    run_studio(
709        cfg,
710        Some(git_repo),
711        Some(commit_service),
712        Some(agent_service),
713        Some(Mode::Commit),
714        None,
715        None,
716    )
717}
718
719/// Handle the `Gen` command
720async fn handle_gen(
721    common: CommonParams,
722    config: GenConfig,
723    repository_url: Option<String>,
724) -> anyhow::Result<()> {
725    log_debug!(
726        "Handling 'gen' command with common: {:?}, auto_commit: {}, use_gitmoji: {}, print: {}, verify: {}, amend: {}",
727        common,
728        config.auto_commit,
729        config.use_gitmoji,
730        config.print_only,
731        config.verify,
732        config.amend
733    );
734
735    ui::print_version(crate_version!());
736    ui::print_newline();
737
738    handle_gen_with_agent(common, config, repository_url).await
739}
740
741/// Handle the `Config` command
742fn handle_config(
743    common: &CommonParams,
744    api_key: Option<String>,
745    model: Option<String>,
746    fast_model: Option<String>,
747    token_limit: Option<usize>,
748    param: Option<Vec<String>>,
749    subagent_timeout: Option<u64>,
750) -> anyhow::Result<()> {
751    log_debug!(
752        "Handling 'config' command with common: {:?}, api_key: {}, model: {:?}, token_limit: {:?}, param: {:?}, subagent_timeout: {:?}",
753        common,
754        if api_key.is_some() {
755            "[REDACTED]"
756        } else {
757            "<none>"
758        },
759        model,
760        token_limit,
761        param,
762        subagent_timeout
763    );
764    commands::handle_config_command(
765        common,
766        api_key,
767        model,
768        fast_model,
769        token_limit,
770        param,
771        subagent_timeout,
772    )
773}
774
775/// Handle the `Review` command
776#[allow(clippy::too_many_arguments)]
777async fn handle_review(
778    common: CommonParams,
779    print: bool,
780    raw: bool,
781    repository_url: Option<String>,
782    include_unstaged: bool,
783    commit: Option<String>,
784    from: Option<String>,
785    to: Option<String>,
786) -> anyhow::Result<()> {
787    log_debug!(
788        "Handling 'review' command with common: {:?}, print: {}, raw: {}, include_unstaged: {}, commit: {:?}, from: {:?}, to: {:?}",
789        common,
790        print,
791        raw,
792        include_unstaged,
793        commit,
794        from,
795        to
796    );
797
798    // For raw output, skip all formatting
799    if !raw {
800        ui::print_version(crate_version!());
801        ui::print_newline();
802    }
803
804    use crate::agents::{IrisAgentService, TaskContext};
805
806    // Create spinner for progress indication (skip for raw output)
807    let spinner = if raw {
808        None
809    } else {
810        Some(ui::create_spinner("Initializing Iris..."))
811    };
812
813    // Use IrisAgentService for agent execution
814    let service = IrisAgentService::from_common_params(&common, repository_url)?;
815    let default_base = service
816        .git_repo()
817        .and_then(|repo| repo.get_default_base_ref().ok())
818        .unwrap_or_else(|| "main".to_string());
819    let context =
820        TaskContext::for_review_with_base(commit, from, to, include_unstaged, &default_base)?;
821    let response = service.execute_task("review", context).await?;
822
823    // Finish spinner
824    if let Some(s) = spinner {
825        s.finish_and_clear();
826    }
827
828    if raw || print {
829        println!("{response}");
830    } else {
831        ui::print_success("Code review completed successfully");
832        println!("{response}");
833    }
834    Ok(())
835}
836
837/// Handle the `Changelog` command
838#[allow(clippy::too_many_arguments)]
839async fn handle_changelog(
840    common: CommonParams,
841    from: String,
842    to: Option<String>,
843    raw: bool,
844    repository_url: Option<String>,
845    update: bool,
846    file: Option<String>,
847    version_name: Option<String>,
848) -> anyhow::Result<()> {
849    log_debug!(
850        "Handling 'changelog' command with common: {:?}, from: {}, to: {:?}, raw: {}, update: {}, file: {:?}, version_name: {:?}",
851        common,
852        from,
853        to,
854        raw,
855        update,
856        file,
857        version_name
858    );
859
860    // For raw output, skip all formatting
861    if !raw {
862        ui::print_version(crate_version!());
863        ui::print_newline();
864    }
865
866    use crate::agents::{IrisAgentService, TaskContext};
867    use crate::changelog::ChangelogGenerator;
868    use crate::git::GitRepo;
869    use anyhow::Context;
870    use std::sync::Arc;
871
872    // Create structured context for changelog with version_name and current date
873    let context = TaskContext::for_changelog(from.clone(), to.clone(), version_name.clone(), None);
874    let to_ref = to.unwrap_or_else(|| "HEAD".to_string());
875
876    // Create spinner for progress indication (skip for raw output)
877    let spinner = if raw {
878        None
879    } else {
880        Some(ui::create_spinner("Initializing Iris..."))
881    };
882
883    // Use IrisAgentService for agent execution
884    let service = IrisAgentService::from_common_params(&common, repository_url.clone())?;
885    let response = service.execute_task("changelog", context).await?;
886
887    // Finish spinner
888    if let Some(s) = spinner {
889        s.finish_and_clear();
890    }
891
892    // Print the changelog
893    println!("{response}");
894
895    if update {
896        // Extract the formatted content for file update
897        let formatted_content = response.to_string();
898        let changelog_path = file.unwrap_or_else(|| "CHANGELOG.md".to_string());
899        let repo_url_for_update = repository_url.or(common.repository_url.clone());
900
901        // Create GitRepo for file update
902        let git_repo = if let Some(url) = repo_url_for_update {
903            Arc::new(
904                GitRepo::clone_remote_repository(&url)
905                    .context("Failed to clone repository for changelog update")?,
906            )
907        } else {
908            let repo_path = std::env::current_dir()?;
909            Arc::new(
910                GitRepo::new(&repo_path)
911                    .context("Failed to create GitRepo for changelog update")?,
912            )
913        };
914
915        // Update changelog file
916        let update_spinner =
917            ui::create_spinner(&format!("Updating changelog file at {changelog_path}..."));
918
919        match ChangelogGenerator::update_changelog_file(
920            &formatted_content,
921            &changelog_path,
922            &git_repo,
923            &to_ref,
924            version_name,
925        ) {
926            Ok(()) => {
927                update_spinner.finish_and_clear();
928                ui::print_success(&format!(
929                    "✨ Changelog successfully updated at {}",
930                    changelog_path.bright_green()
931                ));
932            }
933            Err(e) => {
934                update_spinner.finish_and_clear();
935                ui::print_error(&format!("Failed to update changelog file: {e}"));
936                return Err(e);
937            }
938        }
939    }
940    Ok(())
941}
942
943/// Handle the `Release Notes` command
944#[allow(clippy::too_many_arguments)]
945async fn handle_release_notes(
946    common: CommonParams,
947    from: String,
948    to: Option<String>,
949    raw: bool,
950    repository_url: Option<String>,
951    update: bool,
952    file: Option<String>,
953    version_name: Option<String>,
954) -> anyhow::Result<()> {
955    log_debug!(
956        "Handling 'release-notes' command with common: {:?}, from: {}, to: {:?}, raw: {}, update: {}, file: {:?}, version_name: {:?}",
957        common,
958        from,
959        to,
960        raw,
961        update,
962        file,
963        version_name
964    );
965
966    // For raw output, skip all formatting
967    if !raw {
968        ui::print_version(crate_version!());
969        ui::print_newline();
970    }
971
972    use crate::agents::{IrisAgentService, TaskContext};
973    use std::fs;
974    use std::path::Path;
975
976    // Create structured context for release notes with version_name and current date
977    let context = TaskContext::for_changelog(from, to, version_name, None);
978
979    // Create spinner for progress indication (skip for raw output)
980    let spinner = if raw {
981        None
982    } else {
983        Some(ui::create_spinner("Initializing Iris..."))
984    };
985
986    // Use IrisAgentService for agent execution
987    let service = IrisAgentService::from_common_params(&common, repository_url)?;
988    let response = service.execute_task("release_notes", context).await?;
989
990    // Finish spinner
991    if let Some(s) = spinner {
992        s.finish_and_clear();
993    }
994
995    println!("{response}");
996
997    // Handle --update flag
998    if update {
999        let release_notes_path = file.unwrap_or_else(|| "RELEASE_NOTES.md".to_string());
1000        let formatted_content = response.to_string();
1001
1002        let update_spinner = ui::create_spinner(&format!(
1003            "Updating release notes file at {release_notes_path}..."
1004        ));
1005
1006        // Write or append to file
1007        let path = Path::new(&release_notes_path);
1008        let result = if path.exists() {
1009            // Prepend to existing file
1010            let existing = fs::read_to_string(path)?;
1011            fs::write(path, format!("{formatted_content}\n\n---\n\n{existing}"))
1012        } else {
1013            // Create new file
1014            fs::write(path, &formatted_content)
1015        };
1016
1017        match result {
1018            Ok(()) => {
1019                update_spinner.finish_and_clear();
1020                ui::print_success(&format!(
1021                    "✨ Release notes successfully updated at {}",
1022                    release_notes_path.bright_green()
1023                ));
1024            }
1025            Err(e) => {
1026                update_spinner.finish_and_clear();
1027                ui::print_error(&format!("Failed to update release notes file: {e}"));
1028                return Err(e.into());
1029            }
1030        }
1031    }
1032
1033    Ok(())
1034}
1035
1036/// Handle the command based on parsed arguments
1037#[allow(clippy::too_many_lines)]
1038///
1039/// # Errors
1040///
1041/// Returns an error when the selected command fails.
1042pub async fn handle_command(
1043    command: Commands,
1044    repository_url: Option<String>,
1045) -> anyhow::Result<()> {
1046    match command {
1047        Commands::Gen {
1048            common,
1049            auto_commit,
1050            print,
1051            no_verify,
1052            amend,
1053        } => {
1054            // Get gitmoji setting from common params (--gitmoji/--no-gitmoji flags)
1055            // Default to true if not explicitly set
1056            let use_gitmoji = common.resolved_gitmoji().unwrap_or(true);
1057            handle_gen(
1058                common,
1059                GenConfig {
1060                    auto_commit,
1061                    use_gitmoji,
1062                    print_only: print,
1063                    verify: !no_verify,
1064                    amend,
1065                },
1066                repository_url,
1067            )
1068            .await
1069        }
1070        Commands::Config {
1071            common,
1072            api_key,
1073            fast_model,
1074            token_limit,
1075            param,
1076            subagent_timeout,
1077        } => handle_config(
1078            &common,
1079            api_key,
1080            common.model.clone(),
1081            fast_model,
1082            token_limit,
1083            param,
1084            subagent_timeout,
1085        ),
1086        Commands::Review {
1087            common,
1088            print,
1089            raw,
1090            include_unstaged,
1091            commit,
1092            from,
1093            to,
1094        } => {
1095            handle_review(
1096                common,
1097                print,
1098                raw,
1099                repository_url,
1100                include_unstaged,
1101                commit,
1102                from,
1103                to,
1104            )
1105            .await
1106        }
1107        Commands::Changelog {
1108            common,
1109            from,
1110            to,
1111            raw,
1112            update,
1113            file,
1114            version_name,
1115        } => {
1116            handle_changelog(
1117                common,
1118                from,
1119                to,
1120                raw,
1121                repository_url,
1122                update,
1123                file,
1124                version_name,
1125            )
1126            .await
1127        }
1128        Commands::ReleaseNotes {
1129            common,
1130            from,
1131            to,
1132            raw,
1133            update,
1134            file,
1135            version_name,
1136        } => {
1137            handle_release_notes(
1138                common,
1139                from,
1140                to,
1141                raw,
1142                repository_url,
1143                update,
1144                file,
1145                version_name,
1146            )
1147            .await
1148        }
1149        Commands::ProjectConfig {
1150            common,
1151            fast_model,
1152            token_limit,
1153            param,
1154            subagent_timeout,
1155            print,
1156        } => commands::handle_project_config_command(
1157            &common,
1158            common.model.clone(),
1159            fast_model,
1160            token_limit,
1161            param,
1162            subagent_timeout,
1163            print,
1164        ),
1165        Commands::ListPresets => commands::handle_list_presets_command(),
1166        Commands::Themes => {
1167            handle_themes();
1168            Ok(())
1169        }
1170        Commands::Completions { shell } => {
1171            handle_completions(shell);
1172            Ok(())
1173        }
1174        Commands::Hook { action } => commands::handle_hook_command(&action),
1175        Commands::Pr {
1176            common,
1177            print,
1178            raw,
1179            copy,
1180            from,
1181            to,
1182        } => handle_pr(common, print, raw, copy, from, to, repository_url).await,
1183        Commands::Studio {
1184            common,
1185            mode,
1186            from,
1187            to,
1188        } => handle_studio(common, mode, from, to, repository_url).await,
1189    }
1190}
1191
1192/// Handle the `Themes` command - list available themes
1193fn handle_themes() {
1194    ui::print_version(crate_version!());
1195    ui::print_newline();
1196
1197    let available = theme::list_available_themes();
1198    let current = theme::current();
1199    let current_name = &current.meta.name;
1200
1201    // Header
1202    let header_color = theme::current().color(tokens::ACCENT_PRIMARY);
1203    println!(
1204        "{}",
1205        "Available Themes:"
1206            .truecolor(header_color.r, header_color.g, header_color.b)
1207            .bold()
1208    );
1209    println!();
1210
1211    for info in available {
1212        let is_current = info.display_name == *current_name;
1213        let marker = if is_current { "● " } else { "  " };
1214
1215        let name_color = if is_current {
1216            theme::current().color(tokens::SUCCESS)
1217        } else {
1218            theme::current().color(tokens::ACCENT_SECONDARY)
1219        };
1220
1221        let desc_color = theme::current().color(tokens::TEXT_SECONDARY);
1222
1223        print!(
1224            "{}{}",
1225            marker.truecolor(name_color.r, name_color.g, name_color.b),
1226            info.name
1227                .truecolor(name_color.r, name_color.g, name_color.b)
1228                .bold()
1229        );
1230
1231        // Show display name if different from filename
1232        if info.display_name != info.name {
1233            print!(
1234                " ({})",
1235                info.display_name
1236                    .truecolor(desc_color.r, desc_color.g, desc_color.b)
1237            );
1238        }
1239
1240        // Show variant
1241        let variant_str = match info.variant {
1242            theme::ThemeVariant::Dark => "dark",
1243            theme::ThemeVariant::Light => "light",
1244        };
1245        let dim_color = theme::current().color(tokens::TEXT_DIM);
1246        print!(
1247            " [{}]",
1248            variant_str.truecolor(dim_color.r, dim_color.g, dim_color.b)
1249        );
1250
1251        if is_current {
1252            let active_color = theme::current().color(tokens::SUCCESS);
1253            print!(
1254                " {}",
1255                "(active)".truecolor(active_color.r, active_color.g, active_color.b)
1256            );
1257        }
1258
1259        println!();
1260    }
1261
1262    println!();
1263
1264    // Usage hint
1265    let hint_color = theme::current().color(tokens::TEXT_DIM);
1266    println!(
1267        "{}",
1268        "Use --theme <name> to override, or set 'theme' in config.toml".truecolor(
1269            hint_color.r,
1270            hint_color.g,
1271            hint_color.b
1272        )
1273    );
1274}
1275
1276/// Handle the `Completions` command - generate shell completion scripts
1277fn handle_completions(shell: Shell) {
1278    let mut cmd = Cli::command();
1279    generate(shell, &mut cmd, "git-iris", &mut io::stdout());
1280}
1281
1282/// Handle the `Pr` command with agent framework
1283async fn handle_pr_with_agent(
1284    common: CommonParams,
1285    print: bool,
1286    raw: bool,
1287    copy: bool,
1288    from: Option<String>,
1289    to: Option<String>,
1290    repository_url: Option<String>,
1291) -> anyhow::Result<()> {
1292    use crate::agents::{IrisAgentService, StructuredResponse, TaskContext};
1293    use crate::instruction_presets::PresetType;
1294    use arboard::Clipboard;
1295
1296    // Check if the preset is appropriate for PR descriptions (skip for raw output only)
1297    if !raw
1298        && !common.is_valid_preset_for_type(PresetType::Review)
1299        && !common.is_valid_preset_for_type(PresetType::Both)
1300    {
1301        ui::print_warning(
1302            "The specified preset may not be suitable for PR descriptions. Consider using a review or general preset instead.",
1303        );
1304        ui::print_info("Run 'git-iris list-presets' to see available presets for PRs.");
1305    }
1306
1307    // Create spinner for progress indication (skip for raw output only)
1308    let spinner = if raw {
1309        None
1310    } else {
1311        Some(ui::create_spinner("Initializing Iris..."))
1312    };
1313
1314    // Use IrisAgentService for agent execution
1315    let service = IrisAgentService::from_common_params(&common, repository_url)?;
1316    let default_base = service
1317        .git_repo()
1318        .and_then(|repo| repo.get_default_base_ref().ok())
1319        .unwrap_or_else(|| "main".to_string());
1320    let context = TaskContext::for_pr_with_base(from, to, &default_base);
1321    let response = service.execute_task("pr", context).await?;
1322
1323    // Finish spinner
1324    if let Some(s) = spinner {
1325        s.finish_and_clear();
1326    }
1327
1328    // Extract PR from response
1329    let StructuredResponse::PullRequest(generated_pr) = response else {
1330        return Err(anyhow::anyhow!("Expected pull request response"));
1331    };
1332
1333    // Handle clipboard copy
1334    if copy {
1335        let raw_content = generated_pr.raw_content();
1336        match Clipboard::new() {
1337            Ok(mut clipboard) => match clipboard.set_text(raw_content) {
1338                Ok(()) => {
1339                    ui::print_success("PR description copied to clipboard");
1340                }
1341                Err(e) => {
1342                    ui::print_error(&format!("Failed to copy to clipboard: {e}"));
1343                    // Fall back to printing raw
1344                    println!("{raw_content}");
1345                }
1346            },
1347            Err(e) => {
1348                ui::print_error(&format!("Clipboard unavailable: {e}"));
1349                // Fall back to printing raw
1350                println!("{raw_content}");
1351            }
1352        }
1353    } else if raw {
1354        // Raw markdown for piping to files or APIs
1355        println!("{}", generated_pr.raw_content());
1356    } else if print {
1357        // Formatted output for terminal viewing
1358        println!("{}", generated_pr.format());
1359    } else {
1360        ui::print_success("PR description generated successfully");
1361        println!("{}", generated_pr.format());
1362    }
1363
1364    Ok(())
1365}
1366
1367/// Handle the `Pr` command
1368async fn handle_pr(
1369    common: CommonParams,
1370    print: bool,
1371    raw: bool,
1372    copy: bool,
1373    from: Option<String>,
1374    to: Option<String>,
1375    repository_url: Option<String>,
1376) -> anyhow::Result<()> {
1377    log_debug!(
1378        "Handling 'pr' command with common: {:?}, print: {}, raw: {}, copy: {}, from: {:?}, to: {:?}",
1379        common,
1380        print,
1381        raw,
1382        copy,
1383        from,
1384        to
1385    );
1386
1387    // For raw output, skip version banner (piped output should be clean)
1388    // For copy mode, show the banner since we're giving user feedback
1389    if !raw {
1390        ui::print_version(crate_version!());
1391        ui::print_newline();
1392    }
1393
1394    handle_pr_with_agent(common, print, raw, copy, from, to, repository_url).await
1395}
1396
1397/// Handle the `Studio` command
1398#[allow(clippy::unused_async)] // Will need async when agent integration is complete
1399async fn handle_studio(
1400    common: CommonParams,
1401    mode: Option<String>,
1402    from: Option<String>,
1403    to: Option<String>,
1404    repository_url: Option<String>,
1405) -> anyhow::Result<()> {
1406    use crate::agents::IrisAgentService;
1407    use crate::config::Config;
1408    use crate::git::GitRepo;
1409    use crate::services::GitCommitService;
1410    use crate::studio::{Mode, run_studio};
1411    use anyhow::Context;
1412    use std::sync::Arc;
1413
1414    // Disable stdout logging immediately for TUI mode - it owns the terminal
1415    crate::logger::set_log_to_stdout(false);
1416
1417    log_debug!(
1418        "Handling 'studio' command with common: {:?}, mode: {:?}, from: {:?}, to: {:?}",
1419        common,
1420        mode,
1421        from,
1422        to
1423    );
1424
1425    let mut cfg = Config::load()?;
1426    common.apply_to_config(&mut cfg)?;
1427
1428    // Create git repo
1429    let repo_url = repository_url.clone().or(common.repository_url.clone());
1430    let git_repo =
1431        Arc::new(GitRepo::new_from_url(repo_url.clone()).context("Failed to create GitRepo")?);
1432
1433    // Create services
1434    let commit_service = Arc::new(GitCommitService::new(
1435        git_repo.clone(),
1436        cfg.use_gitmoji,
1437        true, // verify hooks
1438    ));
1439
1440    let agent_service = Arc::new(IrisAgentService::from_common_params(
1441        &common,
1442        repository_url,
1443    )?);
1444
1445    // Parse initial mode
1446    let initial_mode = mode
1447        .as_deref()
1448        .and_then(|m| match m.to_lowercase().as_str() {
1449            "explore" => Some(Mode::Explore),
1450            "commit" => Some(Mode::Commit),
1451            "review" => Some(Mode::Review),
1452            "pr" => Some(Mode::PR),
1453            "changelog" => Some(Mode::Changelog),
1454            "release-notes" | "release_notes" => Some(Mode::ReleaseNotes),
1455            _ => {
1456                ui::print_warning(&format!("Unknown mode '{}', using auto-detect", m));
1457                None
1458            }
1459        });
1460
1461    run_studio(
1462        cfg,
1463        Some(git_repo),
1464        Some(commit_service),
1465        Some(agent_service),
1466        initial_mode,
1467        from,
1468        to,
1469    )
1470}