omni_dev/cli/
git.rs

1//! Git-related CLI commands
2
3use anyhow::{Context, Result};
4use clap::{Parser, Subcommand};
5use tracing::{debug, error};
6
7/// Git operations
8#[derive(Parser)]
9pub struct GitCommand {
10    /// Git subcommand to execute
11    #[command(subcommand)]
12    pub command: GitSubcommands,
13}
14
15/// Git subcommands
16#[derive(Subcommand)]
17pub enum GitSubcommands {
18    /// Commit-related operations
19    Commit(CommitCommand),
20    /// Branch-related operations
21    Branch(BranchCommand),
22}
23
24/// Commit operations
25#[derive(Parser)]
26pub struct CommitCommand {
27    /// Commit subcommand to execute
28    #[command(subcommand)]
29    pub command: CommitSubcommands,
30}
31
32/// Commit subcommands
33#[derive(Subcommand)]
34pub enum CommitSubcommands {
35    /// Commit message operations
36    Message(MessageCommand),
37}
38
39/// Message operations
40#[derive(Parser)]
41pub struct MessageCommand {
42    /// Message subcommand to execute
43    #[command(subcommand)]
44    pub command: MessageSubcommands,
45}
46
47/// Message subcommands
48#[derive(Subcommand)]
49pub enum MessageSubcommands {
50    /// Analyze commits and output repository information in YAML format
51    View(ViewCommand),
52    /// Amend commit messages based on a YAML configuration file
53    Amend(AmendCommand),
54    /// AI-powered commit message improvement using Claude
55    Twiddle(TwiddleCommand),
56    /// Check commit messages against guidelines without modifying them
57    Check(CheckCommand),
58}
59
60/// View command options
61#[derive(Parser)]
62pub struct ViewCommand {
63    /// Commit range to analyze (e.g., HEAD~3..HEAD, abc123..def456)
64    #[arg(value_name = "COMMIT_RANGE")]
65    pub commit_range: Option<String>,
66}
67
68/// Amend command options  
69#[derive(Parser)]
70pub struct AmendCommand {
71    /// YAML file containing commit amendments
72    #[arg(value_name = "YAML_FILE")]
73    pub yaml_file: String,
74}
75
76/// Twiddle command options
77#[derive(Parser)]
78pub struct TwiddleCommand {
79    /// Commit range to analyze and improve (e.g., HEAD~3..HEAD, abc123..def456)
80    #[arg(value_name = "COMMIT_RANGE")]
81    pub commit_range: Option<String>,
82
83    /// Claude API model to use (if not specified, uses settings or default)
84    #[arg(long)]
85    pub model: Option<String>,
86
87    /// Skip confirmation prompt and apply amendments automatically
88    #[arg(long)]
89    pub auto_apply: bool,
90
91    /// Save generated amendments to file without applying
92    #[arg(long, value_name = "FILE")]
93    pub save_only: Option<String>,
94
95    /// Use additional project context for better suggestions (Phase 3)
96    #[arg(long, default_value = "true")]
97    pub use_context: bool,
98
99    /// Path to custom context directory (defaults to .omni-dev/)
100    #[arg(long)]
101    pub context_dir: Option<std::path::PathBuf>,
102
103    /// Specify work context (e.g., "feature: user authentication")
104    #[arg(long)]
105    pub work_context: Option<String>,
106
107    /// Override detected branch context
108    #[arg(long)]
109    pub branch_context: Option<String>,
110
111    /// Disable contextual analysis (use basic prompting only)
112    #[arg(long)]
113    pub no_context: bool,
114
115    /// Maximum number of commits to process in a single batch (default: 4)
116    #[arg(long, default_value = "4")]
117    pub batch_size: usize,
118
119    /// Skip AI processing and only output repository YAML
120    #[arg(long)]
121    pub no_ai: bool,
122
123    /// Ignore existing commit messages and generate fresh ones based solely on diffs
124    #[arg(long)]
125    pub fresh: bool,
126}
127
128/// Check command options - validates commit messages against guidelines
129#[derive(Parser)]
130pub struct CheckCommand {
131    /// Commit range to check (e.g., HEAD~3..HEAD, abc123..def456)
132    /// Defaults to commits ahead of main branch
133    #[arg(value_name = "COMMIT_RANGE")]
134    pub commit_range: Option<String>,
135
136    /// Claude API model to use (if not specified, uses settings or default)
137    #[arg(long)]
138    pub model: Option<String>,
139
140    /// Path to custom context directory (defaults to .omni-dev/)
141    #[arg(long)]
142    pub context_dir: Option<std::path::PathBuf>,
143
144    /// Explicit path to guidelines file
145    #[arg(long)]
146    pub guidelines: Option<std::path::PathBuf>,
147
148    /// Output format: text (default), json, yaml
149    #[arg(long, default_value = "text")]
150    pub format: String,
151
152    /// Exit with error code if any issues found (including warnings)
153    #[arg(long)]
154    pub strict: bool,
155
156    /// Only show errors/warnings, suppress info-level output
157    #[arg(long)]
158    pub quiet: bool,
159
160    /// Show detailed analysis including passing commits
161    #[arg(long)]
162    pub verbose: bool,
163
164    /// Include passing commits in output (hidden by default)
165    #[arg(long)]
166    pub show_passing: bool,
167
168    /// Number of commits to process per AI request (default: 4)
169    #[arg(long, default_value = "4")]
170    pub batch_size: usize,
171
172    /// Skip generating corrected message suggestions
173    #[arg(long)]
174    pub no_suggestions: bool,
175}
176
177/// Branch operations
178#[derive(Parser)]
179pub struct BranchCommand {
180    /// Branch subcommand to execute
181    #[command(subcommand)]
182    pub command: BranchSubcommands,
183}
184
185/// Branch subcommands
186#[derive(Subcommand)]
187pub enum BranchSubcommands {
188    /// Analyze branch commits and output repository information in YAML format
189    Info(InfoCommand),
190    /// Create operations
191    Create(CreateCommand),
192}
193
194/// Info command options
195#[derive(Parser)]
196pub struct InfoCommand {
197    /// Base branch to compare against (defaults to main/master)
198    #[arg(value_name = "BASE_BRANCH")]
199    pub base_branch: Option<String>,
200}
201
202/// Create operations
203#[derive(Parser)]
204pub struct CreateCommand {
205    /// Create subcommand to execute
206    #[command(subcommand)]
207    pub command: CreateSubcommands,
208}
209
210/// Create subcommands
211#[derive(Subcommand)]
212pub enum CreateSubcommands {
213    /// Create a pull request with AI-generated description
214    Pr(CreatePrCommand),
215}
216
217/// Create PR command options
218#[derive(Parser)]
219pub struct CreatePrCommand {
220    /// Base branch for the PR to be merged into (defaults to main/master)
221    #[arg(long, value_name = "BRANCH")]
222    pub base: Option<String>,
223
224    /// Skip confirmation prompt and create PR automatically
225    #[arg(long)]
226    pub auto_apply: bool,
227
228    /// Save generated PR details to file without creating PR
229    #[arg(long, value_name = "FILE")]
230    pub save_only: Option<String>,
231
232    /// Create PR as ready for review (overrides default)
233    #[arg(long, conflicts_with = "draft")]
234    pub ready: bool,
235
236    /// Create PR as draft (overrides default)
237    #[arg(long, conflicts_with = "ready")]
238    pub draft: bool,
239}
240
241impl GitCommand {
242    /// Execute git command
243    pub fn execute(self) -> Result<()> {
244        match self.command {
245            GitSubcommands::Commit(commit_cmd) => commit_cmd.execute(),
246            GitSubcommands::Branch(branch_cmd) => branch_cmd.execute(),
247        }
248    }
249}
250
251impl CommitCommand {
252    /// Execute commit command
253    pub fn execute(self) -> Result<()> {
254        match self.command {
255            CommitSubcommands::Message(message_cmd) => message_cmd.execute(),
256        }
257    }
258}
259
260impl MessageCommand {
261    /// Execute message command
262    pub fn execute(self) -> Result<()> {
263        match self.command {
264            MessageSubcommands::View(view_cmd) => view_cmd.execute(),
265            MessageSubcommands::Amend(amend_cmd) => amend_cmd.execute(),
266            MessageSubcommands::Twiddle(twiddle_cmd) => {
267                // Use tokio runtime for async execution
268                let rt =
269                    tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?;
270                rt.block_on(twiddle_cmd.execute())
271            }
272            MessageSubcommands::Check(check_cmd) => {
273                // Use tokio runtime for async execution
274                let rt =
275                    tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?;
276                rt.block_on(check_cmd.execute())
277            }
278        }
279    }
280}
281
282impl ViewCommand {
283    /// Execute view command
284    pub fn execute(self) -> Result<()> {
285        use crate::data::{
286            AiInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
287            WorkingDirectoryInfo,
288        };
289        use crate::git::{GitRepository, RemoteInfo};
290        use crate::utils::ai_scratch;
291
292        let commit_range = self.commit_range.as_deref().unwrap_or("HEAD");
293
294        // Open git repository
295        let repo = GitRepository::open()
296            .context("Failed to open git repository. Make sure you're in a git repository.")?;
297
298        // Get working directory status
299        let wd_status = repo.get_working_directory_status()?;
300        let working_directory = WorkingDirectoryInfo {
301            clean: wd_status.clean,
302            untracked_changes: wd_status
303                .untracked_changes
304                .into_iter()
305                .map(|fs| FileStatusInfo {
306                    status: fs.status,
307                    file: fs.file,
308                })
309                .collect(),
310        };
311
312        // Get remote information
313        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
314
315        // Parse commit range and get commits
316        let commits = repo.get_commits_in_range(commit_range)?;
317
318        // Create version information
319        let versions = Some(VersionInfo {
320            omni_dev: env!("CARGO_PKG_VERSION").to_string(),
321        });
322
323        // Get AI scratch directory
324        let ai_scratch_path =
325            ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
326        let ai_info = AiInfo {
327            scratch: ai_scratch_path.to_string_lossy().to_string(),
328        };
329
330        // Build repository view
331        let mut repo_view = RepositoryView {
332            versions,
333            explanation: FieldExplanation::default(),
334            working_directory,
335            remotes,
336            ai: ai_info,
337            branch_info: None,
338            pr_template: None,
339            pr_template_location: None,
340            branch_prs: None,
341            commits,
342        };
343
344        // Update field presence based on actual data
345        repo_view.update_field_presence();
346
347        // Output as YAML
348        let yaml_output = crate::data::to_yaml(&repo_view)?;
349        println!("{}", yaml_output);
350
351        Ok(())
352    }
353}
354
355impl AmendCommand {
356    /// Execute amend command
357    pub fn execute(self) -> Result<()> {
358        use crate::git::AmendmentHandler;
359
360        println!("🔄 Starting commit amendment process...");
361        println!("📄 Loading amendments from: {}", self.yaml_file);
362
363        // Create amendment handler and apply amendments
364        let handler = AmendmentHandler::new().context("Failed to initialize amendment handler")?;
365
366        handler
367            .apply_amendments(&self.yaml_file)
368            .context("Failed to apply amendments")?;
369
370        Ok(())
371    }
372}
373
374impl TwiddleCommand {
375    /// Execute twiddle command with contextual intelligence
376    pub async fn execute(self) -> Result<()> {
377        // If --no-ai flag is set, skip AI processing and output YAML directly
378        if self.no_ai {
379            return self.execute_no_ai().await;
380        }
381
382        // Determine if contextual analysis should be used
383        let use_contextual = self.use_context && !self.no_context;
384
385        if use_contextual {
386            println!(
387                "🪄 Starting AI-powered commit message improvement with contextual intelligence..."
388            );
389        } else {
390            println!("🪄 Starting AI-powered commit message improvement...");
391        }
392
393        // 1. Generate repository view to get all commits
394        let full_repo_view = self.generate_repository_view().await?;
395
396        // 2. Check if batching is needed
397        if full_repo_view.commits.len() > self.batch_size {
398            println!(
399                "📦 Processing {} commits in batches of {} to ensure reliable analysis...",
400                full_repo_view.commits.len(),
401                self.batch_size
402            );
403            return self
404                .execute_with_batching(use_contextual, full_repo_view)
405                .await;
406        }
407
408        // 3. Collect contextual information (Phase 3)
409        let context = if use_contextual {
410            Some(self.collect_context(&full_repo_view).await?)
411        } else {
412            None
413        };
414
415        // 4. Show context summary if available
416        if let Some(ref ctx) = context {
417            self.show_context_summary(ctx)?;
418        }
419
420        // 5. Initialize Claude client
421        let claude_client = crate::claude::create_default_claude_client(self.model.clone())?;
422
423        // Show model information
424        self.show_model_info_from_client(&claude_client)?;
425
426        // 6. Generate amendments via Claude API with context
427        if self.fresh {
428            println!("🔄 Fresh mode: ignoring existing commit messages...");
429        }
430        if use_contextual && context.is_some() {
431            println!("🤖 Analyzing commits with enhanced contextual intelligence...");
432        } else {
433            println!("🤖 Analyzing commits with Claude AI...");
434        }
435
436        let amendments = if let Some(ctx) = context {
437            claude_client
438                .generate_contextual_amendments_with_options(&full_repo_view, &ctx, self.fresh)
439                .await?
440        } else {
441            claude_client
442                .generate_amendments_with_options(&full_repo_view, self.fresh)
443                .await?
444        };
445
446        // 6. Handle different output modes
447        if let Some(save_path) = self.save_only {
448            amendments.save_to_file(save_path)?;
449            println!("💾 Amendments saved to file");
450            return Ok(());
451        }
452
453        // 7. Handle amendments
454        if !amendments.amendments.is_empty() {
455            // Create temporary file for amendments
456            let temp_dir = tempfile::tempdir()?;
457            let amendments_file = temp_dir.path().join("twiddle_amendments.yaml");
458            amendments.save_to_file(&amendments_file)?;
459
460            // Show file path and get user choice
461            if !self.auto_apply && !self.handle_amendments_file(&amendments_file, &amendments)? {
462                println!("❌ Amendment cancelled by user");
463                return Ok(());
464            }
465
466            // 8. Apply amendments (re-read from file to capture any user edits)
467            self.apply_amendments_from_file(&amendments_file).await?;
468            println!("✅ Commit messages improved successfully!");
469        } else {
470            println!("✨ No commits found to process!");
471        }
472
473        Ok(())
474    }
475
476    /// Execute twiddle command with automatic batching for large commit ranges
477    async fn execute_with_batching(
478        &self,
479        use_contextual: bool,
480        full_repo_view: crate::data::RepositoryView,
481    ) -> Result<()> {
482        use crate::data::amendments::AmendmentFile;
483
484        // Initialize Claude client
485        let claude_client = crate::claude::create_default_claude_client(self.model.clone())?;
486
487        // Show model information
488        self.show_model_info_from_client(&claude_client)?;
489
490        // Split commits into batches
491        let commit_batches: Vec<_> = full_repo_view.commits.chunks(self.batch_size).collect();
492
493        let total_batches = commit_batches.len();
494        let mut all_amendments = AmendmentFile {
495            amendments: Vec::new(),
496        };
497
498        if self.fresh {
499            println!("🔄 Fresh mode: ignoring existing commit messages...");
500        }
501        println!("📊 Processing {} batches...", total_batches);
502
503        for (batch_num, commit_batch) in commit_batches.into_iter().enumerate() {
504            println!(
505                "🔄 Processing batch {}/{} ({} commits)...",
506                batch_num + 1,
507                total_batches,
508                commit_batch.len()
509            );
510
511            // Create a repository view for just this batch
512            let batch_repo_view = crate::data::RepositoryView {
513                versions: full_repo_view.versions.clone(),
514                explanation: full_repo_view.explanation.clone(),
515                working_directory: full_repo_view.working_directory.clone(),
516                remotes: full_repo_view.remotes.clone(),
517                ai: full_repo_view.ai.clone(),
518                branch_info: full_repo_view.branch_info.clone(),
519                pr_template: full_repo_view.pr_template.clone(),
520                pr_template_location: full_repo_view.pr_template_location.clone(),
521                branch_prs: full_repo_view.branch_prs.clone(),
522                commits: commit_batch.to_vec(),
523            };
524
525            // Collect context for this batch if needed
526            let batch_context = if use_contextual {
527                Some(self.collect_context(&batch_repo_view).await?)
528            } else {
529                None
530            };
531
532            // Generate amendments for this batch
533            let batch_amendments = if let Some(ctx) = batch_context {
534                claude_client
535                    .generate_contextual_amendments_with_options(&batch_repo_view, &ctx, self.fresh)
536                    .await?
537            } else {
538                claude_client
539                    .generate_amendments_with_options(&batch_repo_view, self.fresh)
540                    .await?
541            };
542
543            // Merge amendments from this batch
544            all_amendments
545                .amendments
546                .extend(batch_amendments.amendments);
547
548            if batch_num + 1 < total_batches {
549                println!("   ✅ Batch {}/{} completed", batch_num + 1, total_batches);
550                // Small delay between batches to be respectful to the API
551                tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
552            }
553        }
554
555        println!(
556            "✅ All batches completed! Found {} commits to improve.",
557            all_amendments.amendments.len()
558        );
559
560        // Handle different output modes
561        if let Some(save_path) = &self.save_only {
562            all_amendments.save_to_file(save_path)?;
563            println!("💾 Amendments saved to file");
564            return Ok(());
565        }
566
567        // Handle amendments
568        if !all_amendments.amendments.is_empty() {
569            // Create temporary file for amendments
570            let temp_dir = tempfile::tempdir()?;
571            let amendments_file = temp_dir.path().join("twiddle_amendments.yaml");
572            all_amendments.save_to_file(&amendments_file)?;
573
574            // Show file path and get user choice
575            if !self.auto_apply
576                && !self.handle_amendments_file(&amendments_file, &all_amendments)?
577            {
578                println!("❌ Amendment cancelled by user");
579                return Ok(());
580            }
581
582            // Apply all amendments (re-read from file to capture any user edits)
583            self.apply_amendments_from_file(&amendments_file).await?;
584            println!("✅ Commit messages improved successfully!");
585        } else {
586            println!("✨ No commits found to process!");
587        }
588
589        Ok(())
590    }
591
592    /// Generate repository view (reuse ViewCommand logic)
593    async fn generate_repository_view(&self) -> Result<crate::data::RepositoryView> {
594        use crate::data::{
595            AiInfo, BranchInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
596            WorkingDirectoryInfo,
597        };
598        use crate::git::{GitRepository, RemoteInfo};
599        use crate::utils::ai_scratch;
600
601        let commit_range = self.commit_range.as_deref().unwrap_or("HEAD~5..HEAD");
602
603        // Open git repository
604        let repo = GitRepository::open()
605            .context("Failed to open git repository. Make sure you're in a git repository.")?;
606
607        // Get current branch name
608        let current_branch = repo
609            .get_current_branch()
610            .unwrap_or_else(|_| "HEAD".to_string());
611
612        // Get working directory status
613        let wd_status = repo.get_working_directory_status()?;
614        let working_directory = WorkingDirectoryInfo {
615            clean: wd_status.clean,
616            untracked_changes: wd_status
617                .untracked_changes
618                .into_iter()
619                .map(|fs| FileStatusInfo {
620                    status: fs.status,
621                    file: fs.file,
622                })
623                .collect(),
624        };
625
626        // Get remote information
627        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
628
629        // Parse commit range and get commits
630        let commits = repo.get_commits_in_range(commit_range)?;
631
632        // Create version information
633        let versions = Some(VersionInfo {
634            omni_dev: env!("CARGO_PKG_VERSION").to_string(),
635        });
636
637        // Get AI scratch directory
638        let ai_scratch_path =
639            ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
640        let ai_info = AiInfo {
641            scratch: ai_scratch_path.to_string_lossy().to_string(),
642        };
643
644        // Build repository view with branch info
645        let mut repo_view = RepositoryView {
646            versions,
647            explanation: FieldExplanation::default(),
648            working_directory,
649            remotes,
650            ai: ai_info,
651            branch_info: Some(BranchInfo {
652                branch: current_branch,
653            }),
654            pr_template: None,
655            pr_template_location: None,
656            branch_prs: None,
657            commits,
658        };
659
660        // Update field presence based on actual data
661        repo_view.update_field_presence();
662
663        Ok(repo_view)
664    }
665
666    /// Handle amendments file - show path and get user choice
667    fn handle_amendments_file(
668        &self,
669        amendments_file: &std::path::Path,
670        amendments: &crate::data::amendments::AmendmentFile,
671    ) -> Result<bool> {
672        use std::io::{self, Write};
673
674        println!(
675            "\n📝 Found {} commits that could be improved.",
676            amendments.amendments.len()
677        );
678        println!("💾 Amendments saved to: {}", amendments_file.display());
679        println!();
680
681        loop {
682            print!("❓ [A]pply amendments, [S]how file, [E]dit file, or [Q]uit? [A/s/e/q] ");
683            io::stdout().flush()?;
684
685            let mut input = String::new();
686            io::stdin().read_line(&mut input)?;
687
688            match input.trim().to_lowercase().as_str() {
689                "a" | "apply" | "" => return Ok(true),
690                "s" | "show" => {
691                    self.show_amendments_file(amendments_file)?;
692                    println!();
693                }
694                "e" | "edit" => {
695                    self.edit_amendments_file(amendments_file)?;
696                    println!();
697                }
698                "q" | "quit" => return Ok(false),
699                _ => {
700                    println!(
701                        "Invalid choice. Please enter 'a' to apply, 's' to show, 'e' to edit, or 'q' to quit."
702                    );
703                }
704            }
705        }
706    }
707
708    /// Show the contents of the amendments file
709    fn show_amendments_file(&self, amendments_file: &std::path::Path) -> Result<()> {
710        use std::fs;
711
712        println!("\n📄 Amendments file contents:");
713        println!("─────────────────────────────");
714
715        let contents =
716            fs::read_to_string(amendments_file).context("Failed to read amendments file")?;
717
718        println!("{}", contents);
719        println!("─────────────────────────────");
720
721        Ok(())
722    }
723
724    /// Open the amendments file in an external editor
725    fn edit_amendments_file(&self, amendments_file: &std::path::Path) -> Result<()> {
726        use std::env;
727        use std::io::{self, Write};
728        use std::process::Command;
729
730        // Try to get editor from environment variables
731        let editor = env::var("OMNI_DEV_EDITOR")
732            .or_else(|_| env::var("EDITOR"))
733            .unwrap_or_else(|_| {
734                // Prompt user for editor if neither environment variable is set
735                println!(
736                    "🔧 Neither OMNI_DEV_EDITOR nor EDITOR environment variables are defined."
737                );
738                print!("Please enter the command to use as your editor: ");
739                io::stdout().flush().expect("Failed to flush stdout");
740
741                let mut input = String::new();
742                io::stdin()
743                    .read_line(&mut input)
744                    .expect("Failed to read user input");
745                input.trim().to_string()
746            });
747
748        if editor.is_empty() {
749            println!("❌ No editor specified. Returning to menu.");
750            return Ok(());
751        }
752
753        println!("📝 Opening amendments file in editor: {}", editor);
754
755        // Split editor command to handle arguments
756        let mut cmd_parts = editor.split_whitespace();
757        let editor_cmd = cmd_parts.next().unwrap_or(&editor);
758        let args: Vec<&str> = cmd_parts.collect();
759
760        let mut command = Command::new(editor_cmd);
761        command.args(args);
762        command.arg(amendments_file.to_string_lossy().as_ref());
763
764        match command.status() {
765            Ok(status) => {
766                if status.success() {
767                    println!("✅ Editor session completed.");
768                } else {
769                    println!(
770                        "⚠️  Editor exited with non-zero status: {:?}",
771                        status.code()
772                    );
773                }
774            }
775            Err(e) => {
776                println!("❌ Failed to execute editor '{}': {}", editor, e);
777                println!("   Please check that the editor command is correct and available in your PATH.");
778            }
779        }
780
781        Ok(())
782    }
783
784    /// Apply amendments from a file path (re-reads from disk to capture user edits)
785    async fn apply_amendments_from_file(&self, amendments_file: &std::path::Path) -> Result<()> {
786        use crate::git::AmendmentHandler;
787
788        // Use AmendmentHandler to apply amendments directly from file
789        let handler = AmendmentHandler::new().context("Failed to initialize amendment handler")?;
790        handler
791            .apply_amendments(&amendments_file.to_string_lossy())
792            .context("Failed to apply amendments")?;
793
794        Ok(())
795    }
796
797    /// Collect contextual information for enhanced commit message generation
798    async fn collect_context(
799        &self,
800        repo_view: &crate::data::RepositoryView,
801    ) -> Result<crate::data::context::CommitContext> {
802        use crate::claude::context::{BranchAnalyzer, ProjectDiscovery, WorkPatternAnalyzer};
803        use crate::data::context::CommitContext;
804
805        let mut context = CommitContext::new();
806
807        // 1. Discover project context
808        let context_dir = self
809            .context_dir
810            .as_ref()
811            .cloned()
812            .unwrap_or_else(|| std::path::PathBuf::from(".omni-dev"));
813
814        // ProjectDiscovery takes repo root and context directory
815        let repo_root = std::path::PathBuf::from(".");
816        let discovery = ProjectDiscovery::new(repo_root, context_dir.clone());
817        debug!(context_dir = ?context_dir, "Using context directory");
818        match discovery.discover() {
819            Ok(project_context) => {
820                debug!("Discovery successful");
821
822                // Show diagnostic information about loaded guidance files
823                self.show_guidance_files_status(&project_context, &context_dir)?;
824
825                context.project = project_context;
826            }
827            Err(e) => {
828                debug!(error = %e, "Discovery failed");
829                context.project = Default::default();
830            }
831        }
832
833        // 2. Analyze current branch from repository view
834        if let Some(branch_info) = &repo_view.branch_info {
835            context.branch = BranchAnalyzer::analyze(&branch_info.branch).unwrap_or_default();
836        } else {
837            // Fallback to getting current branch directly if not in repo view
838            use crate::git::GitRepository;
839            let repo = GitRepository::open()?;
840            let current_branch = repo
841                .get_current_branch()
842                .unwrap_or_else(|_| "HEAD".to_string());
843            context.branch = BranchAnalyzer::analyze(&current_branch).unwrap_or_default();
844        }
845
846        // 3. Analyze commit range patterns
847        if !repo_view.commits.is_empty() {
848            context.range = WorkPatternAnalyzer::analyze_commit_range(&repo_view.commits);
849        }
850
851        // 4. Apply user-provided context overrides
852        if let Some(ref work_ctx) = self.work_context {
853            context.user_provided = Some(work_ctx.clone());
854        }
855
856        if let Some(ref branch_ctx) = self.branch_context {
857            context.branch.description = branch_ctx.clone();
858        }
859
860        Ok(context)
861    }
862
863    /// Show context summary to user
864    fn show_context_summary(&self, context: &crate::data::context::CommitContext) -> Result<()> {
865        use crate::data::context::{VerbosityLevel, WorkPattern};
866
867        println!("🔍 Context Analysis:");
868
869        // Project context
870        if !context.project.valid_scopes.is_empty() {
871            let scope_names: Vec<&str> = context
872                .project
873                .valid_scopes
874                .iter()
875                .map(|s| s.name.as_str())
876                .collect();
877            println!("   📁 Valid scopes: {}", scope_names.join(", "));
878        }
879
880        // Branch context
881        if context.branch.is_feature_branch {
882            println!(
883                "   🌿 Branch: {} ({})",
884                context.branch.description, context.branch.work_type
885            );
886            if let Some(ref ticket) = context.branch.ticket_id {
887                println!("   🎫 Ticket: {}", ticket);
888            }
889        }
890
891        // Work pattern
892        match context.range.work_pattern {
893            WorkPattern::Sequential => println!("   🔄 Pattern: Sequential development"),
894            WorkPattern::Refactoring => println!("   🧹 Pattern: Refactoring work"),
895            WorkPattern::BugHunt => println!("   🐛 Pattern: Bug investigation"),
896            WorkPattern::Documentation => println!("   📖 Pattern: Documentation updates"),
897            WorkPattern::Configuration => println!("   ⚙️  Pattern: Configuration changes"),
898            WorkPattern::Unknown => {}
899        }
900
901        // Verbosity level
902        match context.suggested_verbosity() {
903            VerbosityLevel::Comprehensive => {
904                println!("   📝 Detail level: Comprehensive (significant changes detected)")
905            }
906            VerbosityLevel::Detailed => println!("   📝 Detail level: Detailed"),
907            VerbosityLevel::Concise => println!("   📝 Detail level: Concise"),
908        }
909
910        // User context
911        if let Some(ref user_ctx) = context.user_provided {
912            println!("   👤 User context: {}", user_ctx);
913        }
914
915        println!();
916        Ok(())
917    }
918
919    /// Show model information from actual AI client
920    fn show_model_info_from_client(
921        &self,
922        client: &crate::claude::client::ClaudeClient,
923    ) -> Result<()> {
924        use crate::claude::model_config::get_model_registry;
925
926        println!("🤖 AI Model Configuration:");
927
928        // Get actual metadata from the client
929        let metadata = client.get_ai_client_metadata();
930        let registry = get_model_registry();
931
932        if let Some(spec) = registry.get_model_spec(&metadata.model) {
933            // Highlight the API identifier portion in yellow
934            if metadata.model != spec.api_identifier {
935                println!(
936                    "   📡 Model: {} → \x1b[33m{}\x1b[0m",
937                    metadata.model, spec.api_identifier
938                );
939            } else {
940                println!("   📡 Model: \x1b[33m{}\x1b[0m", metadata.model);
941            }
942
943            println!("   🏷️  Provider: {}", spec.provider);
944            println!("   📊 Generation: {}", spec.generation);
945            println!("   ⭐ Tier: {} ({})", spec.tier, {
946                if let Some(tier_info) = registry.get_tier_info(&spec.provider, &spec.tier) {
947                    &tier_info.description
948                } else {
949                    "No description available"
950                }
951            });
952            println!("   📤 Max output tokens: {}", spec.max_output_tokens);
953            println!("   📥 Input context: {}", spec.input_context);
954
955            if spec.legacy {
956                println!("   ⚠️  Legacy model (consider upgrading to newer version)");
957            }
958        } else {
959            // Fallback to client metadata if not in registry
960            println!("   📡 Model: \x1b[33m{}\x1b[0m", metadata.model);
961            println!("   🏷️  Provider: {}", metadata.provider);
962            println!("   ⚠️  Model not found in registry, using client metadata:");
963            println!("   📤 Max output tokens: {}", metadata.max_response_length);
964            println!("   📥 Input context: {}", metadata.max_context_length);
965        }
966
967        println!();
968        Ok(())
969    }
970
971    /// Show diagnostic information about loaded guidance files
972    fn show_guidance_files_status(
973        &self,
974        project_context: &crate::data::context::ProjectContext,
975        context_dir: &std::path::Path,
976    ) -> Result<()> {
977        println!("📋 Project guidance files status:");
978
979        // Check commit guidelines
980        let guidelines_found = project_context.commit_guidelines.is_some();
981        let guidelines_source = if guidelines_found {
982            let local_path = context_dir.join("local").join("commit-guidelines.md");
983            let project_path = context_dir.join("commit-guidelines.md");
984            let home_path = dirs::home_dir()
985                .map(|h| h.join(".omni-dev").join("commit-guidelines.md"))
986                .unwrap_or_default();
987
988            if local_path.exists() {
989                format!("✅ Local override: {}", local_path.display())
990            } else if project_path.exists() {
991                format!("✅ Project: {}", project_path.display())
992            } else if home_path.exists() {
993                format!("✅ Global: {}", home_path.display())
994            } else {
995                "✅ (source unknown)".to_string()
996            }
997        } else {
998            "❌ None found".to_string()
999        };
1000        println!("   📝 Commit guidelines: {}", guidelines_source);
1001
1002        // Check scopes
1003        let scopes_count = project_context.valid_scopes.len();
1004        let scopes_source = if scopes_count > 0 {
1005            let local_path = context_dir.join("local").join("scopes.yaml");
1006            let project_path = context_dir.join("scopes.yaml");
1007            let home_path = dirs::home_dir()
1008                .map(|h| h.join(".omni-dev").join("scopes.yaml"))
1009                .unwrap_or_default();
1010
1011            let source = if local_path.exists() {
1012                format!("Local override: {}", local_path.display())
1013            } else if project_path.exists() {
1014                format!("Project: {}", project_path.display())
1015            } else if home_path.exists() {
1016                format!("Global: {}", home_path.display())
1017            } else {
1018                "(source unknown + ecosystem defaults)".to_string()
1019            };
1020            format!("✅ {} ({} scopes)", source, scopes_count)
1021        } else {
1022            "❌ None found".to_string()
1023        };
1024        println!("   🎯 Valid scopes: {}", scopes_source);
1025
1026        println!();
1027        Ok(())
1028    }
1029
1030    /// Execute twiddle command without AI - create amendments with original messages
1031    async fn execute_no_ai(&self) -> Result<()> {
1032        use crate::data::amendments::{Amendment, AmendmentFile};
1033
1034        println!("📋 Generating amendments YAML without AI processing...");
1035
1036        // Generate repository view to get all commits
1037        let repo_view = self.generate_repository_view().await?;
1038
1039        // Create amendments with original commit messages (no AI improvements)
1040        let amendments: Vec<Amendment> = repo_view
1041            .commits
1042            .iter()
1043            .map(|commit| Amendment {
1044                commit: commit.hash.clone(),
1045                message: commit.original_message.clone(),
1046            })
1047            .collect();
1048
1049        let amendment_file = AmendmentFile { amendments };
1050
1051        // Handle different output modes
1052        if let Some(save_path) = &self.save_only {
1053            amendment_file.save_to_file(save_path)?;
1054            println!("💾 Amendments saved to file");
1055            return Ok(());
1056        }
1057
1058        // Handle amendments using the same flow as the AI-powered version
1059        if !amendment_file.amendments.is_empty() {
1060            // Create temporary file for amendments
1061            let temp_dir = tempfile::tempdir()?;
1062            let amendments_file = temp_dir.path().join("twiddle_amendments.yaml");
1063            amendment_file.save_to_file(&amendments_file)?;
1064
1065            // Show file path and get user choice
1066            if !self.auto_apply
1067                && !self.handle_amendments_file(&amendments_file, &amendment_file)?
1068            {
1069                println!("❌ Amendment cancelled by user");
1070                return Ok(());
1071            }
1072
1073            // Apply amendments (re-read from file to capture any user edits)
1074            self.apply_amendments_from_file(&amendments_file).await?;
1075            println!("✅ Commit messages applied successfully!");
1076        } else {
1077            println!("✨ No commits found to process!");
1078        }
1079
1080        Ok(())
1081    }
1082}
1083
1084impl BranchCommand {
1085    /// Execute branch command
1086    pub fn execute(self) -> Result<()> {
1087        match self.command {
1088            BranchSubcommands::Info(info_cmd) => info_cmd.execute(),
1089            BranchSubcommands::Create(create_cmd) => {
1090                // Use tokio runtime for async execution
1091                let rt = tokio::runtime::Runtime::new()
1092                    .context("Failed to create tokio runtime for PR creation")?;
1093                rt.block_on(create_cmd.execute())
1094            }
1095        }
1096    }
1097}
1098
1099impl InfoCommand {
1100    /// Execute info command
1101    pub fn execute(self) -> Result<()> {
1102        use crate::data::{
1103            AiInfo, BranchInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
1104            WorkingDirectoryInfo,
1105        };
1106        use crate::git::{GitRepository, RemoteInfo};
1107        use crate::utils::ai_scratch;
1108
1109        // Open git repository
1110        let repo = GitRepository::open()
1111            .context("Failed to open git repository. Make sure you're in a git repository.")?;
1112
1113        // Get current branch name
1114        let current_branch = repo.get_current_branch().context(
1115            "Failed to get current branch. Make sure you're not in detached HEAD state.",
1116        )?;
1117
1118        // Determine base branch
1119        let base_branch = match self.base_branch {
1120            Some(branch) => {
1121                // Validate that the specified base branch exists
1122                if !repo.branch_exists(&branch)? {
1123                    anyhow::bail!("Base branch '{}' does not exist", branch);
1124                }
1125                branch
1126            }
1127            None => {
1128                // Default to main or master
1129                if repo.branch_exists("main")? {
1130                    "main".to_string()
1131                } else if repo.branch_exists("master")? {
1132                    "master".to_string()
1133                } else {
1134                    anyhow::bail!("No default base branch found (main or master)");
1135                }
1136            }
1137        };
1138
1139        // Calculate commit range: [base_branch]..HEAD
1140        let commit_range = format!("{}..HEAD", base_branch);
1141
1142        // Get working directory status
1143        let wd_status = repo.get_working_directory_status()?;
1144        let working_directory = WorkingDirectoryInfo {
1145            clean: wd_status.clean,
1146            untracked_changes: wd_status
1147                .untracked_changes
1148                .into_iter()
1149                .map(|fs| FileStatusInfo {
1150                    status: fs.status,
1151                    file: fs.file,
1152                })
1153                .collect(),
1154        };
1155
1156        // Get remote information
1157        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
1158
1159        // Parse commit range and get commits
1160        let commits = repo.get_commits_in_range(&commit_range)?;
1161
1162        // Check for PR template
1163        let pr_template_result = Self::read_pr_template().ok();
1164        let (pr_template, pr_template_location) = match pr_template_result {
1165            Some((content, location)) => (Some(content), Some(location)),
1166            None => (None, None),
1167        };
1168
1169        // Get PRs for current branch
1170        let branch_prs = Self::get_branch_prs(&current_branch)
1171            .ok()
1172            .filter(|prs| !prs.is_empty());
1173
1174        // Create version information
1175        let versions = Some(VersionInfo {
1176            omni_dev: env!("CARGO_PKG_VERSION").to_string(),
1177        });
1178
1179        // Get AI scratch directory
1180        let ai_scratch_path =
1181            ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
1182        let ai_info = AiInfo {
1183            scratch: ai_scratch_path.to_string_lossy().to_string(),
1184        };
1185
1186        // Build repository view with branch info
1187        let mut repo_view = RepositoryView {
1188            versions,
1189            explanation: FieldExplanation::default(),
1190            working_directory,
1191            remotes,
1192            ai: ai_info,
1193            branch_info: Some(BranchInfo {
1194                branch: current_branch,
1195            }),
1196            pr_template,
1197            pr_template_location,
1198            branch_prs,
1199            commits,
1200        };
1201
1202        // Update field presence based on actual data
1203        repo_view.update_field_presence();
1204
1205        // Output as YAML
1206        let yaml_output = crate::data::to_yaml(&repo_view)?;
1207        println!("{}", yaml_output);
1208
1209        Ok(())
1210    }
1211
1212    /// Read PR template file if it exists, returning both content and location
1213    fn read_pr_template() -> Result<(String, String)> {
1214        use std::fs;
1215        use std::path::Path;
1216
1217        let template_path = Path::new(".github/pull_request_template.md");
1218        if template_path.exists() {
1219            let content = fs::read_to_string(template_path)
1220                .context("Failed to read .github/pull_request_template.md")?;
1221            Ok((content, template_path.to_string_lossy().to_string()))
1222        } else {
1223            anyhow::bail!("PR template file does not exist")
1224        }
1225    }
1226
1227    /// Get pull requests for the current branch using gh CLI
1228    fn get_branch_prs(branch_name: &str) -> Result<Vec<crate::data::PullRequest>> {
1229        use serde_json::Value;
1230        use std::process::Command;
1231
1232        // Use gh CLI to get PRs for the branch
1233        let output = Command::new("gh")
1234            .args([
1235                "pr",
1236                "list",
1237                "--head",
1238                branch_name,
1239                "--json",
1240                "number,title,state,url,body,baseRefName",
1241                "--limit",
1242                "50",
1243            ])
1244            .output()
1245            .context("Failed to execute gh command")?;
1246
1247        if !output.status.success() {
1248            anyhow::bail!(
1249                "gh command failed: {}",
1250                String::from_utf8_lossy(&output.stderr)
1251            );
1252        }
1253
1254        let json_str = String::from_utf8_lossy(&output.stdout);
1255        let prs_json: Value =
1256            serde_json::from_str(&json_str).context("Failed to parse PR JSON from gh")?;
1257
1258        let mut prs = Vec::new();
1259        if let Some(prs_array) = prs_json.as_array() {
1260            for pr_json in prs_array {
1261                if let (Some(number), Some(title), Some(state), Some(url), Some(body)) = (
1262                    pr_json.get("number").and_then(|n| n.as_u64()),
1263                    pr_json.get("title").and_then(|t| t.as_str()),
1264                    pr_json.get("state").and_then(|s| s.as_str()),
1265                    pr_json.get("url").and_then(|u| u.as_str()),
1266                    pr_json.get("body").and_then(|b| b.as_str()),
1267                ) {
1268                    let base = pr_json
1269                        .get("baseRefName")
1270                        .and_then(|b| b.as_str())
1271                        .unwrap_or("")
1272                        .to_string();
1273                    prs.push(crate::data::PullRequest {
1274                        number,
1275                        title: title.to_string(),
1276                        state: state.to_string(),
1277                        url: url.to_string(),
1278                        body: body.to_string(),
1279                        base,
1280                    });
1281                }
1282            }
1283        }
1284
1285        Ok(prs)
1286    }
1287}
1288
1289/// PR action choices
1290#[derive(Debug, PartialEq)]
1291enum PrAction {
1292    CreateNew,
1293    UpdateExisting,
1294    Cancel,
1295}
1296
1297/// AI-generated PR content with structured fields
1298#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
1299pub struct PrContent {
1300    /// Concise PR title (ideally 50-80 characters)
1301    pub title: String,
1302    /// Full PR description in markdown format
1303    pub description: String,
1304}
1305
1306impl CreateCommand {
1307    /// Execute create command
1308    pub async fn execute(self) -> Result<()> {
1309        match self.command {
1310            CreateSubcommands::Pr(pr_cmd) => pr_cmd.execute().await,
1311        }
1312    }
1313}
1314
1315impl CreatePrCommand {
1316    /// Determine if PR should be created as draft
1317    ///
1318    /// Priority order:
1319    /// 1. --ready flag (not draft)
1320    /// 2. --draft flag (draft)
1321    /// 3. OMNI_DEV_DEFAULT_DRAFT_PR env/config setting
1322    /// 4. Hard-coded default (draft)
1323    fn should_create_as_draft(&self) -> bool {
1324        use crate::utils::settings::get_env_var;
1325
1326        // Explicit flags take precedence
1327        if self.ready {
1328            return false;
1329        }
1330        if self.draft {
1331            return true;
1332        }
1333
1334        // Check configuration setting
1335        get_env_var("OMNI_DEV_DEFAULT_DRAFT_PR")
1336            .ok()
1337            .and_then(|val| match val.to_lowercase().as_str() {
1338                "true" | "1" | "yes" => Some(true),
1339                "false" | "0" | "no" => Some(false),
1340                _ => None,
1341            })
1342            .unwrap_or(true) // Default to draft if not configured
1343    }
1344
1345    /// Execute create PR command
1346    pub async fn execute(self) -> Result<()> {
1347        println!("🔄 Starting pull request creation process...");
1348
1349        // 1. Generate repository view (reuse InfoCommand logic)
1350        let repo_view = self.generate_repository_view()?;
1351
1352        // 2. Validate branch state (always needed)
1353        self.validate_branch_state(&repo_view)?;
1354
1355        // 3. Show guidance files status early (before AI processing)
1356        use crate::claude::context::ProjectDiscovery;
1357        let repo_root = std::path::PathBuf::from(".");
1358        let context_dir = std::path::PathBuf::from(".omni-dev");
1359        let discovery = ProjectDiscovery::new(repo_root, context_dir);
1360        let project_context = discovery.discover().unwrap_or_default();
1361        self.show_guidance_files_status(&project_context)?;
1362
1363        // 4. Show AI model configuration before generation
1364        let claude_client = crate::claude::create_default_claude_client(None)?;
1365        self.show_model_info_from_client(&claude_client)?;
1366
1367        // 5. Show branch analysis and commit information
1368        self.show_commit_range_info(&repo_view)?;
1369
1370        // 6. Show context analysis (quick collection for display only)
1371        let context = {
1372            use crate::claude::context::{BranchAnalyzer, WorkPatternAnalyzer};
1373            use crate::data::context::CommitContext;
1374            let mut context = CommitContext::new();
1375            context.project = project_context;
1376
1377            // Quick analysis for display
1378            if let Some(branch_info) = &repo_view.branch_info {
1379                context.branch = BranchAnalyzer::analyze(&branch_info.branch).unwrap_or_default();
1380            }
1381
1382            if !repo_view.commits.is_empty() {
1383                context.range = WorkPatternAnalyzer::analyze_commit_range(&repo_view.commits);
1384            }
1385            context
1386        };
1387        self.show_context_summary(&context)?;
1388
1389        // 7. Generate AI-powered PR content (title + description)
1390        debug!("About to generate PR content from AI");
1391        let (pr_content, _claude_client) = self
1392            .generate_pr_content_with_client_internal(&repo_view, claude_client)
1393            .await?;
1394
1395        // 8. Show detailed context information (like twiddle command)
1396        self.show_context_information(&repo_view).await?;
1397        debug!(
1398            generated_title = %pr_content.title,
1399            generated_description_length = pr_content.description.len(),
1400            generated_description_preview = %pr_content.description.lines().take(3).collect::<Vec<_>>().join("\\n"),
1401            "Generated PR content from AI"
1402        );
1403
1404        // 5. Handle different output modes
1405        if let Some(save_path) = self.save_only {
1406            let pr_yaml = crate::data::to_yaml(&pr_content)
1407                .context("Failed to serialize PR content to YAML")?;
1408            std::fs::write(&save_path, &pr_yaml).context("Failed to save PR details to file")?;
1409            println!("💾 PR details saved to: {}", save_path);
1410            return Ok(());
1411        }
1412
1413        // 6. Create temporary file for PR details
1414        debug!("About to serialize PR content to YAML");
1415        let temp_dir = tempfile::tempdir()?;
1416        let pr_file = temp_dir.path().join("pr-details.yaml");
1417
1418        debug!(
1419            pre_serialize_title = %pr_content.title,
1420            pre_serialize_description_length = pr_content.description.len(),
1421            pre_serialize_description_preview = %pr_content.description.lines().take(3).collect::<Vec<_>>().join("\\n"),
1422            "About to serialize PR content with to_yaml"
1423        );
1424
1425        let pr_yaml =
1426            crate::data::to_yaml(&pr_content).context("Failed to serialize PR content to YAML")?;
1427
1428        debug!(
1429            file_path = %pr_file.display(),
1430            yaml_content_length = pr_yaml.len(),
1431            yaml_content = %pr_yaml,
1432            original_title = %pr_content.title,
1433            original_description_length = pr_content.description.len(),
1434            "Writing PR details to temporary YAML file"
1435        );
1436
1437        std::fs::write(&pr_file, &pr_yaml)?;
1438
1439        // 7. Handle PR details file - show path and get user choice
1440        let pr_action = if self.auto_apply {
1441            // For auto-apply, default to update if PR exists, otherwise create new
1442            if repo_view
1443                .branch_prs
1444                .as_ref()
1445                .is_some_and(|prs| !prs.is_empty())
1446            {
1447                PrAction::UpdateExisting
1448            } else {
1449                PrAction::CreateNew
1450            }
1451        } else {
1452            self.handle_pr_file(&pr_file, &repo_view)?
1453        };
1454
1455        if pr_action == PrAction::Cancel {
1456            println!("❌ PR operation cancelled by user");
1457            return Ok(());
1458        }
1459
1460        // 8. Validate environment only when we're about to create the PR
1461        self.validate_environment()?;
1462
1463        // 9. Create or update PR (re-read from file to capture any user edits)
1464        let final_pr_yaml =
1465            std::fs::read_to_string(&pr_file).context("Failed to read PR details file")?;
1466
1467        debug!(
1468            yaml_length = final_pr_yaml.len(),
1469            yaml_content = %final_pr_yaml,
1470            "Read PR details YAML from file"
1471        );
1472
1473        let final_pr_content: PrContent = serde_yaml::from_str(&final_pr_yaml)
1474            .context("Failed to parse PR details YAML. Please check the file format.")?;
1475
1476        debug!(
1477            title = %final_pr_content.title,
1478            description_length = final_pr_content.description.len(),
1479            description_preview = %final_pr_content.description.lines().take(3).collect::<Vec<_>>().join("\\n"),
1480            "Parsed PR content from YAML"
1481        );
1482
1483        // Determine draft status
1484        let is_draft = self.should_create_as_draft();
1485
1486        match pr_action {
1487            PrAction::CreateNew => {
1488                self.create_github_pr(
1489                    &repo_view,
1490                    &final_pr_content.title,
1491                    &final_pr_content.description,
1492                    is_draft,
1493                    self.base.as_deref(),
1494                )?;
1495                println!("✅ Pull request created successfully!");
1496            }
1497            PrAction::UpdateExisting => {
1498                self.update_github_pr(
1499                    &repo_view,
1500                    &final_pr_content.title,
1501                    &final_pr_content.description,
1502                    self.base.as_deref(),
1503                )?;
1504                println!("✅ Pull request updated successfully!");
1505            }
1506            PrAction::Cancel => unreachable!(), // Already handled above
1507        }
1508
1509        Ok(())
1510    }
1511
1512    /// Validate environment and dependencies
1513    fn validate_environment(&self) -> Result<()> {
1514        // Check if gh CLI is available
1515        let gh_check = std::process::Command::new("gh")
1516            .args(["--version"])
1517            .output();
1518
1519        match gh_check {
1520            Ok(output) if output.status.success() => {
1521                // Test if gh can access the current repo (this validates both auth and repo access)
1522                let repo_check = std::process::Command::new("gh")
1523                    .args(["repo", "view", "--json", "name"])
1524                    .output();
1525
1526                match repo_check {
1527                    Ok(repo_output) if repo_output.status.success() => Ok(()),
1528                    Ok(repo_output) => {
1529                        // Get more specific error from stderr
1530                        let error_details = String::from_utf8_lossy(&repo_output.stderr);
1531                        if error_details.contains("authentication") || error_details.contains("login") {
1532                            anyhow::bail!("GitHub CLI (gh) authentication failed. Please run 'gh auth login' or check your GITHUB_TOKEN environment variable.")
1533                        } else {
1534                            anyhow::bail!("GitHub CLI (gh) cannot access this repository. Error: {}", error_details.trim())
1535                        }
1536                    }
1537                    Err(e) => anyhow::bail!("Failed to test GitHub CLI access: {}", e),
1538                }
1539            }
1540            _ => anyhow::bail!("GitHub CLI (gh) is not installed or not available in PATH. Please install it from https://cli.github.com/"),
1541        }
1542    }
1543
1544    /// Generate repository view (reuse InfoCommand logic)
1545    fn generate_repository_view(&self) -> Result<crate::data::RepositoryView> {
1546        use crate::data::{
1547            AiInfo, BranchInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
1548            WorkingDirectoryInfo,
1549        };
1550        use crate::git::{GitRepository, RemoteInfo};
1551        use crate::utils::ai_scratch;
1552
1553        // Open git repository
1554        let repo = GitRepository::open()
1555            .context("Failed to open git repository. Make sure you're in a git repository.")?;
1556
1557        // Get current branch name
1558        let current_branch = repo.get_current_branch().context(
1559            "Failed to get current branch. Make sure you're not in detached HEAD state.",
1560        )?;
1561
1562        // Get remote information to determine proper remote and main branch
1563        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
1564
1565        // Find the primary remote (prefer origin, fallback to first available)
1566        let primary_remote = remotes
1567            .iter()
1568            .find(|r| r.name == "origin")
1569            .or_else(|| remotes.first())
1570            .ok_or_else(|| anyhow::anyhow!("No remotes found in repository"))?;
1571
1572        // Determine base branch (with remote prefix)
1573        let base_branch = match self.base.as_ref() {
1574            Some(branch) => {
1575                // User specified base branch - try to resolve it
1576                // First, check if it's already a valid remote ref (e.g., "origin/main")
1577                let remote_ref = format!("refs/remotes/{}", branch);
1578                if repo.repository().find_reference(&remote_ref).is_ok() {
1579                    branch.clone()
1580                } else {
1581                    // Try prepending the primary remote name (e.g., "main" -> "origin/main")
1582                    let with_remote = format!("{}/{}", primary_remote.name, branch);
1583                    let remote_ref = format!("refs/remotes/{}", with_remote);
1584                    if repo.repository().find_reference(&remote_ref).is_ok() {
1585                        with_remote
1586                    } else {
1587                        anyhow::bail!(
1588                            "Remote branch '{}' does not exist (also tried '{}')",
1589                            branch,
1590                            with_remote
1591                        );
1592                    }
1593                }
1594            }
1595            None => {
1596                // Auto-detect using the primary remote's main branch
1597                let main_branch = &primary_remote.main_branch;
1598                if main_branch == "unknown" {
1599                    anyhow::bail!(
1600                        "Could not determine main branch for remote '{}'",
1601                        primary_remote.name
1602                    );
1603                }
1604
1605                let remote_main = format!("{}/{}", primary_remote.name, main_branch);
1606
1607                // Validate that the remote main branch exists
1608                let remote_ref = format!("refs/remotes/{}", remote_main);
1609                if repo.repository().find_reference(&remote_ref).is_err() {
1610                    anyhow::bail!(
1611                        "Remote main branch '{}' does not exist. Try running 'git fetch' first.",
1612                        remote_main
1613                    );
1614                }
1615
1616                remote_main
1617            }
1618        };
1619
1620        // Calculate commit range: [remote_base]..HEAD
1621        let commit_range = format!("{}..HEAD", base_branch);
1622
1623        // Get working directory status
1624        let wd_status = repo.get_working_directory_status()?;
1625        let working_directory = WorkingDirectoryInfo {
1626            clean: wd_status.clean,
1627            untracked_changes: wd_status
1628                .untracked_changes
1629                .into_iter()
1630                .map(|fs| FileStatusInfo {
1631                    status: fs.status,
1632                    file: fs.file,
1633                })
1634                .collect(),
1635        };
1636
1637        // Get remote information
1638        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
1639
1640        // Parse commit range and get commits
1641        let commits = repo.get_commits_in_range(&commit_range)?;
1642
1643        // Check for PR template
1644        let pr_template_result = InfoCommand::read_pr_template().ok();
1645        let (pr_template, pr_template_location) = match pr_template_result {
1646            Some((content, location)) => (Some(content), Some(location)),
1647            None => (None, None),
1648        };
1649
1650        // Get PRs for current branch
1651        let branch_prs = InfoCommand::get_branch_prs(&current_branch)
1652            .ok()
1653            .filter(|prs| !prs.is_empty());
1654
1655        // Create version information
1656        let versions = Some(VersionInfo {
1657            omni_dev: env!("CARGO_PKG_VERSION").to_string(),
1658        });
1659
1660        // Get AI scratch directory
1661        let ai_scratch_path =
1662            ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
1663        let ai_info = AiInfo {
1664            scratch: ai_scratch_path.to_string_lossy().to_string(),
1665        };
1666
1667        // Build repository view with branch info
1668        let mut repo_view = RepositoryView {
1669            versions,
1670            explanation: FieldExplanation::default(),
1671            working_directory,
1672            remotes,
1673            ai: ai_info,
1674            branch_info: Some(BranchInfo {
1675                branch: current_branch,
1676            }),
1677            pr_template,
1678            pr_template_location,
1679            branch_prs,
1680            commits,
1681        };
1682
1683        // Update field presence based on actual data
1684        repo_view.update_field_presence();
1685
1686        Ok(repo_view)
1687    }
1688
1689    /// Validate branch state for PR creation
1690    fn validate_branch_state(&self, repo_view: &crate::data::RepositoryView) -> Result<()> {
1691        // Check if working directory is clean
1692        if !repo_view.working_directory.clean {
1693            anyhow::bail!(
1694                "Working directory has uncommitted changes. Please commit or stash your changes before creating a PR."
1695            );
1696        }
1697
1698        // Check if there are any untracked changes
1699        if !repo_view.working_directory.untracked_changes.is_empty() {
1700            let file_list: Vec<&str> = repo_view
1701                .working_directory
1702                .untracked_changes
1703                .iter()
1704                .map(|f| f.file.as_str())
1705                .collect();
1706            anyhow::bail!(
1707                "Working directory has untracked changes: {}. Please commit or stash your changes before creating a PR.",
1708                file_list.join(", ")
1709            );
1710        }
1711
1712        // Check if commits exist
1713        if repo_view.commits.is_empty() {
1714            anyhow::bail!("No commits found to create PR from. Make sure you have commits that are not in the base branch.");
1715        }
1716
1717        // Check if PR already exists for this branch
1718        if let Some(existing_prs) = &repo_view.branch_prs {
1719            if !existing_prs.is_empty() {
1720                let pr_info: Vec<String> = existing_prs
1721                    .iter()
1722                    .map(|pr| format!("#{} ({})", pr.number, pr.state))
1723                    .collect();
1724
1725                println!(
1726                    "📋 Existing PR(s) found for this branch: {}",
1727                    pr_info.join(", ")
1728                );
1729                // Don't bail - we'll handle this in the main flow
1730            }
1731        }
1732
1733        Ok(())
1734    }
1735
1736    /// Show detailed context information (similar to twiddle command)
1737    async fn show_context_information(
1738        &self,
1739        _repo_view: &crate::data::RepositoryView,
1740    ) -> Result<()> {
1741        // Note: commit range info and context summary are now shown earlier
1742        // This method is kept for potential future detailed information
1743        // that should be shown after AI generation
1744
1745        Ok(())
1746    }
1747
1748    /// Show commit range and count information
1749    fn show_commit_range_info(&self, repo_view: &crate::data::RepositoryView) -> Result<()> {
1750        // Recreate the base branch determination logic from generate_repository_view
1751        let base_branch = match self.base.as_ref() {
1752            Some(branch) => {
1753                // User specified base branch
1754                // Get the primary remote name from repo_view
1755                let primary_remote_name = repo_view
1756                    .remotes
1757                    .iter()
1758                    .find(|r| r.name == "origin")
1759                    .or_else(|| repo_view.remotes.first())
1760                    .map(|r| r.name.as_str())
1761                    .unwrap_or("origin");
1762                // Check if already has remote prefix
1763                if branch.starts_with(&format!("{}/", primary_remote_name)) {
1764                    branch.clone()
1765                } else {
1766                    format!("{}/{}", primary_remote_name, branch)
1767                }
1768            }
1769            None => {
1770                // Auto-detected base branch from remotes
1771                repo_view
1772                    .remotes
1773                    .iter()
1774                    .find(|r| r.name == "origin")
1775                    .or_else(|| repo_view.remotes.first())
1776                    .map(|r| format!("{}/{}", r.name, r.main_branch))
1777                    .unwrap_or_else(|| "unknown".to_string())
1778            }
1779        };
1780
1781        let commit_range = format!("{}..HEAD", base_branch);
1782        let commit_count = repo_view.commits.len();
1783
1784        // Get current branch name
1785        let current_branch = repo_view
1786            .branch_info
1787            .as_ref()
1788            .map(|bi| bi.branch.as_str())
1789            .unwrap_or("unknown");
1790
1791        println!("📊 Branch Analysis:");
1792        println!("   🌿 Current branch: {}", current_branch);
1793        println!("   📏 Commit range: {}", commit_range);
1794        println!("   📝 Commits found: {} commits", commit_count);
1795        println!();
1796
1797        Ok(())
1798    }
1799
1800    /// Collect contextual information for enhanced PR generation (adapted from twiddle)
1801    async fn collect_context(
1802        &self,
1803        repo_view: &crate::data::RepositoryView,
1804    ) -> Result<crate::data::context::CommitContext> {
1805        use crate::claude::context::{BranchAnalyzer, ProjectDiscovery, WorkPatternAnalyzer};
1806        use crate::data::context::CommitContext;
1807        use crate::git::GitRepository;
1808
1809        let mut context = CommitContext::new();
1810
1811        // 1. Discover project context
1812        let context_dir = std::path::PathBuf::from(".omni-dev");
1813
1814        // ProjectDiscovery takes repo root and context directory
1815        let repo_root = std::path::PathBuf::from(".");
1816        let discovery = ProjectDiscovery::new(repo_root, context_dir.clone());
1817        match discovery.discover() {
1818            Ok(project_context) => {
1819                context.project = project_context;
1820            }
1821            Err(_e) => {
1822                context.project = Default::default();
1823            }
1824        }
1825
1826        // 2. Analyze current branch
1827        let repo = GitRepository::open()?;
1828        let current_branch = repo
1829            .get_current_branch()
1830            .unwrap_or_else(|_| "HEAD".to_string());
1831        context.branch = BranchAnalyzer::analyze(&current_branch).unwrap_or_default();
1832
1833        // 3. Analyze commit range patterns
1834        if !repo_view.commits.is_empty() {
1835            context.range = WorkPatternAnalyzer::analyze_commit_range(&repo_view.commits);
1836        }
1837
1838        Ok(context)
1839    }
1840
1841    /// Show guidance files status (adapted from twiddle)
1842    fn show_guidance_files_status(
1843        &self,
1844        project_context: &crate::data::context::ProjectContext,
1845    ) -> Result<()> {
1846        let context_dir = std::path::PathBuf::from(".omni-dev");
1847
1848        println!("📋 Project guidance files status:");
1849
1850        // Check PR guidelines (for PR commands)
1851        let pr_guidelines_found = project_context.pr_guidelines.is_some();
1852        let pr_guidelines_source = if pr_guidelines_found {
1853            let local_path = context_dir.join("local").join("pr-guidelines.md");
1854            let project_path = context_dir.join("pr-guidelines.md");
1855            let home_path = dirs::home_dir()
1856                .map(|h| h.join(".omni-dev").join("pr-guidelines.md"))
1857                .unwrap_or_default();
1858
1859            if local_path.exists() {
1860                format!("✅ Local override: {}", local_path.display())
1861            } else if project_path.exists() {
1862                format!("✅ Project: {}", project_path.display())
1863            } else if home_path.exists() {
1864                format!("✅ Global: {}", home_path.display())
1865            } else {
1866                "✅ (source unknown)".to_string()
1867            }
1868        } else {
1869            "❌ None found".to_string()
1870        };
1871        println!("   🔀 PR guidelines: {}", pr_guidelines_source);
1872
1873        // Check scopes
1874        let scopes_count = project_context.valid_scopes.len();
1875        let scopes_source = if scopes_count > 0 {
1876            let local_path = context_dir.join("local").join("scopes.yaml");
1877            let project_path = context_dir.join("scopes.yaml");
1878            let home_path = dirs::home_dir()
1879                .map(|h| h.join(".omni-dev").join("scopes.yaml"))
1880                .unwrap_or_default();
1881
1882            let source = if local_path.exists() {
1883                format!("Local override: {}", local_path.display())
1884            } else if project_path.exists() {
1885                format!("Project: {}", project_path.display())
1886            } else if home_path.exists() {
1887                format!("Global: {}", home_path.display())
1888            } else {
1889                "(source unknown + ecosystem defaults)".to_string()
1890            };
1891            format!("✅ {} ({} scopes)", source, scopes_count)
1892        } else {
1893            "❌ None found".to_string()
1894        };
1895        println!("   🎯 Valid scopes: {}", scopes_source);
1896
1897        // Check PR template
1898        let pr_template_path = std::path::Path::new(".github/pull_request_template.md");
1899        let pr_template_status = if pr_template_path.exists() {
1900            format!("✅ Project: {}", pr_template_path.display())
1901        } else {
1902            "❌ None found".to_string()
1903        };
1904        println!("   📋 PR template: {}", pr_template_status);
1905
1906        println!();
1907        Ok(())
1908    }
1909
1910    /// Show context summary (adapted from twiddle)
1911    fn show_context_summary(&self, context: &crate::data::context::CommitContext) -> Result<()> {
1912        use crate::data::context::{VerbosityLevel, WorkPattern};
1913
1914        println!("🔍 Context Analysis:");
1915
1916        // Project context
1917        if !context.project.valid_scopes.is_empty() {
1918            let scope_names: Vec<&str> = context
1919                .project
1920                .valid_scopes
1921                .iter()
1922                .map(|s| s.name.as_str())
1923                .collect();
1924            println!("   📁 Valid scopes: {}", scope_names.join(", "));
1925        }
1926
1927        // Branch context
1928        if context.branch.is_feature_branch {
1929            println!(
1930                "   🌿 Branch: {} ({})",
1931                context.branch.description, context.branch.work_type
1932            );
1933            if let Some(ref ticket) = context.branch.ticket_id {
1934                println!("   🎫 Ticket: {}", ticket);
1935            }
1936        }
1937
1938        // Work pattern
1939        match context.range.work_pattern {
1940            WorkPattern::Sequential => println!("   🔄 Pattern: Sequential development"),
1941            WorkPattern::Refactoring => println!("   🧹 Pattern: Refactoring work"),
1942            WorkPattern::BugHunt => println!("   🐛 Pattern: Bug investigation"),
1943            WorkPattern::Documentation => println!("   📖 Pattern: Documentation updates"),
1944            WorkPattern::Configuration => println!("   ⚙️  Pattern: Configuration changes"),
1945            WorkPattern::Unknown => {}
1946        }
1947
1948        // Verbosity level
1949        match context.suggested_verbosity() {
1950            VerbosityLevel::Comprehensive => {
1951                println!("   📝 Detail level: Comprehensive (significant changes detected)")
1952            }
1953            VerbosityLevel::Detailed => println!("   📝 Detail level: Detailed"),
1954            VerbosityLevel::Concise => println!("   📝 Detail level: Concise"),
1955        }
1956
1957        println!();
1958        Ok(())
1959    }
1960
1961    /// Generate PR content with pre-created client (internal method that doesn't show model info)
1962    async fn generate_pr_content_with_client_internal(
1963        &self,
1964        repo_view: &crate::data::RepositoryView,
1965        claude_client: crate::claude::client::ClaudeClient,
1966    ) -> Result<(PrContent, crate::claude::client::ClaudeClient)> {
1967        use tracing::debug;
1968
1969        // Get PR template (either from repo or default)
1970        let pr_template = match &repo_view.pr_template {
1971            Some(template) => template.clone(),
1972            None => self.get_default_pr_template(),
1973        };
1974
1975        debug!(
1976            pr_template_length = pr_template.len(),
1977            pr_template_preview = %pr_template.lines().take(5).collect::<Vec<_>>().join("\\n"),
1978            "Using PR template for generation"
1979        );
1980
1981        println!("🤖 Generating AI-powered PR description...");
1982
1983        // Collect project context for PR guidelines
1984        debug!("Collecting context for PR generation");
1985        let context = self.collect_context(repo_view).await?;
1986        debug!("Context collection completed");
1987
1988        // Generate AI-powered PR content with context
1989        debug!("About to call Claude AI for PR content generation");
1990        match claude_client
1991            .generate_pr_content_with_context(repo_view, &pr_template, &context)
1992            .await
1993        {
1994            Ok(pr_content) => {
1995                debug!(
1996                    ai_generated_title = %pr_content.title,
1997                    ai_generated_description_length = pr_content.description.len(),
1998                    ai_generated_description_preview = %pr_content.description.lines().take(3).collect::<Vec<_>>().join("\\n"),
1999                    "AI successfully generated PR content"
2000                );
2001                Ok((pr_content, claude_client))
2002            }
2003            Err(e) => {
2004                debug!(error = %e, "AI PR generation failed, falling back to basic description");
2005                // Fallback to basic description with commit analysis (silently)
2006                let mut description = pr_template;
2007                self.enhance_description_with_commits(&mut description, repo_view)?;
2008
2009                // Generate fallback title from commits
2010                let title = self.generate_title_from_commits(repo_view);
2011
2012                debug!(
2013                    fallback_title = %title,
2014                    fallback_description_length = description.len(),
2015                    "Created fallback PR content"
2016                );
2017
2018                Ok((PrContent { title, description }, claude_client))
2019            }
2020        }
2021    }
2022
2023    /// Get default PR template when none exists in the repository
2024    fn get_default_pr_template(&self) -> String {
2025        r#"# Pull Request
2026
2027## Description
2028<!-- Provide a brief description of what this PR does -->
2029
2030## Type of Change
2031<!-- Mark the relevant option with an "x" -->
2032- [ ] Bug fix (non-breaking change which fixes an issue)
2033- [ ] New feature (non-breaking change which adds functionality)
2034- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
2035- [ ] Documentation update
2036- [ ] Refactoring (no functional changes)
2037- [ ] Performance improvement
2038- [ ] Test coverage improvement
2039
2040## Changes Made
2041<!-- List the specific changes made in this PR -->
2042- 
2043- 
2044- 
2045
2046## Testing
2047- [ ] All existing tests pass
2048- [ ] New tests added for new functionality
2049- [ ] Manual testing performed
2050
2051## Additional Notes
2052<!-- Add any additional notes for reviewers -->
2053"#.to_string()
2054    }
2055
2056    /// Enhance PR description with commit analysis
2057    fn enhance_description_with_commits(
2058        &self,
2059        description: &mut String,
2060        repo_view: &crate::data::RepositoryView,
2061    ) -> Result<()> {
2062        if repo_view.commits.is_empty() {
2063            return Ok(());
2064        }
2065
2066        // Add commit summary section
2067        description.push_str("\n---\n");
2068        description.push_str("## 📝 Commit Summary\n");
2069        description
2070            .push_str("*This section was automatically generated based on commit analysis*\n\n");
2071
2072        // Analyze commit types and scopes
2073        let mut types_found = std::collections::HashSet::new();
2074        let mut scopes_found = std::collections::HashSet::new();
2075        let mut has_breaking_changes = false;
2076
2077        for commit in &repo_view.commits {
2078            let detected_type = &commit.analysis.detected_type;
2079            types_found.insert(detected_type.clone());
2080            if detected_type.contains("BREAKING")
2081                || commit.original_message.contains("BREAKING CHANGE")
2082            {
2083                has_breaking_changes = true;
2084            }
2085
2086            let detected_scope = &commit.analysis.detected_scope;
2087            if !detected_scope.is_empty() {
2088                scopes_found.insert(detected_scope.clone());
2089            }
2090        }
2091
2092        // Update type checkboxes based on detected types
2093        if let Some(feat_pos) = description.find("- [ ] New feature") {
2094            if types_found.contains("feat") {
2095                description.replace_range(feat_pos..feat_pos + 5, "- [x]");
2096            }
2097        }
2098        if let Some(fix_pos) = description.find("- [ ] Bug fix") {
2099            if types_found.contains("fix") {
2100                description.replace_range(fix_pos..fix_pos + 5, "- [x]");
2101            }
2102        }
2103        if let Some(docs_pos) = description.find("- [ ] Documentation update") {
2104            if types_found.contains("docs") {
2105                description.replace_range(docs_pos..docs_pos + 5, "- [x]");
2106            }
2107        }
2108        if let Some(refactor_pos) = description.find("- [ ] Refactoring") {
2109            if types_found.contains("refactor") {
2110                description.replace_range(refactor_pos..refactor_pos + 5, "- [x]");
2111            }
2112        }
2113        if let Some(breaking_pos) = description.find("- [ ] Breaking change") {
2114            if has_breaking_changes {
2115                description.replace_range(breaking_pos..breaking_pos + 5, "- [x]");
2116            }
2117        }
2118
2119        // Add detected scopes
2120        if !scopes_found.is_empty() {
2121            let scopes_list: Vec<_> = scopes_found.into_iter().collect();
2122            description.push_str(&format!(
2123                "**Affected areas:** {}\n\n",
2124                scopes_list.join(", ")
2125            ));
2126        }
2127
2128        // Add commit list
2129        description.push_str("### Commits in this PR:\n");
2130        for commit in &repo_view.commits {
2131            let short_hash = &commit.hash[..8];
2132            let first_line = commit.original_message.lines().next().unwrap_or("").trim();
2133            description.push_str(&format!("- `{}` {}\n", short_hash, first_line));
2134        }
2135
2136        // Add file change summary
2137        let total_files: usize = repo_view
2138            .commits
2139            .iter()
2140            .map(|c| c.analysis.file_changes.total_files)
2141            .sum();
2142
2143        if total_files > 0 {
2144            description.push_str(&format!("\n**Files changed:** {} files\n", total_files));
2145        }
2146
2147        Ok(())
2148    }
2149
2150    /// Handle PR description file - show path and get user choice
2151    fn handle_pr_file(
2152        &self,
2153        pr_file: &std::path::Path,
2154        repo_view: &crate::data::RepositoryView,
2155    ) -> Result<PrAction> {
2156        use std::io::{self, Write};
2157
2158        println!("\n📝 PR details generated.");
2159        println!("💾 Details saved to: {}", pr_file.display());
2160
2161        // Show draft status
2162        let is_draft = self.should_create_as_draft();
2163        let status_icon = if is_draft { "📋" } else { "✅" };
2164        let status_text = if is_draft {
2165            "draft"
2166        } else {
2167            "ready for review"
2168        };
2169        println!("{} PR will be created as: {}", status_icon, status_text);
2170        println!();
2171
2172        // Check if there are existing PRs and show different options
2173        let has_existing_prs = repo_view
2174            .branch_prs
2175            .as_ref()
2176            .is_some_and(|prs| !prs.is_empty());
2177
2178        loop {
2179            if has_existing_prs {
2180                print!("❓ [U]pdate existing PR, [N]ew PR anyway, [S]how file, [E]dit file, or [Q]uit? [U/n/s/e/q] ");
2181            } else {
2182                print!(
2183                    "❓ [A]ccept and create PR, [S]how file, [E]dit file, or [Q]uit? [A/s/e/q] "
2184                );
2185            }
2186            io::stdout().flush()?;
2187
2188            let mut input = String::new();
2189            io::stdin().read_line(&mut input)?;
2190
2191            match input.trim().to_lowercase().as_str() {
2192                "u" | "update" if has_existing_prs => return Ok(PrAction::UpdateExisting),
2193                "n" | "new" if has_existing_prs => return Ok(PrAction::CreateNew),
2194                "a" | "accept" | "" if !has_existing_prs => return Ok(PrAction::CreateNew),
2195                "s" | "show" => {
2196                    self.show_pr_file(pr_file)?;
2197                    println!();
2198                }
2199                "e" | "edit" => {
2200                    self.edit_pr_file(pr_file)?;
2201                    println!();
2202                }
2203                "q" | "quit" => return Ok(PrAction::Cancel),
2204                _ => {
2205                    if has_existing_prs {
2206                        println!("Invalid choice. Please enter 'u' to update existing PR, 'n' for new PR, 's' to show, 'e' to edit, or 'q' to quit.");
2207                    } else {
2208                        println!("Invalid choice. Please enter 'a' to accept, 's' to show, 'e' to edit, or 'q' to quit.");
2209                    }
2210                }
2211            }
2212        }
2213    }
2214
2215    /// Show the contents of the PR details file
2216    fn show_pr_file(&self, pr_file: &std::path::Path) -> Result<()> {
2217        use std::fs;
2218
2219        println!("\n📄 PR details file contents:");
2220        println!("─────────────────────────────");
2221
2222        let contents = fs::read_to_string(pr_file).context("Failed to read PR details file")?;
2223        println!("{}", contents);
2224        println!("─────────────────────────────");
2225
2226        Ok(())
2227    }
2228
2229    /// Open the PR details file in an external editor
2230    fn edit_pr_file(&self, pr_file: &std::path::Path) -> Result<()> {
2231        use std::env;
2232        use std::io::{self, Write};
2233        use std::process::Command;
2234
2235        // Try to get editor from environment variables
2236        let editor = env::var("OMNI_DEV_EDITOR")
2237            .or_else(|_| env::var("EDITOR"))
2238            .unwrap_or_else(|_| {
2239                // Prompt user for editor if neither environment variable is set
2240                println!(
2241                    "🔧 Neither OMNI_DEV_EDITOR nor EDITOR environment variables are defined."
2242                );
2243                print!("Please enter the command to use as your editor: ");
2244                io::stdout().flush().expect("Failed to flush stdout");
2245
2246                let mut input = String::new();
2247                io::stdin()
2248                    .read_line(&mut input)
2249                    .expect("Failed to read user input");
2250                input.trim().to_string()
2251            });
2252
2253        if editor.is_empty() {
2254            println!("❌ No editor specified. Returning to menu.");
2255            return Ok(());
2256        }
2257
2258        println!("📝 Opening PR details file in editor: {}", editor);
2259
2260        // Split editor command to handle arguments
2261        let mut cmd_parts = editor.split_whitespace();
2262        let editor_cmd = cmd_parts.next().unwrap_or(&editor);
2263        let args: Vec<&str> = cmd_parts.collect();
2264
2265        let mut command = Command::new(editor_cmd);
2266        command.args(args);
2267        command.arg(pr_file.to_string_lossy().as_ref());
2268
2269        match command.status() {
2270            Ok(status) => {
2271                if status.success() {
2272                    println!("✅ Editor session completed.");
2273                } else {
2274                    println!(
2275                        "⚠️  Editor exited with non-zero status: {:?}",
2276                        status.code()
2277                    );
2278                }
2279            }
2280            Err(e) => {
2281                println!("❌ Failed to execute editor '{}': {}", editor, e);
2282                println!("   Please check that the editor command is correct and available in your PATH.");
2283            }
2284        }
2285
2286        Ok(())
2287    }
2288
2289    /// Generate a concise title from commit analysis (fallback)
2290    fn generate_title_from_commits(&self, repo_view: &crate::data::RepositoryView) -> String {
2291        if repo_view.commits.is_empty() {
2292            return "Pull Request".to_string();
2293        }
2294
2295        // For single commit, use its first line
2296        if repo_view.commits.len() == 1 {
2297            return repo_view.commits[0]
2298                .original_message
2299                .lines()
2300                .next()
2301                .unwrap_or("Pull Request")
2302                .trim()
2303                .to_string();
2304        }
2305
2306        // For multiple commits, generate from branch name
2307        let branch_name = repo_view
2308            .branch_info
2309            .as_ref()
2310            .map(|bi| bi.branch.as_str())
2311            .unwrap_or("feature");
2312
2313        let cleaned_branch = branch_name.replace(['/', '-', '_'], " ");
2314
2315        format!("feat: {}", cleaned_branch)
2316    }
2317
2318    /// Create new GitHub PR using gh CLI
2319    fn create_github_pr(
2320        &self,
2321        repo_view: &crate::data::RepositoryView,
2322        title: &str,
2323        description: &str,
2324        is_draft: bool,
2325        new_base: Option<&str>,
2326    ) -> Result<()> {
2327        use std::process::Command;
2328
2329        // Get branch name
2330        let branch_name = repo_view
2331            .branch_info
2332            .as_ref()
2333            .map(|bi| &bi.branch)
2334            .context("Branch info not available")?;
2335
2336        let pr_status = if is_draft {
2337            "draft"
2338        } else {
2339            "ready for review"
2340        };
2341        println!("🚀 Creating pull request ({})...", pr_status);
2342        println!("   📋 Title: {}", title);
2343        println!("   🌿 Branch: {}", branch_name);
2344        if let Some(base) = new_base {
2345            println!("   🎯 Base: {}", base);
2346        }
2347
2348        // Check if branch is pushed to remote and push if needed
2349        debug!("Opening git repository to check branch status");
2350        let git_repo =
2351            crate::git::GitRepository::open().context("Failed to open git repository")?;
2352
2353        debug!(
2354            "Checking if branch '{}' exists on remote 'origin'",
2355            branch_name
2356        );
2357        if !git_repo.branch_exists_on_remote(branch_name, "origin")? {
2358            println!("📤 Pushing branch to remote...");
2359            debug!(
2360                "Branch '{}' not found on remote, attempting to push",
2361                branch_name
2362            );
2363            git_repo
2364                .push_branch(branch_name, "origin")
2365                .context("Failed to push branch to remote")?;
2366        } else {
2367            debug!("Branch '{}' already exists on remote 'origin'", branch_name);
2368        }
2369
2370        // Create PR using gh CLI with explicit head branch
2371        debug!("Creating PR with gh CLI - title: '{}'", title);
2372        debug!("PR description length: {} characters", description.len());
2373        debug!("PR draft status: {}", is_draft);
2374        if let Some(base) = new_base {
2375            debug!("PR base branch: {}", base);
2376        }
2377
2378        let mut args = vec![
2379            "pr",
2380            "create",
2381            "--head",
2382            branch_name,
2383            "--title",
2384            title,
2385            "--body",
2386            description,
2387        ];
2388
2389        if let Some(base) = new_base {
2390            args.push("--base");
2391            args.push(base);
2392        }
2393
2394        if is_draft {
2395            args.push("--draft");
2396        }
2397
2398        let pr_result = Command::new("gh")
2399            .args(&args)
2400            .output()
2401            .context("Failed to create pull request")?;
2402
2403        if pr_result.status.success() {
2404            let pr_url = String::from_utf8_lossy(&pr_result.stdout);
2405            let pr_url = pr_url.trim();
2406            debug!("PR created successfully with URL: {}", pr_url);
2407            println!("🎉 Pull request created: {}", pr_url);
2408        } else {
2409            let error_msg = String::from_utf8_lossy(&pr_result.stderr);
2410            error!("gh CLI failed to create PR: {}", error_msg);
2411            anyhow::bail!("Failed to create pull request: {}", error_msg);
2412        }
2413
2414        Ok(())
2415    }
2416
2417    /// Update existing GitHub PR using gh CLI
2418    fn update_github_pr(
2419        &self,
2420        repo_view: &crate::data::RepositoryView,
2421        title: &str,
2422        description: &str,
2423        new_base: Option<&str>,
2424    ) -> Result<()> {
2425        use std::io::{self, Write};
2426        use std::process::Command;
2427
2428        // Get the first existing PR (assuming we're updating the most recent one)
2429        let existing_pr = repo_view
2430            .branch_prs
2431            .as_ref()
2432            .and_then(|prs| prs.first())
2433            .context("No existing PR found to update")?;
2434
2435        let pr_number = existing_pr.number;
2436        let current_base = &existing_pr.base;
2437
2438        println!("🚀 Updating pull request #{}...", pr_number);
2439        println!("   📋 Title: {}", title);
2440
2441        // Check if base branch should be changed
2442        let change_base = if let Some(base) = new_base {
2443            if !current_base.is_empty() && current_base != base {
2444                print!(
2445                    "   🎯 Current base: {} → New base: {}. Change? [y/N]: ",
2446                    current_base, base
2447                );
2448                io::stdout().flush()?;
2449
2450                let mut input = String::new();
2451                io::stdin().read_line(&mut input)?;
2452                let response = input.trim().to_lowercase();
2453                response == "y" || response == "yes"
2454            } else {
2455                false
2456            }
2457        } else {
2458            false
2459        };
2460
2461        debug!(
2462            pr_number = pr_number,
2463            title = %title,
2464            description_length = description.len(),
2465            description_preview = %description.lines().take(3).collect::<Vec<_>>().join("\\n"),
2466            change_base = change_base,
2467            "Updating GitHub PR with title and description"
2468        );
2469
2470        // Update PR using gh CLI
2471        let pr_number_str = pr_number.to_string();
2472        let mut gh_args = vec![
2473            "pr",
2474            "edit",
2475            &pr_number_str,
2476            "--title",
2477            title,
2478            "--body",
2479            description,
2480        ];
2481
2482        if change_base {
2483            if let Some(base) = new_base {
2484                gh_args.push("--base");
2485                gh_args.push(base);
2486            }
2487        }
2488
2489        debug!(
2490            args = ?gh_args,
2491            "Executing gh command to update PR"
2492        );
2493
2494        let pr_result = Command::new("gh")
2495            .args(&gh_args)
2496            .output()
2497            .context("Failed to update pull request")?;
2498
2499        if pr_result.status.success() {
2500            // Get the PR URL using the existing PR data
2501            println!("🎉 Pull request updated: {}", existing_pr.url);
2502            if change_base {
2503                if let Some(base) = new_base {
2504                    println!("   🎯 Base branch changed to: {}", base);
2505                }
2506            }
2507        } else {
2508            let error_msg = String::from_utf8_lossy(&pr_result.stderr);
2509            anyhow::bail!("Failed to update pull request: {}", error_msg);
2510        }
2511
2512        Ok(())
2513    }
2514
2515    /// Show model information from actual AI client
2516    fn show_model_info_from_client(
2517        &self,
2518        client: &crate::claude::client::ClaudeClient,
2519    ) -> Result<()> {
2520        use crate::claude::model_config::get_model_registry;
2521
2522        println!("🤖 AI Model Configuration:");
2523
2524        // Get actual metadata from the client
2525        let metadata = client.get_ai_client_metadata();
2526        let registry = get_model_registry();
2527
2528        if let Some(spec) = registry.get_model_spec(&metadata.model) {
2529            // Highlight the API identifier portion in yellow
2530            if metadata.model != spec.api_identifier {
2531                println!(
2532                    "   📡 Model: {} → \x1b[33m{}\x1b[0m",
2533                    metadata.model, spec.api_identifier
2534                );
2535            } else {
2536                println!("   📡 Model: \x1b[33m{}\x1b[0m", metadata.model);
2537            }
2538
2539            println!("   🏷️  Provider: {}", spec.provider);
2540            println!("   📊 Generation: {}", spec.generation);
2541            println!("   ⭐ Tier: {} ({})", spec.tier, {
2542                if let Some(tier_info) = registry.get_tier_info(&spec.provider, &spec.tier) {
2543                    &tier_info.description
2544                } else {
2545                    "No description available"
2546                }
2547            });
2548            println!("   📤 Max output tokens: {}", spec.max_output_tokens);
2549            println!("   📥 Input context: {}", spec.input_context);
2550
2551            if spec.legacy {
2552                println!("   ⚠️  Legacy model (consider upgrading to newer version)");
2553            }
2554        } else {
2555            // Fallback to client metadata if not in registry
2556            println!("   📡 Model: \x1b[33m{}\x1b[0m", metadata.model);
2557            println!("   🏷️  Provider: {}", metadata.provider);
2558            println!("   ⚠️  Model not found in registry, using client metadata:");
2559            println!("   📤 Max output tokens: {}", metadata.max_response_length);
2560            println!("   📥 Input context: {}", metadata.max_context_length);
2561        }
2562
2563        println!();
2564        Ok(())
2565    }
2566}
2567
2568impl CheckCommand {
2569    /// Execute check command - validates commit messages against guidelines
2570    pub async fn execute(self) -> Result<()> {
2571        use crate::data::check::OutputFormat;
2572
2573        // Parse output format
2574        let output_format: OutputFormat = self.format.parse().unwrap_or(OutputFormat::Text);
2575
2576        if !self.quiet && output_format == OutputFormat::Text {
2577            println!("🔍 Checking commit messages against guidelines...");
2578        }
2579
2580        // 1. Generate repository view to get all commits
2581        let repo_view = self.generate_repository_view().await?;
2582
2583        // 2. Check for empty commit range (exit code 3)
2584        if repo_view.commits.is_empty() {
2585            eprintln!("error: no commits found in range");
2586            std::process::exit(3);
2587        }
2588
2589        if !self.quiet && output_format == OutputFormat::Text {
2590            println!("📊 Found {} commits to check", repo_view.commits.len());
2591        }
2592
2593        // 3. Load commit guidelines
2594        let guidelines = self.load_guidelines().await?;
2595
2596        // 4. Initialize Claude client
2597        let claude_client = crate::claude::create_default_claude_client(self.model.clone())?;
2598
2599        if self.verbose && output_format == OutputFormat::Text {
2600            self.show_model_info(&claude_client)?;
2601        }
2602
2603        // 5. Check if batching is needed
2604        let report = if repo_view.commits.len() > self.batch_size {
2605            if !self.quiet && output_format == OutputFormat::Text {
2606                println!(
2607                    "📦 Processing {} commits in batches of {}...",
2608                    repo_view.commits.len(),
2609                    self.batch_size
2610                );
2611            }
2612            self.check_with_batching(&claude_client, &repo_view, guidelines.as_deref())
2613                .await?
2614        } else {
2615            // 6. Single batch check
2616            if !self.quiet && output_format == OutputFormat::Text {
2617                println!("🤖 Analyzing commits with AI...");
2618            }
2619            claude_client
2620                .check_commits(&repo_view, guidelines.as_deref(), !self.no_suggestions)
2621                .await?
2622        };
2623
2624        // 7. Output results
2625        self.output_report(&report, output_format)?;
2626
2627        // 8. Determine exit code
2628        let exit_code = report.exit_code(self.strict);
2629        if exit_code != 0 {
2630            std::process::exit(exit_code);
2631        }
2632
2633        Ok(())
2634    }
2635
2636    /// Generate repository view (reuse logic from TwiddleCommand)
2637    async fn generate_repository_view(&self) -> Result<crate::data::RepositoryView> {
2638        use crate::data::{
2639            AiInfo, BranchInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
2640            WorkingDirectoryInfo,
2641        };
2642        use crate::git::{GitRepository, RemoteInfo};
2643        use crate::utils::ai_scratch;
2644
2645        // Open git repository
2646        let repo = GitRepository::open()
2647            .context("Failed to open git repository. Make sure you're in a git repository.")?;
2648
2649        // Get current branch name
2650        let current_branch = repo
2651            .get_current_branch()
2652            .unwrap_or_else(|_| "HEAD".to_string());
2653
2654        // Determine commit range
2655        let commit_range = match &self.commit_range {
2656            Some(range) => range.clone(),
2657            None => {
2658                // Default to commits ahead of main branch
2659                let base = if repo.branch_exists("main")? {
2660                    "main"
2661                } else if repo.branch_exists("master")? {
2662                    "master"
2663                } else {
2664                    "HEAD~5"
2665                };
2666                format!("{}..HEAD", base)
2667            }
2668        };
2669
2670        // Get working directory status
2671        let wd_status = repo.get_working_directory_status()?;
2672        let working_directory = WorkingDirectoryInfo {
2673            clean: wd_status.clean,
2674            untracked_changes: wd_status
2675                .untracked_changes
2676                .into_iter()
2677                .map(|fs| FileStatusInfo {
2678                    status: fs.status,
2679                    file: fs.file,
2680                })
2681                .collect(),
2682        };
2683
2684        // Get remote information
2685        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
2686
2687        // Parse commit range and get commits
2688        let commits = repo.get_commits_in_range(&commit_range)?;
2689
2690        // Create version information
2691        let versions = Some(VersionInfo {
2692            omni_dev: env!("CARGO_PKG_VERSION").to_string(),
2693        });
2694
2695        // Get AI scratch directory
2696        let ai_scratch_path =
2697            ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
2698        let ai_info = AiInfo {
2699            scratch: ai_scratch_path.to_string_lossy().to_string(),
2700        };
2701
2702        // Build repository view with branch info
2703        let mut repo_view = RepositoryView {
2704            versions,
2705            explanation: FieldExplanation::default(),
2706            working_directory,
2707            remotes,
2708            ai: ai_info,
2709            branch_info: Some(BranchInfo {
2710                branch: current_branch,
2711            }),
2712            pr_template: None,
2713            pr_template_location: None,
2714            branch_prs: None,
2715            commits,
2716        };
2717
2718        // Update field presence based on actual data
2719        repo_view.update_field_presence();
2720
2721        Ok(repo_view)
2722    }
2723
2724    /// Load commit guidelines from file or context directory
2725    async fn load_guidelines(&self) -> Result<Option<String>> {
2726        use std::fs;
2727
2728        // If explicit guidelines path is provided, use it
2729        if let Some(guidelines_path) = &self.guidelines {
2730            let content = fs::read_to_string(guidelines_path).with_context(|| {
2731                format!("Failed to read guidelines file: {:?}", guidelines_path)
2732            })?;
2733            return Ok(Some(content));
2734        }
2735
2736        // Otherwise, use project discovery to find guidelines
2737        let context_dir = self
2738            .context_dir
2739            .clone()
2740            .unwrap_or_else(|| std::path::PathBuf::from(".omni-dev"));
2741
2742        // Try local override first
2743        let local_path = context_dir.join("local").join("commit-guidelines.md");
2744        if local_path.exists() {
2745            let content = fs::read_to_string(&local_path)
2746                .with_context(|| format!("Failed to read guidelines: {:?}", local_path))?;
2747            return Ok(Some(content));
2748        }
2749
2750        // Try project-level guidelines
2751        let project_path = context_dir.join("commit-guidelines.md");
2752        if project_path.exists() {
2753            let content = fs::read_to_string(&project_path)
2754                .with_context(|| format!("Failed to read guidelines: {:?}", project_path))?;
2755            return Ok(Some(content));
2756        }
2757
2758        // Try global guidelines
2759        if let Some(home) = dirs::home_dir() {
2760            let home_path = home.join(".omni-dev").join("commit-guidelines.md");
2761            if home_path.exists() {
2762                let content = fs::read_to_string(&home_path)
2763                    .with_context(|| format!("Failed to read guidelines: {:?}", home_path))?;
2764                return Ok(Some(content));
2765            }
2766        }
2767
2768        // No custom guidelines found, will use defaults
2769        Ok(None)
2770    }
2771
2772    /// Check commits with batching for large commit ranges
2773    async fn check_with_batching(
2774        &self,
2775        claude_client: &crate::claude::client::ClaudeClient,
2776        full_repo_view: &crate::data::RepositoryView,
2777        guidelines: Option<&str>,
2778    ) -> Result<crate::data::check::CheckReport> {
2779        use crate::data::check::{CheckReport, CommitCheckResult};
2780
2781        let commit_batches: Vec<_> = full_repo_view.commits.chunks(self.batch_size).collect();
2782        let total_batches = commit_batches.len();
2783        let mut all_results: Vec<CommitCheckResult> = Vec::new();
2784
2785        for (batch_num, commit_batch) in commit_batches.into_iter().enumerate() {
2786            if !self.quiet {
2787                println!(
2788                    "🔄 Processing batch {}/{} ({} commits)...",
2789                    batch_num + 1,
2790                    total_batches,
2791                    commit_batch.len()
2792                );
2793            }
2794
2795            // Create a repository view for just this batch
2796            let batch_repo_view = crate::data::RepositoryView {
2797                versions: full_repo_view.versions.clone(),
2798                explanation: full_repo_view.explanation.clone(),
2799                working_directory: full_repo_view.working_directory.clone(),
2800                remotes: full_repo_view.remotes.clone(),
2801                ai: full_repo_view.ai.clone(),
2802                branch_info: full_repo_view.branch_info.clone(),
2803                pr_template: full_repo_view.pr_template.clone(),
2804                pr_template_location: full_repo_view.pr_template_location.clone(),
2805                branch_prs: full_repo_view.branch_prs.clone(),
2806                commits: commit_batch.to_vec(),
2807            };
2808
2809            // Check this batch
2810            let batch_report = claude_client
2811                .check_commits(&batch_repo_view, guidelines, !self.no_suggestions)
2812                .await?;
2813
2814            // Merge results
2815            all_results.extend(batch_report.commits);
2816
2817            if batch_num + 1 < total_batches {
2818                // Small delay between batches
2819                tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
2820            }
2821        }
2822
2823        Ok(CheckReport::new(all_results))
2824    }
2825
2826    /// Output the check report in the specified format
2827    fn output_report(
2828        &self,
2829        report: &crate::data::check::CheckReport,
2830        format: crate::data::check::OutputFormat,
2831    ) -> Result<()> {
2832        use crate::data::check::OutputFormat;
2833
2834        match format {
2835            OutputFormat::Text => self.output_text_report(report),
2836            OutputFormat::Json => {
2837                let json = serde_json::to_string_pretty(report)
2838                    .context("Failed to serialize report to JSON")?;
2839                println!("{}", json);
2840                Ok(())
2841            }
2842            OutputFormat::Yaml => {
2843                let yaml =
2844                    crate::data::to_yaml(report).context("Failed to serialize report to YAML")?;
2845                println!("{}", yaml);
2846                Ok(())
2847            }
2848        }
2849    }
2850
2851    /// Output text format report
2852    fn output_text_report(&self, report: &crate::data::check::CheckReport) -> Result<()> {
2853        use crate::data::check::IssueSeverity;
2854
2855        println!();
2856
2857        for result in &report.commits {
2858            // Skip passing commits unless --show-passing is set
2859            if result.passes && !self.show_passing {
2860                continue;
2861            }
2862
2863            // Skip info-only commits in quiet mode
2864            if self.quiet {
2865                let has_errors_or_warnings = result
2866                    .issues
2867                    .iter()
2868                    .any(|i| matches!(i.severity, IssueSeverity::Error | IssueSeverity::Warning));
2869                if !has_errors_or_warnings {
2870                    continue;
2871                }
2872            }
2873
2874            // Determine icon
2875            let icon = if result.passes {
2876                "✅"
2877            } else if result
2878                .issues
2879                .iter()
2880                .any(|i| i.severity == IssueSeverity::Error)
2881            {
2882                "❌"
2883            } else {
2884                "⚠️ "
2885            };
2886
2887            // Short hash
2888            let short_hash = if result.hash.len() > 7 {
2889                &result.hash[..7]
2890            } else {
2891                &result.hash
2892            };
2893
2894            println!("{} {} - \"{}\"", icon, short_hash, result.message);
2895
2896            // Print issues
2897            for issue in &result.issues {
2898                // Skip info issues in quiet mode
2899                if self.quiet && issue.severity == IssueSeverity::Info {
2900                    continue;
2901                }
2902
2903                let severity_str = match issue.severity {
2904                    IssueSeverity::Error => "\x1b[31mERROR\x1b[0m  ",
2905                    IssueSeverity::Warning => "\x1b[33mWARNING\x1b[0m",
2906                    IssueSeverity::Info => "\x1b[36mINFO\x1b[0m   ",
2907                };
2908
2909                println!(
2910                    "   {} [{}] {}",
2911                    severity_str, issue.section, issue.explanation
2912                );
2913            }
2914
2915            // Print suggestion if available and not in quiet mode
2916            if !self.quiet {
2917                if let Some(suggestion) = &result.suggestion {
2918                    println!();
2919                    println!("   Suggested message:");
2920                    for line in suggestion.message.lines() {
2921                        println!("      {}", line);
2922                    }
2923                    if self.verbose {
2924                        println!();
2925                        println!("   Why this is better:");
2926                        for line in suggestion.explanation.lines() {
2927                            println!("   {}", line);
2928                        }
2929                    }
2930                }
2931            }
2932
2933            println!();
2934        }
2935
2936        // Print summary
2937        println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2938        println!("Summary: {} commits checked", report.summary.total_commits);
2939        println!(
2940            "  {} errors, {} warnings",
2941            report.summary.error_count, report.summary.warning_count
2942        );
2943        println!(
2944            "  {} passed, {} with issues",
2945            report.summary.passing_commits, report.summary.failing_commits
2946        );
2947
2948        Ok(())
2949    }
2950
2951    /// Show model information
2952    fn show_model_info(&self, client: &crate::claude::client::ClaudeClient) -> Result<()> {
2953        use crate::claude::model_config::get_model_registry;
2954
2955        println!("🤖 AI Model Configuration:");
2956
2957        let metadata = client.get_ai_client_metadata();
2958        let registry = get_model_registry();
2959
2960        if let Some(spec) = registry.get_model_spec(&metadata.model) {
2961            if metadata.model != spec.api_identifier {
2962                println!(
2963                    "   📡 Model: {} → \x1b[33m{}\x1b[0m",
2964                    metadata.model, spec.api_identifier
2965                );
2966            } else {
2967                println!("   📡 Model: \x1b[33m{}\x1b[0m", metadata.model);
2968            }
2969            println!("   🏷️  Provider: {}", spec.provider);
2970        } else {
2971            println!("   📡 Model: \x1b[33m{}\x1b[0m", metadata.model);
2972            println!("   🏷️  Provider: {}", metadata.provider);
2973        }
2974
2975        println!();
2976        Ok(())
2977    }
2978}