Skip to main content

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