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 'main')
158        #[arg(
159            long,
160            help = "Starting branch for comparison (defaults to 'main'). Used with --to for 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"
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 main --to feature-branch\n• From main to branch: --to feature-branch\n\nSupported commitish syntax: HEAD~2, HEAD^, @~3, main~1, origin/main^, 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"
301        )]
302        mode: Option<String>,
303
304        /// Starting ref for PR/changelog comparison (defaults to main/master)
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
409/// Define custom styles for Clap
410fn get_styles() -> Styles {
411    Styles::styled()
412        .header(AnsiColor::Magenta.on_default().bold())
413        .usage(AnsiColor::Cyan.on_default().bold())
414        .literal(AnsiColor::Green.on_default().bold())
415        .placeholder(AnsiColor::Yellow.on_default())
416        .valid(AnsiColor::Blue.on_default().bold())
417        .invalid(AnsiColor::Red.on_default().bold())
418        .error(AnsiColor::Red.on_default().bold())
419}
420
421/// Parse the command-line arguments
422pub fn parse_args() -> Cli {
423    Cli::parse()
424}
425
426/// Generate dynamic help including available LLM providers
427fn get_dynamic_help() -> String {
428    let providers_list = Provider::all_names()
429        .iter()
430        .map(|p| format!("{}", p.bold()))
431        .collect::<Vec<_>>()
432        .join(" • ");
433
434    format!("\nAvailable LLM Providers: {providers_list}")
435}
436
437/// Main function to parse arguments and handle the command
438pub async fn main() -> anyhow::Result<()> {
439    let cli = parse_args();
440
441    if cli.version {
442        ui::print_version(crate_version!());
443        return Ok(());
444    }
445
446    if cli.log {
447        crate::logger::enable_logging();
448        crate::logger::set_log_to_stdout(true);
449        let log_file = cli.log_file.as_deref().unwrap_or(LOG_FILE);
450        crate::logger::set_log_file(log_file)?;
451        log_debug!("Debug logging enabled");
452    } else {
453        crate::logger::disable_logging();
454    }
455
456    // Set quiet mode in the UI module
457    if cli.quiet {
458        crate::ui::set_quiet_mode(true);
459    }
460
461    // Initialize theme
462    initialize_theme(cli.theme.as_deref());
463
464    // Enable debug mode if requested
465    if cli.debug {
466        crate::agents::debug::enable_debug_mode();
467        crate::agents::debug::debug_header("🔮 IRIS DEBUG MODE ACTIVATED 🔮");
468    }
469
470    if let Some(command) = cli.command {
471        handle_command(command, cli.repository_url).await
472    } else {
473        // Default: launch Studio with auto-detect mode
474        handle_studio(
475            CommonParams::default(),
476            None,
477            None,
478            None,
479            cli.repository_url,
480        )
481        .await
482    }
483}
484
485/// Initialize the theme from CLI flag or config
486fn initialize_theme(cli_theme: Option<&str>) {
487    use crate::config::Config;
488
489    // CLI flag takes precedence
490    let theme_name = if let Some(name) = cli_theme {
491        Some(name.to_string())
492    } else {
493        // Try to load from config
494        Config::load().ok().and_then(|c| {
495            if c.theme.is_empty() {
496                None
497            } else {
498                Some(c.theme)
499            }
500        })
501    };
502
503    // Load the theme if specified, otherwise default is already active
504    if let Some(name) = theme_name {
505        if let Err(e) = theme::load_theme_by_name(&name) {
506            ui::print_warning(&format!(
507                "Failed to load theme '{}': {}. Using default.",
508                name, e
509            ));
510        } else {
511            log_debug!("Loaded theme: {}", name);
512        }
513    }
514}
515
516/// Configuration for the Gen command
517#[allow(clippy::struct_excessive_bools)]
518struct GenConfig {
519    auto_commit: bool,
520    use_gitmoji: bool,
521    print_only: bool,
522    verify: bool,
523    amend: bool,
524}
525
526/// Handle the `Gen` command with agent framework and Studio integration
527#[allow(clippy::too_many_lines)]
528async fn handle_gen_with_agent(
529    common: CommonParams,
530    config: GenConfig,
531    repository_url: Option<String>,
532) -> anyhow::Result<()> {
533    use crate::agents::{IrisAgentService, StructuredResponse, TaskContext};
534    use crate::config::Config;
535    use crate::git::GitRepo;
536    use crate::instruction_presets::PresetType;
537    use crate::output::format_commit_result;
538    use crate::services::GitCommitService;
539    use crate::studio::{Mode, run_studio};
540    use crate::types::format_commit_message;
541    use anyhow::Context;
542    use std::sync::Arc;
543
544    // Check if the preset is appropriate for commit messages
545    if !common.is_valid_preset_for_type(PresetType::Commit) {
546        ui::print_warning(
547            "The specified preset may not be suitable for commit messages. Consider using a commit or general preset instead.",
548        );
549        ui::print_info("Run 'git-iris list-presets' to see available presets for commits.");
550    }
551
552    // Amend mode requires --print or --auto-commit (Studio amend support coming later)
553    if config.amend && !config.print_only && !config.auto_commit {
554        ui::print_warning("--amend requires --print or --auto-commit for now.");
555        ui::print_info("Example: git-iris gen --amend --auto-commit");
556        return Ok(());
557    }
558
559    let mut cfg = Config::load()?;
560    common.apply_to_config(&mut cfg)?;
561
562    // Create git repo and services
563    let repo_url = repository_url.clone().or(common.repository_url.clone());
564    let git_repo = Arc::new(GitRepo::new_from_url(repo_url).context("Failed to create GitRepo")?);
565    let use_gitmoji = config.use_gitmoji && cfg.use_gitmoji;
566
567    // Create GitCommitService for commit operations
568    let commit_service = Arc::new(GitCommitService::new(
569        git_repo.clone(),
570        use_gitmoji,
571        config.verify,
572    ));
573
574    // Create IrisAgentService for LLM operations
575    let agent_service = Arc::new(IrisAgentService::from_common_params(
576        &common,
577        repository_url.clone(),
578    )?);
579
580    // Get git info for staged files check
581    let git_info = git_repo.get_git_info(&cfg)?;
582
583    // For --print or --auto-commit, we need to generate the message first
584    if config.print_only || config.auto_commit {
585        // For amend mode, we allow empty staged changes (amending message only)
586        // For regular commits, we require staged changes
587        if git_info.staged_files.is_empty() && !config.amend {
588            ui::print_warning(
589                "No staged changes. Please stage your changes before generating a commit message.",
590            );
591            ui::print_info("You can stage changes using 'git add <file>' or 'git add .'");
592            return Ok(());
593        }
594
595        // Run pre-commit hook before we do anything else
596        if let Err(e) = commit_service.pre_commit() {
597            ui::print_error(&format!("Pre-commit failed: {e}"));
598            return Err(e);
599        }
600
601        // Create spinner for agent mode
602        let spinner_msg = if config.amend {
603            "Generating amended commit message..."
604        } else {
605            "Generating commit message..."
606        };
607        let spinner = ui::create_spinner(spinner_msg);
608
609        // Use IrisAgentService for commit message generation
610        // For amend, we pass the original message as context
611        let context = if config.amend {
612            let original_message = commit_service.get_head_commit_message().unwrap_or_default();
613            TaskContext::for_amend(original_message)
614        } else {
615            TaskContext::for_gen()
616        };
617        let response = agent_service.execute_task("commit", context).await?;
618
619        // Extract commit message from response
620        let StructuredResponse::CommitMessage(generated_message) = response else {
621            return Err(anyhow::anyhow!("Expected commit message response"));
622        };
623
624        // Finish spinner after agent completes
625        spinner.finish_and_clear();
626
627        if config.print_only {
628            println!("{}", format_commit_message(&generated_message));
629            return Ok(());
630        }
631
632        // Auto-commit/amend mode
633        if commit_service.is_remote() {
634            ui::print_error(
635                "Cannot automatically commit to a remote repository. Use --print instead.",
636            );
637            return Err(anyhow::anyhow!(
638                "Auto-commit not supported for remote repositories"
639            ));
640        }
641
642        let commit_result = if config.amend {
643            commit_service.perform_amend(&format_commit_message(&generated_message))
644        } else {
645            commit_service.perform_commit(&format_commit_message(&generated_message))
646        };
647
648        match commit_result {
649            Ok(result) => {
650                let output =
651                    format_commit_result(&result, &format_commit_message(&generated_message));
652                println!("{output}");
653            }
654            Err(e) => {
655                let action = if config.amend { "amend" } else { "commit" };
656                eprintln!("Failed to {action}: {e}");
657                return Err(e);
658            }
659        }
660        return Ok(());
661    }
662
663    // Interactive mode: launch Studio (it handles staged check and auto-generation)
664    if commit_service.is_remote() {
665        ui::print_warning(
666            "Interactive commit not available for remote repositories. Use --print instead.",
667        );
668        return Ok(());
669    }
670
671    // Launch Studio in Commit mode - it will auto-generate if there are staged changes
672    run_studio(
673        cfg,
674        Some(git_repo),
675        Some(commit_service),
676        Some(agent_service),
677        Some(Mode::Commit),
678        None,
679        None,
680    )
681}
682
683/// Handle the `Gen` command
684async fn handle_gen(
685    common: CommonParams,
686    config: GenConfig,
687    repository_url: Option<String>,
688) -> anyhow::Result<()> {
689    log_debug!(
690        "Handling 'gen' command with common: {:?}, auto_commit: {}, use_gitmoji: {}, print: {}, verify: {}, amend: {}",
691        common,
692        config.auto_commit,
693        config.use_gitmoji,
694        config.print_only,
695        config.verify,
696        config.amend
697    );
698
699    ui::print_version(crate_version!());
700    ui::print_newline();
701
702    handle_gen_with_agent(common, config, repository_url).await
703}
704
705/// Handle the `Config` command
706fn handle_config(
707    common: &CommonParams,
708    api_key: Option<String>,
709    model: Option<String>,
710    fast_model: Option<String>,
711    token_limit: Option<usize>,
712    param: Option<Vec<String>>,
713    subagent_timeout: Option<u64>,
714) -> anyhow::Result<()> {
715    log_debug!(
716        "Handling 'config' command with common: {:?}, api_key: {:?}, model: {:?}, token_limit: {:?}, param: {:?}, subagent_timeout: {:?}",
717        common,
718        api_key,
719        model,
720        token_limit,
721        param,
722        subagent_timeout
723    );
724    commands::handle_config_command(
725        common,
726        api_key,
727        model,
728        fast_model,
729        token_limit,
730        param,
731        subagent_timeout,
732    )
733}
734
735/// Handle the `Review` command
736#[allow(clippy::too_many_arguments)]
737async fn handle_review(
738    common: CommonParams,
739    print: bool,
740    raw: bool,
741    repository_url: Option<String>,
742    include_unstaged: bool,
743    commit: Option<String>,
744    from: Option<String>,
745    to: Option<String>,
746) -> anyhow::Result<()> {
747    log_debug!(
748        "Handling 'review' command with common: {:?}, print: {}, raw: {}, include_unstaged: {}, commit: {:?}, from: {:?}, to: {:?}",
749        common,
750        print,
751        raw,
752        include_unstaged,
753        commit,
754        from,
755        to
756    );
757
758    // For raw output, skip all formatting
759    if !raw {
760        ui::print_version(crate_version!());
761        ui::print_newline();
762    }
763
764    use crate::agents::{IrisAgentService, TaskContext};
765
766    // Validate parameters and create structured context
767    let context = TaskContext::for_review(commit, from, to, include_unstaged)?;
768
769    // Create spinner for progress indication (skip for raw output)
770    let spinner = if raw {
771        None
772    } else {
773        Some(ui::create_spinner("Initializing Iris..."))
774    };
775
776    // Use IrisAgentService for agent execution
777    let service = IrisAgentService::from_common_params(&common, repository_url)?;
778    let response = service.execute_task("review", context).await?;
779
780    // Finish spinner
781    if let Some(s) = spinner {
782        s.finish_and_clear();
783    }
784
785    if raw || print {
786        println!("{response}");
787    } else {
788        ui::print_success("Code review completed successfully");
789        println!("{response}");
790    }
791    Ok(())
792}
793
794/// Handle the `Changelog` command
795#[allow(clippy::too_many_arguments)]
796async fn handle_changelog(
797    common: CommonParams,
798    from: String,
799    to: Option<String>,
800    raw: bool,
801    repository_url: Option<String>,
802    update: bool,
803    file: Option<String>,
804    version_name: Option<String>,
805) -> anyhow::Result<()> {
806    log_debug!(
807        "Handling 'changelog' command with common: {:?}, from: {}, to: {:?}, raw: {}, update: {}, file: {:?}, version_name: {:?}",
808        common,
809        from,
810        to,
811        raw,
812        update,
813        file,
814        version_name
815    );
816
817    // For raw output, skip all formatting
818    if !raw {
819        ui::print_version(crate_version!());
820        ui::print_newline();
821    }
822
823    use crate::agents::{IrisAgentService, TaskContext};
824    use crate::changelog::ChangelogGenerator;
825    use crate::git::GitRepo;
826    use anyhow::Context;
827    use std::sync::Arc;
828
829    // Create structured context for changelog with version_name and current date
830    let context = TaskContext::for_changelog(from.clone(), to.clone(), version_name.clone(), None);
831    let to_ref = to.unwrap_or_else(|| "HEAD".to_string());
832
833    // Create spinner for progress indication (skip for raw output)
834    let spinner = if raw {
835        None
836    } else {
837        Some(ui::create_spinner("Initializing Iris..."))
838    };
839
840    // Use IrisAgentService for agent execution
841    let service = IrisAgentService::from_common_params(&common, repository_url.clone())?;
842    let response = service.execute_task("changelog", context).await?;
843
844    // Finish spinner
845    if let Some(s) = spinner {
846        s.finish_and_clear();
847    }
848
849    // Print the changelog
850    println!("{response}");
851
852    if update {
853        // Extract the formatted content for file update
854        let formatted_content = response.to_string();
855        let changelog_path = file.unwrap_or_else(|| "CHANGELOG.md".to_string());
856        let repo_url_for_update = repository_url.or(common.repository_url.clone());
857
858        // Create GitRepo for file update
859        let git_repo = if let Some(url) = repo_url_for_update {
860            Arc::new(
861                GitRepo::clone_remote_repository(&url)
862                    .context("Failed to clone repository for changelog update")?,
863            )
864        } else {
865            let repo_path = std::env::current_dir()?;
866            Arc::new(
867                GitRepo::new(&repo_path)
868                    .context("Failed to create GitRepo for changelog update")?,
869            )
870        };
871
872        // Update changelog file
873        let update_spinner =
874            ui::create_spinner(&format!("Updating changelog file at {changelog_path}..."));
875
876        match ChangelogGenerator::update_changelog_file(
877            &formatted_content,
878            &changelog_path,
879            &git_repo,
880            &to_ref,
881            version_name,
882        ) {
883            Ok(()) => {
884                update_spinner.finish_and_clear();
885                ui::print_success(&format!(
886                    "✨ Changelog successfully updated at {}",
887                    changelog_path.bright_green()
888                ));
889            }
890            Err(e) => {
891                update_spinner.finish_and_clear();
892                ui::print_error(&format!("Failed to update changelog file: {e}"));
893                return Err(e);
894            }
895        }
896    }
897    Ok(())
898}
899
900/// Handle the `Release Notes` command
901#[allow(clippy::too_many_arguments)]
902async fn handle_release_notes(
903    common: CommonParams,
904    from: String,
905    to: Option<String>,
906    raw: bool,
907    repository_url: Option<String>,
908    update: bool,
909    file: Option<String>,
910    version_name: Option<String>,
911) -> anyhow::Result<()> {
912    log_debug!(
913        "Handling 'release-notes' command with common: {:?}, from: {}, to: {:?}, raw: {}, update: {}, file: {:?}, version_name: {:?}",
914        common,
915        from,
916        to,
917        raw,
918        update,
919        file,
920        version_name
921    );
922
923    // For raw output, skip all formatting
924    if !raw {
925        ui::print_version(crate_version!());
926        ui::print_newline();
927    }
928
929    use crate::agents::{IrisAgentService, TaskContext};
930    use std::fs;
931    use std::path::Path;
932
933    // Create structured context for release notes with version_name and current date
934    let context = TaskContext::for_changelog(from, to, version_name, None);
935
936    // Create spinner for progress indication (skip for raw output)
937    let spinner = if raw {
938        None
939    } else {
940        Some(ui::create_spinner("Initializing Iris..."))
941    };
942
943    // Use IrisAgentService for agent execution
944    let service = IrisAgentService::from_common_params(&common, repository_url)?;
945    let response = service.execute_task("release_notes", context).await?;
946
947    // Finish spinner
948    if let Some(s) = spinner {
949        s.finish_and_clear();
950    }
951
952    println!("{response}");
953
954    // Handle --update flag
955    if update {
956        let release_notes_path = file.unwrap_or_else(|| "RELEASE_NOTES.md".to_string());
957        let formatted_content = response.to_string();
958
959        let update_spinner = ui::create_spinner(&format!(
960            "Updating release notes file at {release_notes_path}..."
961        ));
962
963        // Write or append to file
964        let path = Path::new(&release_notes_path);
965        let result = if path.exists() {
966            // Prepend to existing file
967            let existing = fs::read_to_string(path)?;
968            fs::write(path, format!("{formatted_content}\n\n---\n\n{existing}"))
969        } else {
970            // Create new file
971            fs::write(path, &formatted_content)
972        };
973
974        match result {
975            Ok(()) => {
976                update_spinner.finish_and_clear();
977                ui::print_success(&format!(
978                    "✨ Release notes successfully updated at {}",
979                    release_notes_path.bright_green()
980                ));
981            }
982            Err(e) => {
983                update_spinner.finish_and_clear();
984                ui::print_error(&format!("Failed to update release notes file: {e}"));
985                return Err(e.into());
986            }
987        }
988    }
989
990    Ok(())
991}
992
993/// Handle the command based on parsed arguments
994#[allow(clippy::too_many_lines)]
995pub async fn handle_command(
996    command: Commands,
997    repository_url: Option<String>,
998) -> anyhow::Result<()> {
999    match command {
1000        Commands::Gen {
1001            common,
1002            auto_commit,
1003            print,
1004            no_verify,
1005            amend,
1006        } => {
1007            // Get gitmoji setting from common params (--gitmoji/--no-gitmoji flags)
1008            // Default to true if not explicitly set
1009            let use_gitmoji = common.resolved_gitmoji().unwrap_or(true);
1010            handle_gen(
1011                common,
1012                GenConfig {
1013                    auto_commit,
1014                    use_gitmoji,
1015                    print_only: print,
1016                    verify: !no_verify,
1017                    amend,
1018                },
1019                repository_url,
1020            )
1021            .await
1022        }
1023        Commands::Config {
1024            common,
1025            api_key,
1026            fast_model,
1027            token_limit,
1028            param,
1029            subagent_timeout,
1030        } => handle_config(
1031            &common,
1032            api_key,
1033            common.model.clone(),
1034            fast_model,
1035            token_limit,
1036            param,
1037            subagent_timeout,
1038        ),
1039        Commands::Review {
1040            common,
1041            print,
1042            raw,
1043            include_unstaged,
1044            commit,
1045            from,
1046            to,
1047        } => {
1048            handle_review(
1049                common,
1050                print,
1051                raw,
1052                repository_url,
1053                include_unstaged,
1054                commit,
1055                from,
1056                to,
1057            )
1058            .await
1059        }
1060        Commands::Changelog {
1061            common,
1062            from,
1063            to,
1064            raw,
1065            update,
1066            file,
1067            version_name,
1068        } => {
1069            handle_changelog(
1070                common,
1071                from,
1072                to,
1073                raw,
1074                repository_url,
1075                update,
1076                file,
1077                version_name,
1078            )
1079            .await
1080        }
1081        Commands::ReleaseNotes {
1082            common,
1083            from,
1084            to,
1085            raw,
1086            update,
1087            file,
1088            version_name,
1089        } => {
1090            handle_release_notes(
1091                common,
1092                from,
1093                to,
1094                raw,
1095                repository_url,
1096                update,
1097                file,
1098                version_name,
1099            )
1100            .await
1101        }
1102        Commands::ProjectConfig {
1103            common,
1104            fast_model,
1105            token_limit,
1106            param,
1107            subagent_timeout,
1108            print,
1109        } => commands::handle_project_config_command(
1110            &common,
1111            common.model.clone(),
1112            fast_model,
1113            token_limit,
1114            param,
1115            subagent_timeout,
1116            print,
1117        ),
1118        Commands::ListPresets => commands::handle_list_presets_command(),
1119        Commands::Themes => {
1120            handle_themes();
1121            Ok(())
1122        }
1123        Commands::Completions { shell } => {
1124            handle_completions(shell);
1125            Ok(())
1126        }
1127        Commands::Pr {
1128            common,
1129            print,
1130            raw,
1131            copy,
1132            from,
1133            to,
1134        } => handle_pr(common, print, raw, copy, from, to, repository_url).await,
1135        Commands::Studio {
1136            common,
1137            mode,
1138            from,
1139            to,
1140        } => handle_studio(common, mode, from, to, repository_url).await,
1141    }
1142}
1143
1144/// Handle the `Themes` command - list available themes
1145fn handle_themes() {
1146    ui::print_version(crate_version!());
1147    ui::print_newline();
1148
1149    let available = theme::list_available_themes();
1150    let current = theme::current();
1151    let current_name = &current.meta.name;
1152
1153    // Header
1154    let header_color = theme::current().color(tokens::ACCENT_PRIMARY);
1155    println!(
1156        "{}",
1157        "Available Themes:"
1158            .truecolor(header_color.r, header_color.g, header_color.b)
1159            .bold()
1160    );
1161    println!();
1162
1163    for info in available {
1164        let is_current = info.display_name == *current_name;
1165        let marker = if is_current { "● " } else { "  " };
1166
1167        let name_color = if is_current {
1168            theme::current().color(tokens::SUCCESS)
1169        } else {
1170            theme::current().color(tokens::ACCENT_SECONDARY)
1171        };
1172
1173        let desc_color = theme::current().color(tokens::TEXT_SECONDARY);
1174
1175        print!(
1176            "{}{}",
1177            marker.truecolor(name_color.r, name_color.g, name_color.b),
1178            info.name
1179                .truecolor(name_color.r, name_color.g, name_color.b)
1180                .bold()
1181        );
1182
1183        // Show display name if different from filename
1184        if info.display_name != info.name {
1185            print!(
1186                " ({})",
1187                info.display_name
1188                    .truecolor(desc_color.r, desc_color.g, desc_color.b)
1189            );
1190        }
1191
1192        // Show variant
1193        let variant_str = match info.variant {
1194            theme::ThemeVariant::Dark => "dark",
1195            theme::ThemeVariant::Light => "light",
1196        };
1197        let dim_color = theme::current().color(tokens::TEXT_DIM);
1198        print!(
1199            " [{}]",
1200            variant_str.truecolor(dim_color.r, dim_color.g, dim_color.b)
1201        );
1202
1203        if is_current {
1204            let active_color = theme::current().color(tokens::SUCCESS);
1205            print!(
1206                " {}",
1207                "(active)".truecolor(active_color.r, active_color.g, active_color.b)
1208            );
1209        }
1210
1211        println!();
1212    }
1213
1214    println!();
1215
1216    // Usage hint
1217    let hint_color = theme::current().color(tokens::TEXT_DIM);
1218    println!(
1219        "{}",
1220        "Use --theme <name> to override, or set 'theme' in config.toml".truecolor(
1221            hint_color.r,
1222            hint_color.g,
1223            hint_color.b
1224        )
1225    );
1226}
1227
1228/// Handle the `Completions` command - generate shell completion scripts
1229fn handle_completions(shell: Shell) {
1230    let mut cmd = Cli::command();
1231    generate(shell, &mut cmd, "git-iris", &mut io::stdout());
1232}
1233
1234/// Handle the `Pr` command with agent framework
1235async fn handle_pr_with_agent(
1236    common: CommonParams,
1237    print: bool,
1238    raw: bool,
1239    copy: bool,
1240    from: Option<String>,
1241    to: Option<String>,
1242    repository_url: Option<String>,
1243) -> anyhow::Result<()> {
1244    use crate::agents::{IrisAgentService, StructuredResponse, TaskContext};
1245    use crate::instruction_presets::PresetType;
1246    use arboard::Clipboard;
1247
1248    // Check if the preset is appropriate for PR descriptions (skip for raw output only)
1249    if !raw
1250        && !common.is_valid_preset_for_type(PresetType::Review)
1251        && !common.is_valid_preset_for_type(PresetType::Both)
1252    {
1253        ui::print_warning(
1254            "The specified preset may not be suitable for PR descriptions. Consider using a review or general preset instead.",
1255        );
1256        ui::print_info("Run 'git-iris list-presets' to see available presets for PRs.");
1257    }
1258
1259    // Create structured context for PR (handles defaults: from=main, to=HEAD)
1260    let context = TaskContext::for_pr(from, to);
1261
1262    // Create spinner for progress indication (skip for raw output only)
1263    let spinner = if raw {
1264        None
1265    } else {
1266        Some(ui::create_spinner("Initializing Iris..."))
1267    };
1268
1269    // Use IrisAgentService for agent execution
1270    let service = IrisAgentService::from_common_params(&common, repository_url)?;
1271    let response = service.execute_task("pr", context).await?;
1272
1273    // Finish spinner
1274    if let Some(s) = spinner {
1275        s.finish_and_clear();
1276    }
1277
1278    // Extract PR from response
1279    let StructuredResponse::PullRequest(generated_pr) = response else {
1280        return Err(anyhow::anyhow!("Expected pull request response"));
1281    };
1282
1283    // Handle clipboard copy
1284    if copy {
1285        let raw_content = generated_pr.raw_content();
1286        match Clipboard::new() {
1287            Ok(mut clipboard) => match clipboard.set_text(raw_content) {
1288                Ok(()) => {
1289                    ui::print_success("PR description copied to clipboard");
1290                }
1291                Err(e) => {
1292                    ui::print_error(&format!("Failed to copy to clipboard: {e}"));
1293                    // Fall back to printing raw
1294                    println!("{raw_content}");
1295                }
1296            },
1297            Err(e) => {
1298                ui::print_error(&format!("Clipboard unavailable: {e}"));
1299                // Fall back to printing raw
1300                println!("{raw_content}");
1301            }
1302        }
1303    } else if raw {
1304        // Raw markdown for piping to files or APIs
1305        println!("{}", generated_pr.raw_content());
1306    } else if print {
1307        // Formatted output for terminal viewing
1308        println!("{}", generated_pr.format());
1309    } else {
1310        ui::print_success("PR description generated successfully");
1311        println!("{}", generated_pr.format());
1312    }
1313
1314    Ok(())
1315}
1316
1317/// Handle the `Pr` command
1318async fn handle_pr(
1319    common: CommonParams,
1320    print: bool,
1321    raw: bool,
1322    copy: bool,
1323    from: Option<String>,
1324    to: Option<String>,
1325    repository_url: Option<String>,
1326) -> anyhow::Result<()> {
1327    log_debug!(
1328        "Handling 'pr' command with common: {:?}, print: {}, raw: {}, copy: {}, from: {:?}, to: {:?}",
1329        common,
1330        print,
1331        raw,
1332        copy,
1333        from,
1334        to
1335    );
1336
1337    // For raw output, skip version banner (piped output should be clean)
1338    // For copy mode, show the banner since we're giving user feedback
1339    if !raw {
1340        ui::print_version(crate_version!());
1341        ui::print_newline();
1342    }
1343
1344    handle_pr_with_agent(common, print, raw, copy, from, to, repository_url).await
1345}
1346
1347/// Handle the `Studio` command
1348#[allow(clippy::unused_async)] // Will need async when agent integration is complete
1349async fn handle_studio(
1350    common: CommonParams,
1351    mode: Option<String>,
1352    from: Option<String>,
1353    to: Option<String>,
1354    repository_url: Option<String>,
1355) -> anyhow::Result<()> {
1356    use crate::agents::IrisAgentService;
1357    use crate::config::Config;
1358    use crate::git::GitRepo;
1359    use crate::services::GitCommitService;
1360    use crate::studio::{Mode, run_studio};
1361    use anyhow::Context;
1362    use std::sync::Arc;
1363
1364    // Disable stdout logging immediately for TUI mode - it owns the terminal
1365    crate::logger::set_log_to_stdout(false);
1366
1367    log_debug!(
1368        "Handling 'studio' command with common: {:?}, mode: {:?}, from: {:?}, to: {:?}",
1369        common,
1370        mode,
1371        from,
1372        to
1373    );
1374
1375    let mut cfg = Config::load()?;
1376    common.apply_to_config(&mut cfg)?;
1377
1378    // Create git repo
1379    let repo_url = repository_url.clone().or(common.repository_url.clone());
1380    let git_repo =
1381        Arc::new(GitRepo::new_from_url(repo_url.clone()).context("Failed to create GitRepo")?);
1382
1383    // Create services
1384    let commit_service = Arc::new(GitCommitService::new(
1385        git_repo.clone(),
1386        cfg.use_gitmoji,
1387        true, // verify hooks
1388    ));
1389
1390    let agent_service = Arc::new(IrisAgentService::from_common_params(
1391        &common,
1392        repository_url,
1393    )?);
1394
1395    // Parse initial mode
1396    let initial_mode = mode
1397        .as_deref()
1398        .and_then(|m| match m.to_lowercase().as_str() {
1399            "explore" => Some(Mode::Explore),
1400            "commit" => Some(Mode::Commit),
1401            "review" => Some(Mode::Review),
1402            "pr" => Some(Mode::PR),
1403            "changelog" => Some(Mode::Changelog),
1404            _ => {
1405                ui::print_warning(&format!("Unknown mode '{}', using auto-detect", m));
1406                None
1407            }
1408        });
1409
1410    run_studio(
1411        cfg,
1412        Some(git_repo),
1413        Some(commit_service),
1414        Some(agent_service),
1415        initial_mode,
1416        from,
1417        to,
1418    )
1419}