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