git_iris/
cli.rs

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