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
13pub const LOG_FILE: &str = "git-iris-debug.log";
15
16#[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 #[command(subcommand)]
31 pub command: Option<Commands>,
32
33 #[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 #[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 #[arg(
52 short = 'q',
53 long = "quiet",
54 global = true,
55 help = "Suppress non-essential output"
56 )]
57 pub quiet: bool,
58
59 #[arg(
61 short = 'v',
62 long = "version",
63 global = true,
64 help = "Display the version"
65 )]
66 pub version: bool,
67
68 #[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 #[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 #[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#[derive(Subcommand)]
96#[command(subcommand_negates_reqs = true)]
97#[command(subcommand_precedence_over_arg = true)]
98pub enum Commands {
99 #[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 #[arg(short, long, help = "Automatically commit with the generated message")]
113 auto_commit: bool,
114
115 #[arg(short, long, help = "Print the generated message to stdout and exit")]
117 print: bool,
118
119 #[arg(long, help = "Skip verification steps (pre/post commit hooks)")]
121 no_verify: bool,
122
123 #[arg(long, help = "Amend the previous commit with staged changes")]
125 amend: bool,
126 },
127
128 #[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 #[arg(short, long, help = "Print the generated review to stdout and exit")]
139 print: bool,
140
141 #[arg(long, help = "Output raw markdown without any console formatting")]
143 raw: bool,
144
145 #[arg(long, help = "Include unstaged changes in the review")]
147 include_unstaged: bool,
148
149 #[arg(
151 long,
152 help = "Review a specific commit by ID (hash, branch, or reference)"
153 )]
154 commit: Option<String>,
155
156 #[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 #[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 #[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 #[arg(
182 short,
183 long,
184 help = "Print the generated PR description to stdout and exit"
185 )]
186 print: bool,
187
188 #[arg(long, help = "Output raw markdown without any console formatting")]
190 raw: bool,
191
192 #[arg(
194 short,
195 long,
196 help = "Copy raw markdown to clipboard (for pasting into GitHub/GitLab)"
197 )]
198 copy: bool,
199
200 #[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 #[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 #[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 #[arg(long, required = true)]
226 from: String,
227
228 #[arg(long)]
230 to: Option<String>,
231
232 #[arg(long, help = "Output raw markdown without any console formatting")]
234 raw: bool,
235
236 #[arg(long, help = "Update the changelog file with the new changes")]
238 update: bool,
239
240 #[arg(long, help = "Path to the changelog file (defaults to CHANGELOG.md)")]
242 file: Option<String>,
243
244 #[arg(long, help = "Explicit version name to use in the changelog")]
246 version_name: Option<String>,
247 },
248
249 #[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 #[arg(long, required = true)]
260 from: String,
261
262 #[arg(long)]
264 to: Option<String>,
265
266 #[arg(long, help = "Output raw markdown without any console formatting")]
268 raw: bool,
269
270 #[arg(long, help = "Update the release notes file with the new content")]
272 update: bool,
273
274 #[arg(
276 long,
277 help = "Path to the release notes file (defaults to RELEASE_NOTES.md)"
278 )]
279 file: Option<String>,
280
281 #[arg(long, help = "Explicit version name to use in the release notes")]
283 version_name: Option<String>,
284 },
285
286 #[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 #[arg(
297 long,
298 value_name = "MODE",
299 help = "Initial mode: explore, commit, review, pr, changelog"
300 )]
301 mode: Option<String>,
302
303 #[arg(long, value_name = "REF", help = "Starting ref for comparison")]
305 from: Option<String>,
306
307 #[arg(long, value_name = "REF", help = "Ending ref for comparison")]
309 to: Option<String>,
310 },
311
312 #[command(about = "Configure Git-Iris settings and providers")]
315 Config {
316 #[command(flatten)]
317 common: CommonParams,
318
319 #[arg(long, help = "Set API key for the specified provider")]
321 api_key: Option<String>,
322
323 #[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 #[arg(long, help = "Set token limit for the specified provider")]
332 token_limit: Option<usize>,
333
334 #[arg(
336 long,
337 help = "Set additional parameters for the specified provider (key=value)"
338 )]
339 param: Option<Vec<String>>,
340
341 #[arg(
343 long,
344 help = "Set timeout in seconds for parallel subagent tasks (default: 120)"
345 )]
346 subagent_timeout: Option<u64>,
347 },
348
349 #[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 #[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 #[arg(long, help = "Set token limit for the specified provider")]
367 token_limit: Option<usize>,
368
369 #[arg(
371 long,
372 help = "Set additional parameters for the specified provider (key=value)"
373 )]
374 param: Option<Vec<String>>,
375
376 #[arg(
378 long,
379 help = "Set timeout in seconds for parallel subagent tasks (default: 120)"
380 )]
381 subagent_timeout: Option<u64>,
382
383 #[arg(short, long, help = "Print the current project configuration")]
385 print: bool,
386 },
387
388 #[command(about = "List available instruction presets")]
390 ListPresets,
391
392 #[command(about = "List available themes")]
394 Themes,
395
396 #[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 #[arg(value_enum)]
404 shell: Shell,
405 },
406}
407
408fn 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
420pub fn parse_args() -> Cli {
422 Cli::parse()
423}
424
425fn 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
436pub 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 if cli.quiet {
457 crate::ui::set_quiet_mode(true);
458 }
459
460 initialize_theme(cli.theme.as_deref());
462
463 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 handle_studio(
474 CommonParams::default(),
475 None,
476 None,
477 None,
478 cli.repository_url,
479 )
480 .await
481 }
482}
483
484fn initialize_theme(cli_theme: Option<&str>) {
486 use crate::config::Config;
487
488 let theme_name = if let Some(name) = cli_theme {
490 Some(name.to_string())
491 } else {
492 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 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#[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#[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 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 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 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 let commit_service = Arc::new(GitCommitService::new(
568 git_repo.clone(),
569 use_gitmoji,
570 config.verify,
571 ));
572
573 let agent_service = Arc::new(IrisAgentService::from_common_params(
575 &common,
576 repository_url.clone(),
577 )?);
578
579 let git_info = git_repo.get_git_info(&cfg)?;
581
582 if config.print_only || config.auto_commit {
584 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 if let Err(e) = commit_service.pre_commit() {
596 ui::print_error(&format!("Pre-commit failed: {e}"));
597 return Err(e);
598 }
599
600 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 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 let StructuredResponse::CommitMessage(generated_message) = response else {
620 return Err(anyhow::anyhow!("Expected commit message response"));
621 };
622
623 spinner.finish_and_clear();
625
626 if config.print_only {
627 println!("{}", format_commit_message(&generated_message));
628 return Ok(());
629 }
630
631 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 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 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
682async 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
704fn 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#[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 if !raw {
759 ui::print_version(crate_version!());
760 ui::print_newline();
761 }
762
763 use crate::agents::{IrisAgentService, TaskContext};
764
765 let context = TaskContext::for_review(commit, from, to, include_unstaged)?;
767
768 let spinner = if raw {
770 None
771 } else {
772 Some(ui::create_spinner("Initializing Iris..."))
773 };
774
775 let service = IrisAgentService::from_common_params(&common, repository_url)?;
777 let response = service.execute_task("review", context).await?;
778
779 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#[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 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 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 let spinner = if raw {
834 None
835 } else {
836 Some(ui::create_spinner("Initializing Iris..."))
837 };
838
839 let service = IrisAgentService::from_common_params(&common, repository_url.clone())?;
841 let response = service.execute_task("changelog", context).await?;
842
843 if let Some(s) = spinner {
845 s.finish_and_clear();
846 }
847
848 println!("{response}");
850
851 if update {
852 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 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 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#[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 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 let context = TaskContext::for_changelog(from, to, version_name, None);
934
935 let spinner = if raw {
937 None
938 } else {
939 Some(ui::create_spinner("Initializing Iris..."))
940 };
941
942 let service = IrisAgentService::from_common_params(&common, repository_url)?;
944 let response = service.execute_task("release_notes", context).await?;
945
946 if let Some(s) = spinner {
948 s.finish_and_clear();
949 }
950
951 println!("{response}");
952
953 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 let path = Path::new(&release_notes_path);
964 let result = if path.exists() {
965 let existing = fs::read_to_string(path)?;
967 fs::write(path, format!("{formatted_content}\n\n---\n\n{existing}"))
968 } else {
969 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#[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 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
1143fn 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 = ¤t.meta.name;
1151
1152 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 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 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 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
1227fn handle_completions(shell: Shell) {
1229 let mut cmd = Cli::command();
1230 generate(shell, &mut cmd, "git-iris", &mut io::stdout());
1231}
1232
1233async 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 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 let context = TaskContext::for_pr(from, to);
1260
1261 let spinner = if raw {
1263 None
1264 } else {
1265 Some(ui::create_spinner("Initializing Iris..."))
1266 };
1267
1268 let service = IrisAgentService::from_common_params(&common, repository_url)?;
1270 let response = service.execute_task("pr", context).await?;
1271
1272 if let Some(s) = spinner {
1274 s.finish_and_clear();
1275 }
1276
1277 let StructuredResponse::PullRequest(generated_pr) = response else {
1279 return Err(anyhow::anyhow!("Expected pull request response"));
1280 };
1281
1282 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 println!("{raw_content}");
1294 }
1295 },
1296 Err(e) => {
1297 ui::print_error(&format!("Clipboard unavailable: {e}"));
1298 println!("{raw_content}");
1300 }
1301 }
1302 } else if raw {
1303 println!("{}", generated_pr.raw_content());
1305 } else if print {
1306 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
1316async 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 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#[allow(clippy::unused_async)] async 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 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 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 let commit_service = Arc::new(GitCommitService::new(
1384 git_repo.clone(),
1385 cfg.use_gitmoji,
1386 true, ));
1388
1389 let agent_service = Arc::new(IrisAgentService::from_common_params(
1390 &common,
1391 repository_url,
1392 )?);
1393
1394 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}