1use crate::commands;
2use crate::common::CommonParams;
3use crate::log_debug;
4use crate::providers::Provider;
5use crate::theme;
6use crate::theme::names::tokens;
7use crate::ui;
8use clap::builder::{Styles, styling::AnsiColor};
9use clap::{CommandFactory, Parser, Subcommand, crate_version};
10use clap_complete::{Shell, generate};
11use colored::Colorize;
12use std::io;
13
14pub const LOG_FILE: &str = "git-iris-debug.log";
16
17#[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 #[command(subcommand)]
32 pub command: Option<Commands>,
33
34 #[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 #[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 #[arg(
53 short = 'q',
54 long = "quiet",
55 global = true,
56 help = "Suppress non-essential output"
57 )]
58 pub quiet: bool,
59
60 #[arg(
62 short = 'v',
63 long = "version",
64 global = true,
65 help = "Display the version"
66 )]
67 pub version: bool,
68
69 #[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 #[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 #[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#[derive(Subcommand)]
97#[command(subcommand_negates_reqs = true)]
98#[command(subcommand_precedence_over_arg = true)]
99pub enum Commands {
100 #[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 #[arg(short, long, help = "Automatically commit with the generated message")]
114 auto_commit: bool,
115
116 #[arg(short, long, help = "Print the generated message to stdout and exit")]
118 print: bool,
119
120 #[arg(long, help = "Skip verification steps (pre/post commit hooks)")]
122 no_verify: bool,
123
124 #[arg(long, help = "Amend the previous commit with staged changes")]
126 amend: bool,
127 },
128
129 #[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 #[arg(short, long, help = "Print the generated review to stdout and exit")]
140 print: bool,
141
142 #[arg(long, help = "Output raw markdown without any console formatting")]
144 raw: bool,
145
146 #[arg(long, help = "Include unstaged changes in the review")]
148 include_unstaged: bool,
149
150 #[arg(
152 long,
153 help = "Review a specific commit by ID (hash, branch, or reference)"
154 )]
155 commit: Option<String>,
156
157 #[arg(
159 long,
160 help = "Starting branch for comparison (defaults to 'main'). Used with --to for branch comparison reviews"
161 )]
162 from: Option<String>,
163
164 #[arg(
166 long,
167 help = "Target branch for comparison (e.g., 'feature-branch', 'pr-branch'). Used with --from for branch comparison reviews"
168 )]
169 to: Option<String>,
170 },
171
172 #[command(
174 about = "Generate a pull request description using AI",
175 long_about = "Generate a comprehensive pull request description based on commit ranges, branch differences, or single commits. Analyzes the overall changeset as an atomic unit and creates professional PR descriptions with summaries, detailed explanations, and testing notes.\n\nUsage examples:\n• Single commit: --from abc1234 or --to abc1234\n• Single commitish: --from HEAD~1 or --to HEAD~2\n• Multiple commits: --from HEAD~3 (reviews last 3 commits)\n• Commit range: --from abc1234 --to def5678\n• Branch comparison: --from main --to feature-branch\n• From main to branch: --to feature-branch\n\nSupported commitish syntax: HEAD~2, HEAD^, @~3, main~1, origin/main^, etc."
176 )]
177 Pr {
178 #[command(flatten)]
179 common: CommonParams,
180
181 #[arg(
183 short,
184 long,
185 help = "Print the generated PR description to stdout and exit"
186 )]
187 print: bool,
188
189 #[arg(long, help = "Output raw markdown without any console formatting")]
191 raw: bool,
192
193 #[arg(
195 short,
196 long,
197 help = "Copy raw markdown to clipboard (for pasting into GitHub/GitLab)"
198 )]
199 copy: bool,
200
201 #[arg(
203 long,
204 help = "Starting branch, commit, or commitish for comparison. For single commit analysis, specify just this parameter with a commit hash (e.g., --from abc1234). For reviewing multiple commits, use commitish syntax (e.g., --from HEAD~3 to review last 3 commits)"
205 )]
206 from: Option<String>,
207
208 #[arg(
210 long,
211 help = "Target branch, commit, or commitish for comparison. For single commit analysis, specify just this parameter with a commit hash or commitish (e.g., --to HEAD~2)"
212 )]
213 to: Option<String>,
214 },
215
216 #[command(
218 about = "Generate a changelog",
219 long_about = "Generate a changelog between two specified Git references."
220 )]
221 Changelog {
222 #[command(flatten)]
223 common: CommonParams,
224
225 #[arg(long, required = true)]
227 from: String,
228
229 #[arg(long)]
231 to: Option<String>,
232
233 #[arg(long, help = "Output raw markdown without any console formatting")]
235 raw: bool,
236
237 #[arg(long, help = "Update the changelog file with the new changes")]
239 update: bool,
240
241 #[arg(long, help = "Path to the changelog file (defaults to CHANGELOG.md)")]
243 file: Option<String>,
244
245 #[arg(long, help = "Explicit version name to use in the changelog")]
247 version_name: Option<String>,
248 },
249
250 #[command(
252 about = "Generate release notes",
253 long_about = "Generate comprehensive release notes between two specified Git references."
254 )]
255 ReleaseNotes {
256 #[command(flatten)]
257 common: CommonParams,
258
259 #[arg(long, required = true)]
261 from: String,
262
263 #[arg(long)]
265 to: Option<String>,
266
267 #[arg(long, help = "Output raw markdown without any console formatting")]
269 raw: bool,
270
271 #[arg(long, help = "Update the release notes file with the new content")]
273 update: bool,
274
275 #[arg(
277 long,
278 help = "Path to the release notes file (defaults to RELEASE_NOTES.md)"
279 )]
280 file: Option<String>,
281
282 #[arg(long, help = "Explicit version name to use in the release notes")]
284 version_name: Option<String>,
285 },
286
287 #[command(
289 about = "Launch Iris Studio TUI",
290 long_about = "Launch Iris Studio, a unified terminal user interface for exploring code, generating commits, reviewing changes, and more. The interface adapts to your repository state."
291 )]
292 Studio {
293 #[command(flatten)]
294 common: CommonParams,
295
296 #[arg(
298 long,
299 value_name = "MODE",
300 help = "Initial mode: explore, commit, review, pr, changelog"
301 )]
302 mode: Option<String>,
303
304 #[arg(long, value_name = "REF", help = "Starting ref for comparison")]
306 from: Option<String>,
307
308 #[arg(long, value_name = "REF", help = "Ending ref for comparison")]
310 to: Option<String>,
311 },
312
313 #[command(about = "Configure Git-Iris settings and providers")]
316 Config {
317 #[command(flatten)]
318 common: CommonParams,
319
320 #[arg(long, help = "Set API key for the specified provider")]
322 api_key: Option<String>,
323
324 #[arg(
326 long,
327 help = "Set fast model for the specified provider (used for status updates and simple tasks)"
328 )]
329 fast_model: Option<String>,
330
331 #[arg(long, help = "Set token limit for the specified provider")]
333 token_limit: Option<usize>,
334
335 #[arg(
337 long,
338 help = "Set additional parameters for the specified provider (key=value)"
339 )]
340 param: Option<Vec<String>>,
341
342 #[arg(
344 long,
345 help = "Set timeout in seconds for parallel subagent tasks (default: 120)"
346 )]
347 subagent_timeout: Option<u64>,
348 },
349
350 #[command(
352 about = "Manage project-specific configuration",
353 long_about = "Create or update a project-specific .irisconfig file in the repository root."
354 )]
355 ProjectConfig {
356 #[command(flatten)]
357 common: CommonParams,
358
359 #[arg(
361 long,
362 help = "Set fast model for the specified provider (used for status updates and simple tasks)"
363 )]
364 fast_model: Option<String>,
365
366 #[arg(long, help = "Set token limit for the specified provider")]
368 token_limit: Option<usize>,
369
370 #[arg(
372 long,
373 help = "Set additional parameters for the specified provider (key=value)"
374 )]
375 param: Option<Vec<String>>,
376
377 #[arg(
379 long,
380 help = "Set timeout in seconds for parallel subagent tasks (default: 120)"
381 )]
382 subagent_timeout: Option<u64>,
383
384 #[arg(short, long, help = "Print the current project configuration")]
386 print: bool,
387 },
388
389 #[command(about = "List available instruction presets")]
391 ListPresets,
392
393 #[command(about = "List available themes")]
395 Themes,
396
397 #[command(
399 about = "Generate shell completions",
400 long_about = "Generate shell completion scripts for bash, zsh, fish, elvish, or powershell.\n\nUsage examples:\n• Bash: git-iris completions bash >> ~/.bashrc\n• Zsh: git-iris completions zsh >> ~/.zshrc\n• Fish: git-iris completions fish > ~/.config/fish/completions/git-iris.fish"
401 )]
402 Completions {
403 #[arg(value_enum)]
405 shell: Shell,
406 },
407}
408
409fn get_styles() -> Styles {
411 Styles::styled()
412 .header(AnsiColor::Magenta.on_default().bold())
413 .usage(AnsiColor::Cyan.on_default().bold())
414 .literal(AnsiColor::Green.on_default().bold())
415 .placeholder(AnsiColor::Yellow.on_default())
416 .valid(AnsiColor::Blue.on_default().bold())
417 .invalid(AnsiColor::Red.on_default().bold())
418 .error(AnsiColor::Red.on_default().bold())
419}
420
421pub fn parse_args() -> Cli {
423 Cli::parse()
424}
425
426fn get_dynamic_help() -> String {
428 let providers_list = Provider::all_names()
429 .iter()
430 .map(|p| format!("{}", p.bold()))
431 .collect::<Vec<_>>()
432 .join(" • ");
433
434 format!("\nAvailable LLM Providers: {providers_list}")
435}
436
437pub async fn main() -> anyhow::Result<()> {
439 let cli = parse_args();
440
441 if cli.version {
442 ui::print_version(crate_version!());
443 return Ok(());
444 }
445
446 if cli.log {
447 crate::logger::enable_logging();
448 crate::logger::set_log_to_stdout(true);
449 let log_file = cli.log_file.as_deref().unwrap_or(LOG_FILE);
450 crate::logger::set_log_file(log_file)?;
451 log_debug!("Debug logging enabled");
452 } else {
453 crate::logger::disable_logging();
454 }
455
456 if cli.quiet {
458 crate::ui::set_quiet_mode(true);
459 }
460
461 initialize_theme(cli.theme.as_deref());
463
464 if cli.debug {
466 crate::agents::debug::enable_debug_mode();
467 crate::agents::debug::debug_header("🔮 IRIS DEBUG MODE ACTIVATED 🔮");
468 }
469
470 if let Some(command) = cli.command {
471 handle_command(command, cli.repository_url).await
472 } else {
473 handle_studio(
475 CommonParams::default(),
476 None,
477 None,
478 None,
479 cli.repository_url,
480 )
481 .await
482 }
483}
484
485fn initialize_theme(cli_theme: Option<&str>) {
487 use crate::config::Config;
488
489 let theme_name = if let Some(name) = cli_theme {
491 Some(name.to_string())
492 } else {
493 Config::load().ok().and_then(|c| {
495 if c.theme.is_empty() {
496 None
497 } else {
498 Some(c.theme)
499 }
500 })
501 };
502
503 if let Some(name) = theme_name {
505 if let Err(e) = theme::load_theme_by_name(&name) {
506 ui::print_warning(&format!(
507 "Failed to load theme '{}': {}. Using default.",
508 name, e
509 ));
510 } else {
511 log_debug!("Loaded theme: {}", name);
512 }
513 }
514}
515
516#[allow(clippy::struct_excessive_bools)]
518struct GenConfig {
519 auto_commit: bool,
520 use_gitmoji: bool,
521 print_only: bool,
522 verify: bool,
523 amend: bool,
524}
525
526#[allow(clippy::too_many_lines)]
528async fn handle_gen_with_agent(
529 common: CommonParams,
530 config: GenConfig,
531 repository_url: Option<String>,
532) -> anyhow::Result<()> {
533 use crate::agents::{IrisAgentService, StructuredResponse, TaskContext};
534 use crate::config::Config;
535 use crate::git::GitRepo;
536 use crate::instruction_presets::PresetType;
537 use crate::output::format_commit_result;
538 use crate::services::GitCommitService;
539 use crate::studio::{Mode, run_studio};
540 use crate::types::format_commit_message;
541 use anyhow::Context;
542 use std::sync::Arc;
543
544 if !common.is_valid_preset_for_type(PresetType::Commit) {
546 ui::print_warning(
547 "The specified preset may not be suitable for commit messages. Consider using a commit or general preset instead.",
548 );
549 ui::print_info("Run 'git-iris list-presets' to see available presets for commits.");
550 }
551
552 if config.amend && !config.print_only && !config.auto_commit {
554 ui::print_warning("--amend requires --print or --auto-commit for now.");
555 ui::print_info("Example: git-iris gen --amend --auto-commit");
556 return Ok(());
557 }
558
559 let mut cfg = Config::load()?;
560 common.apply_to_config(&mut cfg)?;
561
562 let repo_url = repository_url.clone().or(common.repository_url.clone());
564 let git_repo = Arc::new(GitRepo::new_from_url(repo_url).context("Failed to create GitRepo")?);
565 let use_gitmoji = config.use_gitmoji && cfg.use_gitmoji;
566
567 let commit_service = Arc::new(GitCommitService::new(
569 git_repo.clone(),
570 use_gitmoji,
571 config.verify,
572 ));
573
574 let agent_service = Arc::new(IrisAgentService::from_common_params(
576 &common,
577 repository_url.clone(),
578 )?);
579
580 let git_info = git_repo.get_git_info(&cfg)?;
582
583 if config.print_only || config.auto_commit {
585 if git_info.staged_files.is_empty() && !config.amend {
588 ui::print_warning(
589 "No staged changes. Please stage your changes before generating a commit message.",
590 );
591 ui::print_info("You can stage changes using 'git add <file>' or 'git add .'");
592 return Ok(());
593 }
594
595 if let Err(e) = commit_service.pre_commit() {
597 ui::print_error(&format!("Pre-commit failed: {e}"));
598 return Err(e);
599 }
600
601 let spinner_msg = if config.amend {
603 "Generating amended commit message..."
604 } else {
605 "Generating commit message..."
606 };
607 let spinner = ui::create_spinner(spinner_msg);
608
609 let context = if config.amend {
612 let original_message = commit_service.get_head_commit_message().unwrap_or_default();
613 TaskContext::for_amend(original_message)
614 } else {
615 TaskContext::for_gen()
616 };
617 let response = agent_service.execute_task("commit", context).await?;
618
619 let StructuredResponse::CommitMessage(generated_message) = response else {
621 return Err(anyhow::anyhow!("Expected commit message response"));
622 };
623
624 spinner.finish_and_clear();
626
627 if config.print_only {
628 println!("{}", format_commit_message(&generated_message));
629 return Ok(());
630 }
631
632 if commit_service.is_remote() {
634 ui::print_error(
635 "Cannot automatically commit to a remote repository. Use --print instead.",
636 );
637 return Err(anyhow::anyhow!(
638 "Auto-commit not supported for remote repositories"
639 ));
640 }
641
642 let commit_result = if config.amend {
643 commit_service.perform_amend(&format_commit_message(&generated_message))
644 } else {
645 commit_service.perform_commit(&format_commit_message(&generated_message))
646 };
647
648 match commit_result {
649 Ok(result) => {
650 let output =
651 format_commit_result(&result, &format_commit_message(&generated_message));
652 println!("{output}");
653 }
654 Err(e) => {
655 let action = if config.amend { "amend" } else { "commit" };
656 eprintln!("Failed to {action}: {e}");
657 return Err(e);
658 }
659 }
660 return Ok(());
661 }
662
663 if commit_service.is_remote() {
665 ui::print_warning(
666 "Interactive commit not available for remote repositories. Use --print instead.",
667 );
668 return Ok(());
669 }
670
671 run_studio(
673 cfg,
674 Some(git_repo),
675 Some(commit_service),
676 Some(agent_service),
677 Some(Mode::Commit),
678 None,
679 None,
680 )
681}
682
683async fn handle_gen(
685 common: CommonParams,
686 config: GenConfig,
687 repository_url: Option<String>,
688) -> anyhow::Result<()> {
689 log_debug!(
690 "Handling 'gen' command with common: {:?}, auto_commit: {}, use_gitmoji: {}, print: {}, verify: {}, amend: {}",
691 common,
692 config.auto_commit,
693 config.use_gitmoji,
694 config.print_only,
695 config.verify,
696 config.amend
697 );
698
699 ui::print_version(crate_version!());
700 ui::print_newline();
701
702 handle_gen_with_agent(common, config, repository_url).await
703}
704
705fn handle_config(
707 common: &CommonParams,
708 api_key: Option<String>,
709 model: Option<String>,
710 fast_model: Option<String>,
711 token_limit: Option<usize>,
712 param: Option<Vec<String>>,
713 subagent_timeout: Option<u64>,
714) -> anyhow::Result<()> {
715 log_debug!(
716 "Handling 'config' command with common: {:?}, api_key: {:?}, model: {:?}, token_limit: {:?}, param: {:?}, subagent_timeout: {:?}",
717 common,
718 api_key,
719 model,
720 token_limit,
721 param,
722 subagent_timeout
723 );
724 commands::handle_config_command(
725 common,
726 api_key,
727 model,
728 fast_model,
729 token_limit,
730 param,
731 subagent_timeout,
732 )
733}
734
735#[allow(clippy::too_many_arguments)]
737async fn handle_review(
738 common: CommonParams,
739 print: bool,
740 raw: bool,
741 repository_url: Option<String>,
742 include_unstaged: bool,
743 commit: Option<String>,
744 from: Option<String>,
745 to: Option<String>,
746) -> anyhow::Result<()> {
747 log_debug!(
748 "Handling 'review' command with common: {:?}, print: {}, raw: {}, include_unstaged: {}, commit: {:?}, from: {:?}, to: {:?}",
749 common,
750 print,
751 raw,
752 include_unstaged,
753 commit,
754 from,
755 to
756 );
757
758 if !raw {
760 ui::print_version(crate_version!());
761 ui::print_newline();
762 }
763
764 use crate::agents::{IrisAgentService, TaskContext};
765
766 let context = TaskContext::for_review(commit, from, to, include_unstaged)?;
768
769 let spinner = if raw {
771 None
772 } else {
773 Some(ui::create_spinner("Initializing Iris..."))
774 };
775
776 let service = IrisAgentService::from_common_params(&common, repository_url)?;
778 let response = service.execute_task("review", context).await?;
779
780 if let Some(s) = spinner {
782 s.finish_and_clear();
783 }
784
785 if raw || print {
786 println!("{response}");
787 } else {
788 ui::print_success("Code review completed successfully");
789 println!("{response}");
790 }
791 Ok(())
792}
793
794#[allow(clippy::too_many_arguments)]
796async fn handle_changelog(
797 common: CommonParams,
798 from: String,
799 to: Option<String>,
800 raw: bool,
801 repository_url: Option<String>,
802 update: bool,
803 file: Option<String>,
804 version_name: Option<String>,
805) -> anyhow::Result<()> {
806 log_debug!(
807 "Handling 'changelog' command with common: {:?}, from: {}, to: {:?}, raw: {}, update: {}, file: {:?}, version_name: {:?}",
808 common,
809 from,
810 to,
811 raw,
812 update,
813 file,
814 version_name
815 );
816
817 if !raw {
819 ui::print_version(crate_version!());
820 ui::print_newline();
821 }
822
823 use crate::agents::{IrisAgentService, TaskContext};
824 use crate::changelog::ChangelogGenerator;
825 use crate::git::GitRepo;
826 use anyhow::Context;
827 use std::sync::Arc;
828
829 let context = TaskContext::for_changelog(from.clone(), to.clone(), version_name.clone(), None);
831 let to_ref = to.unwrap_or_else(|| "HEAD".to_string());
832
833 let spinner = if raw {
835 None
836 } else {
837 Some(ui::create_spinner("Initializing Iris..."))
838 };
839
840 let service = IrisAgentService::from_common_params(&common, repository_url.clone())?;
842 let response = service.execute_task("changelog", context).await?;
843
844 if let Some(s) = spinner {
846 s.finish_and_clear();
847 }
848
849 println!("{response}");
851
852 if update {
853 let formatted_content = response.to_string();
855 let changelog_path = file.unwrap_or_else(|| "CHANGELOG.md".to_string());
856 let repo_url_for_update = repository_url.or(common.repository_url.clone());
857
858 let git_repo = if let Some(url) = repo_url_for_update {
860 Arc::new(
861 GitRepo::clone_remote_repository(&url)
862 .context("Failed to clone repository for changelog update")?,
863 )
864 } else {
865 let repo_path = std::env::current_dir()?;
866 Arc::new(
867 GitRepo::new(&repo_path)
868 .context("Failed to create GitRepo for changelog update")?,
869 )
870 };
871
872 let update_spinner =
874 ui::create_spinner(&format!("Updating changelog file at {changelog_path}..."));
875
876 match ChangelogGenerator::update_changelog_file(
877 &formatted_content,
878 &changelog_path,
879 &git_repo,
880 &to_ref,
881 version_name,
882 ) {
883 Ok(()) => {
884 update_spinner.finish_and_clear();
885 ui::print_success(&format!(
886 "✨ Changelog successfully updated at {}",
887 changelog_path.bright_green()
888 ));
889 }
890 Err(e) => {
891 update_spinner.finish_and_clear();
892 ui::print_error(&format!("Failed to update changelog file: {e}"));
893 return Err(e);
894 }
895 }
896 }
897 Ok(())
898}
899
900#[allow(clippy::too_many_arguments)]
902async fn handle_release_notes(
903 common: CommonParams,
904 from: String,
905 to: Option<String>,
906 raw: bool,
907 repository_url: Option<String>,
908 update: bool,
909 file: Option<String>,
910 version_name: Option<String>,
911) -> anyhow::Result<()> {
912 log_debug!(
913 "Handling 'release-notes' command with common: {:?}, from: {}, to: {:?}, raw: {}, update: {}, file: {:?}, version_name: {:?}",
914 common,
915 from,
916 to,
917 raw,
918 update,
919 file,
920 version_name
921 );
922
923 if !raw {
925 ui::print_version(crate_version!());
926 ui::print_newline();
927 }
928
929 use crate::agents::{IrisAgentService, TaskContext};
930 use std::fs;
931 use std::path::Path;
932
933 let context = TaskContext::for_changelog(from, to, version_name, None);
935
936 let spinner = if raw {
938 None
939 } else {
940 Some(ui::create_spinner("Initializing Iris..."))
941 };
942
943 let service = IrisAgentService::from_common_params(&common, repository_url)?;
945 let response = service.execute_task("release_notes", context).await?;
946
947 if let Some(s) = spinner {
949 s.finish_and_clear();
950 }
951
952 println!("{response}");
953
954 if update {
956 let release_notes_path = file.unwrap_or_else(|| "RELEASE_NOTES.md".to_string());
957 let formatted_content = response.to_string();
958
959 let update_spinner = ui::create_spinner(&format!(
960 "Updating release notes file at {release_notes_path}..."
961 ));
962
963 let path = Path::new(&release_notes_path);
965 let result = if path.exists() {
966 let existing = fs::read_to_string(path)?;
968 fs::write(path, format!("{formatted_content}\n\n---\n\n{existing}"))
969 } else {
970 fs::write(path, &formatted_content)
972 };
973
974 match result {
975 Ok(()) => {
976 update_spinner.finish_and_clear();
977 ui::print_success(&format!(
978 "✨ Release notes successfully updated at {}",
979 release_notes_path.bright_green()
980 ));
981 }
982 Err(e) => {
983 update_spinner.finish_and_clear();
984 ui::print_error(&format!("Failed to update release notes file: {e}"));
985 return Err(e.into());
986 }
987 }
988 }
989
990 Ok(())
991}
992
993#[allow(clippy::too_many_lines)]
995pub async fn handle_command(
996 command: Commands,
997 repository_url: Option<String>,
998) -> anyhow::Result<()> {
999 match command {
1000 Commands::Gen {
1001 common,
1002 auto_commit,
1003 print,
1004 no_verify,
1005 amend,
1006 } => {
1007 let use_gitmoji = common.resolved_gitmoji().unwrap_or(true);
1010 handle_gen(
1011 common,
1012 GenConfig {
1013 auto_commit,
1014 use_gitmoji,
1015 print_only: print,
1016 verify: !no_verify,
1017 amend,
1018 },
1019 repository_url,
1020 )
1021 .await
1022 }
1023 Commands::Config {
1024 common,
1025 api_key,
1026 fast_model,
1027 token_limit,
1028 param,
1029 subagent_timeout,
1030 } => handle_config(
1031 &common,
1032 api_key,
1033 common.model.clone(),
1034 fast_model,
1035 token_limit,
1036 param,
1037 subagent_timeout,
1038 ),
1039 Commands::Review {
1040 common,
1041 print,
1042 raw,
1043 include_unstaged,
1044 commit,
1045 from,
1046 to,
1047 } => {
1048 handle_review(
1049 common,
1050 print,
1051 raw,
1052 repository_url,
1053 include_unstaged,
1054 commit,
1055 from,
1056 to,
1057 )
1058 .await
1059 }
1060 Commands::Changelog {
1061 common,
1062 from,
1063 to,
1064 raw,
1065 update,
1066 file,
1067 version_name,
1068 } => {
1069 handle_changelog(
1070 common,
1071 from,
1072 to,
1073 raw,
1074 repository_url,
1075 update,
1076 file,
1077 version_name,
1078 )
1079 .await
1080 }
1081 Commands::ReleaseNotes {
1082 common,
1083 from,
1084 to,
1085 raw,
1086 update,
1087 file,
1088 version_name,
1089 } => {
1090 handle_release_notes(
1091 common,
1092 from,
1093 to,
1094 raw,
1095 repository_url,
1096 update,
1097 file,
1098 version_name,
1099 )
1100 .await
1101 }
1102 Commands::ProjectConfig {
1103 common,
1104 fast_model,
1105 token_limit,
1106 param,
1107 subagent_timeout,
1108 print,
1109 } => commands::handle_project_config_command(
1110 &common,
1111 common.model.clone(),
1112 fast_model,
1113 token_limit,
1114 param,
1115 subagent_timeout,
1116 print,
1117 ),
1118 Commands::ListPresets => commands::handle_list_presets_command(),
1119 Commands::Themes => {
1120 handle_themes();
1121 Ok(())
1122 }
1123 Commands::Completions { shell } => {
1124 handle_completions(shell);
1125 Ok(())
1126 }
1127 Commands::Pr {
1128 common,
1129 print,
1130 raw,
1131 copy,
1132 from,
1133 to,
1134 } => handle_pr(common, print, raw, copy, from, to, repository_url).await,
1135 Commands::Studio {
1136 common,
1137 mode,
1138 from,
1139 to,
1140 } => handle_studio(common, mode, from, to, repository_url).await,
1141 }
1142}
1143
1144fn handle_themes() {
1146 ui::print_version(crate_version!());
1147 ui::print_newline();
1148
1149 let available = theme::list_available_themes();
1150 let current = theme::current();
1151 let current_name = ¤t.meta.name;
1152
1153 let header_color = theme::current().color(tokens::ACCENT_PRIMARY);
1155 println!(
1156 "{}",
1157 "Available Themes:"
1158 .truecolor(header_color.r, header_color.g, header_color.b)
1159 .bold()
1160 );
1161 println!();
1162
1163 for info in available {
1164 let is_current = info.display_name == *current_name;
1165 let marker = if is_current { "● " } else { " " };
1166
1167 let name_color = if is_current {
1168 theme::current().color(tokens::SUCCESS)
1169 } else {
1170 theme::current().color(tokens::ACCENT_SECONDARY)
1171 };
1172
1173 let desc_color = theme::current().color(tokens::TEXT_SECONDARY);
1174
1175 print!(
1176 "{}{}",
1177 marker.truecolor(name_color.r, name_color.g, name_color.b),
1178 info.name
1179 .truecolor(name_color.r, name_color.g, name_color.b)
1180 .bold()
1181 );
1182
1183 if info.display_name != info.name {
1185 print!(
1186 " ({})",
1187 info.display_name
1188 .truecolor(desc_color.r, desc_color.g, desc_color.b)
1189 );
1190 }
1191
1192 let variant_str = match info.variant {
1194 theme::ThemeVariant::Dark => "dark",
1195 theme::ThemeVariant::Light => "light",
1196 };
1197 let dim_color = theme::current().color(tokens::TEXT_DIM);
1198 print!(
1199 " [{}]",
1200 variant_str.truecolor(dim_color.r, dim_color.g, dim_color.b)
1201 );
1202
1203 if is_current {
1204 let active_color = theme::current().color(tokens::SUCCESS);
1205 print!(
1206 " {}",
1207 "(active)".truecolor(active_color.r, active_color.g, active_color.b)
1208 );
1209 }
1210
1211 println!();
1212 }
1213
1214 println!();
1215
1216 let hint_color = theme::current().color(tokens::TEXT_DIM);
1218 println!(
1219 "{}",
1220 "Use --theme <name> to override, or set 'theme' in config.toml".truecolor(
1221 hint_color.r,
1222 hint_color.g,
1223 hint_color.b
1224 )
1225 );
1226}
1227
1228fn handle_completions(shell: Shell) {
1230 let mut cmd = Cli::command();
1231 generate(shell, &mut cmd, "git-iris", &mut io::stdout());
1232}
1233
1234async fn handle_pr_with_agent(
1236 common: CommonParams,
1237 print: bool,
1238 raw: bool,
1239 copy: bool,
1240 from: Option<String>,
1241 to: Option<String>,
1242 repository_url: Option<String>,
1243) -> anyhow::Result<()> {
1244 use crate::agents::{IrisAgentService, StructuredResponse, TaskContext};
1245 use crate::instruction_presets::PresetType;
1246 use arboard::Clipboard;
1247
1248 if !raw
1250 && !common.is_valid_preset_for_type(PresetType::Review)
1251 && !common.is_valid_preset_for_type(PresetType::Both)
1252 {
1253 ui::print_warning(
1254 "The specified preset may not be suitable for PR descriptions. Consider using a review or general preset instead.",
1255 );
1256 ui::print_info("Run 'git-iris list-presets' to see available presets for PRs.");
1257 }
1258
1259 let context = TaskContext::for_pr(from, to);
1261
1262 let spinner = if raw {
1264 None
1265 } else {
1266 Some(ui::create_spinner("Initializing Iris..."))
1267 };
1268
1269 let service = IrisAgentService::from_common_params(&common, repository_url)?;
1271 let response = service.execute_task("pr", context).await?;
1272
1273 if let Some(s) = spinner {
1275 s.finish_and_clear();
1276 }
1277
1278 let StructuredResponse::PullRequest(generated_pr) = response else {
1280 return Err(anyhow::anyhow!("Expected pull request response"));
1281 };
1282
1283 if copy {
1285 let raw_content = generated_pr.raw_content();
1286 match Clipboard::new() {
1287 Ok(mut clipboard) => match clipboard.set_text(raw_content) {
1288 Ok(()) => {
1289 ui::print_success("PR description copied to clipboard");
1290 }
1291 Err(e) => {
1292 ui::print_error(&format!("Failed to copy to clipboard: {e}"));
1293 println!("{raw_content}");
1295 }
1296 },
1297 Err(e) => {
1298 ui::print_error(&format!("Clipboard unavailable: {e}"));
1299 println!("{raw_content}");
1301 }
1302 }
1303 } else if raw {
1304 println!("{}", generated_pr.raw_content());
1306 } else if print {
1307 println!("{}", generated_pr.format());
1309 } else {
1310 ui::print_success("PR description generated successfully");
1311 println!("{}", generated_pr.format());
1312 }
1313
1314 Ok(())
1315}
1316
1317async fn handle_pr(
1319 common: CommonParams,
1320 print: bool,
1321 raw: bool,
1322 copy: bool,
1323 from: Option<String>,
1324 to: Option<String>,
1325 repository_url: Option<String>,
1326) -> anyhow::Result<()> {
1327 log_debug!(
1328 "Handling 'pr' command with common: {:?}, print: {}, raw: {}, copy: {}, from: {:?}, to: {:?}",
1329 common,
1330 print,
1331 raw,
1332 copy,
1333 from,
1334 to
1335 );
1336
1337 if !raw {
1340 ui::print_version(crate_version!());
1341 ui::print_newline();
1342 }
1343
1344 handle_pr_with_agent(common, print, raw, copy, from, to, repository_url).await
1345}
1346
1347#[allow(clippy::unused_async)] async fn handle_studio(
1350 common: CommonParams,
1351 mode: Option<String>,
1352 from: Option<String>,
1353 to: Option<String>,
1354 repository_url: Option<String>,
1355) -> anyhow::Result<()> {
1356 use crate::agents::IrisAgentService;
1357 use crate::config::Config;
1358 use crate::git::GitRepo;
1359 use crate::services::GitCommitService;
1360 use crate::studio::{Mode, run_studio};
1361 use anyhow::Context;
1362 use std::sync::Arc;
1363
1364 crate::logger::set_log_to_stdout(false);
1366
1367 log_debug!(
1368 "Handling 'studio' command with common: {:?}, mode: {:?}, from: {:?}, to: {:?}",
1369 common,
1370 mode,
1371 from,
1372 to
1373 );
1374
1375 let mut cfg = Config::load()?;
1376 common.apply_to_config(&mut cfg)?;
1377
1378 let repo_url = repository_url.clone().or(common.repository_url.clone());
1380 let git_repo =
1381 Arc::new(GitRepo::new_from_url(repo_url.clone()).context("Failed to create GitRepo")?);
1382
1383 let commit_service = Arc::new(GitCommitService::new(
1385 git_repo.clone(),
1386 cfg.use_gitmoji,
1387 true, ));
1389
1390 let agent_service = Arc::new(IrisAgentService::from_common_params(
1391 &common,
1392 repository_url,
1393 )?);
1394
1395 let initial_mode = mode
1397 .as_deref()
1398 .and_then(|m| match m.to_lowercase().as_str() {
1399 "explore" => Some(Mode::Explore),
1400 "commit" => Some(Mode::Commit),
1401 "review" => Some(Mode::Review),
1402 "pr" => Some(Mode::PR),
1403 "changelog" => Some(Mode::Changelog),
1404 _ => {
1405 ui::print_warning(&format!("Unknown mode '{}', using auto-detect", m));
1406 None
1407 }
1408 });
1409
1410 run_studio(
1411 cfg,
1412 Some(git_repo),
1413 Some(commit_service),
1414 Some(agent_service),
1415 initial_mode,
1416 from,
1417 to,
1418 )
1419}