omni_dev/cli/
git.rs

1//! Git-related CLI commands
2
3use anyhow::{Context, Result};
4use clap::{Parser, Subcommand};
5use tracing::debug;
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}
57
58/// View command options
59#[derive(Parser)]
60pub struct ViewCommand {
61    /// Commit range to analyze (e.g., HEAD~3..HEAD, abc123..def456)
62    #[arg(value_name = "COMMIT_RANGE")]
63    pub commit_range: Option<String>,
64}
65
66/// Amend command options  
67#[derive(Parser)]
68pub struct AmendCommand {
69    /// YAML file containing commit amendments
70    #[arg(value_name = "YAML_FILE")]
71    pub yaml_file: String,
72}
73
74/// Twiddle command options
75#[derive(Parser)]
76pub struct TwiddleCommand {
77    /// Commit range to analyze and improve (e.g., HEAD~3..HEAD, abc123..def456)
78    #[arg(value_name = "COMMIT_RANGE")]
79    pub commit_range: Option<String>,
80
81    /// Claude API model to use (if not specified, uses settings or default)
82    #[arg(long)]
83    pub model: Option<String>,
84
85    /// Skip confirmation prompt and apply amendments automatically
86    #[arg(long)]
87    pub auto_apply: bool,
88
89    /// Save generated amendments to file without applying
90    #[arg(long, value_name = "FILE")]
91    pub save_only: Option<String>,
92
93    /// Use additional project context for better suggestions (Phase 3)
94    #[arg(long, default_value = "true")]
95    pub use_context: bool,
96
97    /// Path to custom context directory (defaults to .omni-dev/)
98    #[arg(long)]
99    pub context_dir: Option<std::path::PathBuf>,
100
101    /// Specify work context (e.g., "feature: user authentication")
102    #[arg(long)]
103    pub work_context: Option<String>,
104
105    /// Override detected branch context
106    #[arg(long)]
107    pub branch_context: Option<String>,
108
109    /// Disable contextual analysis (use basic prompting only)
110    #[arg(long)]
111    pub no_context: bool,
112
113    /// Maximum number of commits to process in a single batch (default: 4)
114    #[arg(long, default_value = "4")]
115    pub batch_size: usize,
116}
117
118/// Branch operations
119#[derive(Parser)]
120pub struct BranchCommand {
121    /// Branch subcommand to execute
122    #[command(subcommand)]
123    pub command: BranchSubcommands,
124}
125
126/// Branch subcommands
127#[derive(Subcommand)]
128pub enum BranchSubcommands {
129    /// Analyze branch commits and output repository information in YAML format
130    Info(InfoCommand),
131}
132
133/// Info command options
134#[derive(Parser)]
135pub struct InfoCommand {
136    /// Base branch to compare against (defaults to main/master)
137    #[arg(value_name = "BASE_BRANCH")]
138    pub base_branch: Option<String>,
139}
140
141impl GitCommand {
142    /// Execute git command
143    pub fn execute(self) -> Result<()> {
144        match self.command {
145            GitSubcommands::Commit(commit_cmd) => commit_cmd.execute(),
146            GitSubcommands::Branch(branch_cmd) => branch_cmd.execute(),
147        }
148    }
149}
150
151impl CommitCommand {
152    /// Execute commit command
153    pub fn execute(self) -> Result<()> {
154        match self.command {
155            CommitSubcommands::Message(message_cmd) => message_cmd.execute(),
156        }
157    }
158}
159
160impl MessageCommand {
161    /// Execute message command
162    pub fn execute(self) -> Result<()> {
163        match self.command {
164            MessageSubcommands::View(view_cmd) => view_cmd.execute(),
165            MessageSubcommands::Amend(amend_cmd) => amend_cmd.execute(),
166            MessageSubcommands::Twiddle(twiddle_cmd) => {
167                // Use tokio runtime for async execution
168                let rt =
169                    tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?;
170                rt.block_on(twiddle_cmd.execute())
171            }
172        }
173    }
174}
175
176impl ViewCommand {
177    /// Execute view command
178    pub fn execute(self) -> Result<()> {
179        use crate::data::{
180            AiInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
181            WorkingDirectoryInfo,
182        };
183        use crate::git::{GitRepository, RemoteInfo};
184        use crate::utils::ai_scratch;
185
186        let commit_range = self.commit_range.as_deref().unwrap_or("HEAD");
187
188        // Open git repository
189        let repo = GitRepository::open()
190            .context("Failed to open git repository. Make sure you're in a git repository.")?;
191
192        // Get working directory status
193        let wd_status = repo.get_working_directory_status()?;
194        let working_directory = WorkingDirectoryInfo {
195            clean: wd_status.clean,
196            untracked_changes: wd_status
197                .untracked_changes
198                .into_iter()
199                .map(|fs| FileStatusInfo {
200                    status: fs.status,
201                    file: fs.file,
202                })
203                .collect(),
204        };
205
206        // Get remote information
207        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
208
209        // Parse commit range and get commits
210        let commits = repo.get_commits_in_range(commit_range)?;
211
212        // Create version information
213        let versions = Some(VersionInfo {
214            omni_dev: env!("CARGO_PKG_VERSION").to_string(),
215        });
216
217        // Get AI scratch directory
218        let ai_scratch_path =
219            ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
220        let ai_info = AiInfo {
221            scratch: ai_scratch_path.to_string_lossy().to_string(),
222        };
223
224        // Build repository view
225        let mut repo_view = RepositoryView {
226            versions,
227            explanation: FieldExplanation::default(),
228            working_directory,
229            remotes,
230            ai: ai_info,
231            branch_info: None,
232            pr_template: None,
233            branch_prs: None,
234            commits,
235        };
236
237        // Update field presence based on actual data
238        repo_view.update_field_presence();
239
240        // Output as YAML
241        let yaml_output = crate::data::to_yaml(&repo_view)?;
242        println!("{}", yaml_output);
243
244        Ok(())
245    }
246}
247
248impl AmendCommand {
249    /// Execute amend command
250    pub fn execute(self) -> Result<()> {
251        use crate::git::AmendmentHandler;
252
253        println!("๐Ÿ”„ Starting commit amendment process...");
254        println!("๐Ÿ“„ Loading amendments from: {}", self.yaml_file);
255
256        // Create amendment handler and apply amendments
257        let handler = AmendmentHandler::new().context("Failed to initialize amendment handler")?;
258
259        handler
260            .apply_amendments(&self.yaml_file)
261            .context("Failed to apply amendments")?;
262
263        Ok(())
264    }
265}
266
267impl TwiddleCommand {
268    /// Execute twiddle command with contextual intelligence
269    pub async fn execute(self) -> Result<()> {
270        // Determine if contextual analysis should be used
271        let use_contextual = self.use_context && !self.no_context;
272
273        if use_contextual {
274            println!(
275                "๐Ÿช„ Starting AI-powered commit message improvement with contextual intelligence..."
276            );
277        } else {
278            println!("๐Ÿช„ Starting AI-powered commit message improvement...");
279        }
280
281        // 1. Generate repository view to get all commits
282        let full_repo_view = self.generate_repository_view().await?;
283
284        // 2. Check if batching is needed
285        if full_repo_view.commits.len() > self.batch_size {
286            println!(
287                "๐Ÿ“ฆ Processing {} commits in batches of {} to ensure reliable analysis...",
288                full_repo_view.commits.len(),
289                self.batch_size
290            );
291            return self
292                .execute_with_batching(use_contextual, full_repo_view)
293                .await;
294        }
295
296        // 3. Collect contextual information (Phase 3)
297        let context = if use_contextual {
298            Some(self.collect_context(&full_repo_view).await?)
299        } else {
300            None
301        };
302
303        // 4. Show context summary if available
304        if let Some(ref ctx) = context {
305            self.show_context_summary(ctx)?;
306        }
307
308        // 5. Initialize Claude client
309        let claude_client = crate::claude::create_default_claude_client(self.model.clone())?;
310
311        // Show model information
312        self.show_model_info()?;
313
314        // 6. Generate amendments via Claude API with context
315        if use_contextual && context.is_some() {
316            println!("๐Ÿค– Analyzing commits with enhanced contextual intelligence...");
317        } else {
318            println!("๐Ÿค– Analyzing commits with Claude AI...");
319        }
320
321        let amendments = if let Some(ctx) = context {
322            claude_client
323                .generate_contextual_amendments(&full_repo_view, &ctx)
324                .await?
325        } else {
326            claude_client.generate_amendments(&full_repo_view).await?
327        };
328
329        // 6. Handle different output modes
330        if let Some(save_path) = self.save_only {
331            amendments.save_to_file(save_path)?;
332            println!("๐Ÿ’พ Amendments saved to file");
333            return Ok(());
334        }
335
336        // 7. Handle amendments
337        if !amendments.amendments.is_empty() {
338            // Create temporary file for amendments
339            let temp_dir = tempfile::tempdir()?;
340            let amendments_file = temp_dir.path().join("twiddle_amendments.yaml");
341            amendments.save_to_file(&amendments_file)?;
342
343            // Show file path and get user choice
344            if !self.auto_apply && !self.handle_amendments_file(&amendments_file, &amendments)? {
345                println!("โŒ Amendment cancelled by user");
346                return Ok(());
347            }
348
349            // 8. Apply amendments
350            self.apply_amendments(amendments).await?;
351            println!("โœ… Commit messages improved successfully!");
352        } else {
353            println!("โœจ All commit messages are already well-formatted!");
354        }
355
356        Ok(())
357    }
358
359    /// Execute twiddle command with automatic batching for large commit ranges
360    async fn execute_with_batching(
361        &self,
362        use_contextual: bool,
363        full_repo_view: crate::data::RepositoryView,
364    ) -> Result<()> {
365        use crate::data::amendments::AmendmentFile;
366
367        // Initialize Claude client
368        let claude_client = crate::claude::create_default_claude_client(self.model.clone())?;
369
370        // Show model information
371        self.show_model_info()?;
372
373        // Split commits into batches
374        let commit_batches: Vec<_> = full_repo_view.commits.chunks(self.batch_size).collect();
375
376        let total_batches = commit_batches.len();
377        let mut all_amendments = AmendmentFile {
378            amendments: Vec::new(),
379        };
380
381        println!("๐Ÿ“Š Processing {} batches...", total_batches);
382
383        for (batch_num, commit_batch) in commit_batches.into_iter().enumerate() {
384            println!(
385                "๐Ÿ”„ Processing batch {}/{} ({} commits)...",
386                batch_num + 1,
387                total_batches,
388                commit_batch.len()
389            );
390
391            // Create a repository view for just this batch
392            let batch_repo_view = crate::data::RepositoryView {
393                versions: full_repo_view.versions.clone(),
394                explanation: full_repo_view.explanation.clone(),
395                working_directory: full_repo_view.working_directory.clone(),
396                remotes: full_repo_view.remotes.clone(),
397                ai: full_repo_view.ai.clone(),
398                branch_info: full_repo_view.branch_info.clone(),
399                pr_template: full_repo_view.pr_template.clone(),
400                branch_prs: full_repo_view.branch_prs.clone(),
401                commits: commit_batch.to_vec(),
402            };
403
404            // Collect context for this batch if needed
405            let batch_context = if use_contextual {
406                Some(self.collect_context(&batch_repo_view).await?)
407            } else {
408                None
409            };
410
411            // Generate amendments for this batch
412            let batch_amendments = if let Some(ctx) = batch_context {
413                claude_client
414                    .generate_contextual_amendments(&batch_repo_view, &ctx)
415                    .await?
416            } else {
417                claude_client.generate_amendments(&batch_repo_view).await?
418            };
419
420            // Merge amendments from this batch
421            all_amendments
422                .amendments
423                .extend(batch_amendments.amendments);
424
425            if batch_num + 1 < total_batches {
426                println!("   โœ… Batch {}/{} completed", batch_num + 1, total_batches);
427                // Small delay between batches to be respectful to the API
428                tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
429            }
430        }
431
432        println!(
433            "โœ… All batches completed! Found {} commits to improve.",
434            all_amendments.amendments.len()
435        );
436
437        // Handle different output modes
438        if let Some(save_path) = &self.save_only {
439            all_amendments.save_to_file(save_path)?;
440            println!("๐Ÿ’พ Amendments saved to file");
441            return Ok(());
442        }
443
444        // Handle amendments
445        if !all_amendments.amendments.is_empty() {
446            // Create temporary file for amendments
447            let temp_dir = tempfile::tempdir()?;
448            let amendments_file = temp_dir.path().join("twiddle_amendments.yaml");
449            all_amendments.save_to_file(&amendments_file)?;
450
451            // Show file path and get user choice
452            if !self.auto_apply
453                && !self.handle_amendments_file(&amendments_file, &all_amendments)?
454            {
455                println!("โŒ Amendment cancelled by user");
456                return Ok(());
457            }
458
459            // Apply all amendments
460            self.apply_amendments(all_amendments).await?;
461            println!("โœ… Commit messages improved successfully!");
462        } else {
463            println!("โœจ All commit messages are already well-formatted!");
464        }
465
466        Ok(())
467    }
468
469    /// Generate repository view (reuse ViewCommand logic)
470    async fn generate_repository_view(&self) -> Result<crate::data::RepositoryView> {
471        use crate::data::{
472            AiInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
473            WorkingDirectoryInfo,
474        };
475        use crate::git::{GitRepository, RemoteInfo};
476        use crate::utils::ai_scratch;
477
478        let commit_range = self.commit_range.as_deref().unwrap_or("HEAD~5..HEAD");
479
480        // Open git repository
481        let repo = GitRepository::open()
482            .context("Failed to open git repository. Make sure you're in a git repository.")?;
483
484        // Get working directory status
485        let wd_status = repo.get_working_directory_status()?;
486        let working_directory = WorkingDirectoryInfo {
487            clean: wd_status.clean,
488            untracked_changes: wd_status
489                .untracked_changes
490                .into_iter()
491                .map(|fs| FileStatusInfo {
492                    status: fs.status,
493                    file: fs.file,
494                })
495                .collect(),
496        };
497
498        // Get remote information
499        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
500
501        // Parse commit range and get commits
502        let commits = repo.get_commits_in_range(commit_range)?;
503
504        // Create version information
505        let versions = Some(VersionInfo {
506            omni_dev: env!("CARGO_PKG_VERSION").to_string(),
507        });
508
509        // Get AI scratch directory
510        let ai_scratch_path =
511            ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
512        let ai_info = AiInfo {
513            scratch: ai_scratch_path.to_string_lossy().to_string(),
514        };
515
516        // Build repository view
517        let mut repo_view = RepositoryView {
518            versions,
519            explanation: FieldExplanation::default(),
520            working_directory,
521            remotes,
522            ai: ai_info,
523            branch_info: None,
524            pr_template: None,
525            branch_prs: None,
526            commits,
527        };
528
529        // Update field presence based on actual data
530        repo_view.update_field_presence();
531
532        Ok(repo_view)
533    }
534
535    /// Handle amendments file - show path and get user choice
536    fn handle_amendments_file(
537        &self,
538        amendments_file: &std::path::Path,
539        amendments: &crate::data::amendments::AmendmentFile,
540    ) -> Result<bool> {
541        use std::io::{self, Write};
542
543        println!(
544            "\n๐Ÿ“ Found {} commits that could be improved.",
545            amendments.amendments.len()
546        );
547        println!("๐Ÿ’พ Amendments saved to: {}", amendments_file.display());
548        println!();
549
550        loop {
551            print!("โ“ [A]pply amendments, [S]how file, [E]dit file, or [Q]uit? [A/s/e/q] ");
552            io::stdout().flush()?;
553
554            let mut input = String::new();
555            io::stdin().read_line(&mut input)?;
556
557            match input.trim().to_lowercase().as_str() {
558                "a" | "apply" | "" => return Ok(true),
559                "s" | "show" => {
560                    self.show_amendments_file(amendments_file)?;
561                    println!();
562                }
563                "e" | "edit" => {
564                    self.edit_amendments_file(amendments_file)?;
565                    println!();
566                }
567                "q" | "quit" => return Ok(false),
568                _ => {
569                    println!(
570                        "Invalid choice. Please enter 'a' to apply, 's' to show, 'e' to edit, or 'q' to quit."
571                    );
572                }
573            }
574        }
575    }
576
577    /// Show the contents of the amendments file
578    fn show_amendments_file(&self, amendments_file: &std::path::Path) -> Result<()> {
579        use std::fs;
580
581        println!("\n๐Ÿ“„ Amendments file contents:");
582        println!("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€");
583
584        let contents =
585            fs::read_to_string(amendments_file).context("Failed to read amendments file")?;
586
587        println!("{}", contents);
588        println!("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€");
589
590        Ok(())
591    }
592
593    /// Open the amendments file in an external editor
594    fn edit_amendments_file(&self, amendments_file: &std::path::Path) -> Result<()> {
595        use std::env;
596        use std::io::{self, Write};
597        use std::process::Command;
598
599        // Try to get editor from environment variables
600        let editor = env::var("OMNI_DEV_EDITOR")
601            .or_else(|_| env::var("EDITOR"))
602            .unwrap_or_else(|_| {
603                // Prompt user for editor if neither environment variable is set
604                println!(
605                    "๐Ÿ”ง Neither OMNI_DEV_EDITOR nor EDITOR environment variables are defined."
606                );
607                print!("Please enter the command to use as your editor: ");
608                io::stdout().flush().expect("Failed to flush stdout");
609
610                let mut input = String::new();
611                io::stdin()
612                    .read_line(&mut input)
613                    .expect("Failed to read user input");
614                input.trim().to_string()
615            });
616
617        if editor.is_empty() {
618            println!("โŒ No editor specified. Returning to menu.");
619            return Ok(());
620        }
621
622        println!("๐Ÿ“ Opening amendments file in editor: {}", editor);
623
624        // Split editor command to handle arguments
625        let mut cmd_parts = editor.split_whitespace();
626        let editor_cmd = cmd_parts.next().unwrap_or(&editor);
627        let args: Vec<&str> = cmd_parts.collect();
628
629        let mut command = Command::new(editor_cmd);
630        command.args(args);
631        command.arg(amendments_file.to_string_lossy().as_ref());
632
633        match command.status() {
634            Ok(status) => {
635                if status.success() {
636                    println!("โœ… Editor session completed.");
637                } else {
638                    println!(
639                        "โš ๏ธ  Editor exited with non-zero status: {:?}",
640                        status.code()
641                    );
642                }
643            }
644            Err(e) => {
645                println!("โŒ Failed to execute editor '{}': {}", editor, e);
646                println!("   Please check that the editor command is correct and available in your PATH.");
647            }
648        }
649
650        Ok(())
651    }
652
653    /// Apply amendments using existing AmendmentHandler logic
654    async fn apply_amendments(
655        &self,
656        amendments: crate::data::amendments::AmendmentFile,
657    ) -> Result<()> {
658        use crate::git::AmendmentHandler;
659
660        // Create temporary file for amendments
661        let temp_dir = tempfile::tempdir()?;
662        let temp_file = temp_dir.path().join("twiddle_amendments.yaml");
663        amendments.save_to_file(&temp_file)?;
664
665        // Use AmendmentHandler to apply amendments
666        let handler = AmendmentHandler::new().context("Failed to initialize amendment handler")?;
667        handler
668            .apply_amendments(&temp_file.to_string_lossy())
669            .context("Failed to apply amendments")?;
670
671        Ok(())
672    }
673
674    /// Collect contextual information for enhanced commit message generation
675    async fn collect_context(
676        &self,
677        repo_view: &crate::data::RepositoryView,
678    ) -> Result<crate::data::context::CommitContext> {
679        use crate::claude::context::{BranchAnalyzer, ProjectDiscovery, WorkPatternAnalyzer};
680        use crate::data::context::CommitContext;
681        use crate::git::GitRepository;
682
683        let mut context = CommitContext::new();
684
685        // 1. Discover project context
686        let context_dir = self
687            .context_dir
688            .as_ref()
689            .cloned()
690            .unwrap_or_else(|| std::path::PathBuf::from(".omni-dev"));
691
692        // ProjectDiscovery takes repo root and context directory
693        let repo_root = std::path::PathBuf::from(".");
694        let discovery = ProjectDiscovery::new(repo_root, context_dir.clone());
695        debug!(context_dir = ?context_dir, "Using context directory");
696        match discovery.discover() {
697            Ok(project_context) => {
698                debug!("Discovery successful");
699
700                // Show diagnostic information about loaded guidance files
701                self.show_guidance_files_status(&project_context, &context_dir)?;
702
703                context.project = project_context;
704            }
705            Err(e) => {
706                debug!(error = %e, "Discovery failed");
707                context.project = Default::default();
708            }
709        }
710
711        // 2. Analyze current branch
712        let repo = GitRepository::open()?;
713        let current_branch = repo
714            .get_current_branch()
715            .unwrap_or_else(|_| "HEAD".to_string());
716        context.branch = BranchAnalyzer::analyze(&current_branch).unwrap_or_default();
717
718        // 3. Analyze commit range patterns
719        if !repo_view.commits.is_empty() {
720            context.range = WorkPatternAnalyzer::analyze_commit_range(&repo_view.commits);
721        }
722
723        // 4. Apply user-provided context overrides
724        if let Some(ref work_ctx) = self.work_context {
725            context.user_provided = Some(work_ctx.clone());
726        }
727
728        if let Some(ref branch_ctx) = self.branch_context {
729            context.branch.description = branch_ctx.clone();
730        }
731
732        Ok(context)
733    }
734
735    /// Show context summary to user
736    fn show_context_summary(&self, context: &crate::data::context::CommitContext) -> Result<()> {
737        use crate::data::context::{VerbosityLevel, WorkPattern};
738
739        println!("๐Ÿ” Context Analysis:");
740
741        // Project context
742        if !context.project.valid_scopes.is_empty() {
743            let scope_names: Vec<&str> = context
744                .project
745                .valid_scopes
746                .iter()
747                .map(|s| s.name.as_str())
748                .collect();
749            println!("   ๐Ÿ“ Valid scopes: {}", scope_names.join(", "));
750        }
751
752        // Branch context
753        if context.branch.is_feature_branch {
754            println!(
755                "   ๐ŸŒฟ Branch: {} ({})",
756                context.branch.description, context.branch.work_type
757            );
758            if let Some(ref ticket) = context.branch.ticket_id {
759                println!("   ๐ŸŽซ Ticket: {}", ticket);
760            }
761        }
762
763        // Work pattern
764        match context.range.work_pattern {
765            WorkPattern::Sequential => println!("   ๐Ÿ”„ Pattern: Sequential development"),
766            WorkPattern::Refactoring => println!("   ๐Ÿงน Pattern: Refactoring work"),
767            WorkPattern::BugHunt => println!("   ๐Ÿ› Pattern: Bug investigation"),
768            WorkPattern::Documentation => println!("   ๐Ÿ“– Pattern: Documentation updates"),
769            WorkPattern::Configuration => println!("   โš™๏ธ  Pattern: Configuration changes"),
770            WorkPattern::Unknown => {}
771        }
772
773        // Verbosity level
774        match context.suggested_verbosity() {
775            VerbosityLevel::Comprehensive => {
776                println!("   ๐Ÿ“ Detail level: Comprehensive (significant changes detected)")
777            }
778            VerbosityLevel::Detailed => println!("   ๐Ÿ“ Detail level: Detailed"),
779            VerbosityLevel::Concise => println!("   ๐Ÿ“ Detail level: Concise"),
780        }
781
782        // User context
783        if let Some(ref user_ctx) = context.user_provided {
784            println!("   ๐Ÿ‘ค User context: {}", user_ctx);
785        }
786
787        println!();
788        Ok(())
789    }
790
791    /// Show model information and parameters
792    fn show_model_info(&self) -> Result<()> {
793        use crate::claude::model_config::get_model_registry;
794
795        // Get the model name from command line or settings
796        let model_name = self
797            .model
798            .clone()
799            .or_else(|| crate::utils::settings::get_env_var("ANTHROPIC_MODEL").ok())
800            .unwrap_or_else(|| "claude-3-haiku-20240307".to_string());
801
802        println!("๐Ÿค– AI Model Configuration:");
803
804        // Get model specifications from registry
805        let registry = get_model_registry();
806        if let Some(spec) = registry.get_model_spec(&model_name) {
807            // Highlight the API identifier portion in yellow
808            if model_name != spec.api_identifier {
809                println!(
810                    "   ๐Ÿ“ก Model: {} โ†’ \x1b[33m{}\x1b[0m",
811                    model_name, spec.api_identifier
812                );
813            } else {
814                println!("   ๐Ÿ“ก Model: \x1b[33m{}\x1b[0m", model_name);
815            }
816
817            println!("   ๐Ÿท๏ธ  Provider: {}", spec.provider);
818            println!("   ๐Ÿ“Š Generation: {}", spec.generation);
819            println!("   โญ Tier: {} ({})", spec.tier, {
820                if let Some(tier_info) = registry.get_tier_info(&spec.provider, &spec.tier) {
821                    &tier_info.description
822                } else {
823                    "No description available"
824                }
825            });
826            println!("   ๐Ÿ“ค Max output tokens: {}", spec.max_output_tokens);
827            println!("   ๐Ÿ“ฅ Input context: {}", spec.input_context);
828
829            if spec.legacy {
830                println!("   โš ๏ธ  Legacy model (consider upgrading to newer version)");
831            }
832        } else {
833            // Fallback to basic info if model not found in registry
834            let max_tokens = registry.get_max_output_tokens(&model_name);
835            let input_context = registry.get_input_context(&model_name);
836
837            println!("   ๐Ÿ“ก Model: \x1b[33m{}\x1b[0m", model_name);
838            println!("   โš ๏ธ  Model not found in registry, using defaults:");
839            println!("   ๐Ÿ“ค Max output tokens: {}", max_tokens);
840            println!("   ๐Ÿ“ฅ Input context: {}", input_context);
841        }
842
843        println!();
844        Ok(())
845    }
846
847    /// Show diagnostic information about loaded guidance files
848    fn show_guidance_files_status(
849        &self,
850        project_context: &crate::data::context::ProjectContext,
851        context_dir: &std::path::Path,
852    ) -> Result<()> {
853        println!("๐Ÿ“‹ Project guidance files status:");
854
855        // Check commit guidelines
856        let guidelines_found = project_context.commit_guidelines.is_some();
857        let guidelines_source = if guidelines_found {
858            let local_path = context_dir.join("local").join("commit-guidelines.md");
859            let project_path = context_dir.join("commit-guidelines.md");
860            let home_path = dirs::home_dir()
861                .map(|h| h.join(".omni-dev").join("commit-guidelines.md"))
862                .unwrap_or_default();
863
864            if local_path.exists() {
865                format!("โœ… Local override: {}", local_path.display())
866            } else if project_path.exists() {
867                format!("โœ… Project: {}", project_path.display())
868            } else if home_path.exists() {
869                format!("โœ… Global: {}", home_path.display())
870            } else {
871                "โœ… (source unknown)".to_string()
872            }
873        } else {
874            "โŒ None found".to_string()
875        };
876        println!("   ๐Ÿ“ Commit guidelines: {}", guidelines_source);
877
878        // Check scopes
879        let scopes_count = project_context.valid_scopes.len();
880        let scopes_source = if scopes_count > 0 {
881            let local_path = context_dir.join("local").join("scopes.yaml");
882            let project_path = context_dir.join("scopes.yaml");
883            let home_path = dirs::home_dir()
884                .map(|h| h.join(".omni-dev").join("scopes.yaml"))
885                .unwrap_or_default();
886
887            let source = if local_path.exists() {
888                format!("Local override: {}", local_path.display())
889            } else if project_path.exists() {
890                format!("Project: {}", project_path.display())
891            } else if home_path.exists() {
892                format!("Global: {}", home_path.display())
893            } else {
894                "(source unknown + ecosystem defaults)".to_string()
895            };
896            format!("โœ… {} ({} scopes)", source, scopes_count)
897        } else {
898            "โŒ None found".to_string()
899        };
900        println!("   ๐ŸŽฏ Valid scopes: {}", scopes_source);
901
902        println!();
903        Ok(())
904    }
905}
906
907impl BranchCommand {
908    /// Execute branch command
909    pub fn execute(self) -> Result<()> {
910        match self.command {
911            BranchSubcommands::Info(info_cmd) => info_cmd.execute(),
912        }
913    }
914}
915
916impl InfoCommand {
917    /// Execute info command
918    pub fn execute(self) -> Result<()> {
919        use crate::data::{
920            AiInfo, BranchInfo, FieldExplanation, FileStatusInfo, RepositoryView, VersionInfo,
921            WorkingDirectoryInfo,
922        };
923        use crate::git::{GitRepository, RemoteInfo};
924        use crate::utils::ai_scratch;
925
926        // Open git repository
927        let repo = GitRepository::open()
928            .context("Failed to open git repository. Make sure you're in a git repository.")?;
929
930        // Get current branch name
931        let current_branch = repo.get_current_branch().context(
932            "Failed to get current branch. Make sure you're not in detached HEAD state.",
933        )?;
934
935        // Determine base branch
936        let base_branch = match self.base_branch {
937            Some(branch) => {
938                // Validate that the specified base branch exists
939                if !repo.branch_exists(&branch)? {
940                    anyhow::bail!("Base branch '{}' does not exist", branch);
941                }
942                branch
943            }
944            None => {
945                // Default to main or master
946                if repo.branch_exists("main")? {
947                    "main".to_string()
948                } else if repo.branch_exists("master")? {
949                    "master".to_string()
950                } else {
951                    anyhow::bail!("No default base branch found (main or master)");
952                }
953            }
954        };
955
956        // Calculate commit range: [base_branch]..HEAD
957        let commit_range = format!("{}..HEAD", base_branch);
958
959        // Get working directory status
960        let wd_status = repo.get_working_directory_status()?;
961        let working_directory = WorkingDirectoryInfo {
962            clean: wd_status.clean,
963            untracked_changes: wd_status
964                .untracked_changes
965                .into_iter()
966                .map(|fs| FileStatusInfo {
967                    status: fs.status,
968                    file: fs.file,
969                })
970                .collect(),
971        };
972
973        // Get remote information
974        let remotes = RemoteInfo::get_all_remotes(repo.repository())?;
975
976        // Parse commit range and get commits
977        let commits = repo.get_commits_in_range(&commit_range)?;
978
979        // Check for PR template
980        let pr_template = Self::read_pr_template().ok();
981
982        // Get PRs for current branch
983        let branch_prs = Self::get_branch_prs(&current_branch)
984            .ok()
985            .filter(|prs| !prs.is_empty());
986
987        // Create version information
988        let versions = Some(VersionInfo {
989            omni_dev: env!("CARGO_PKG_VERSION").to_string(),
990        });
991
992        // Get AI scratch directory
993        let ai_scratch_path =
994            ai_scratch::get_ai_scratch_dir().context("Failed to determine AI scratch directory")?;
995        let ai_info = AiInfo {
996            scratch: ai_scratch_path.to_string_lossy().to_string(),
997        };
998
999        // Build repository view with branch info
1000        let mut repo_view = RepositoryView {
1001            versions,
1002            explanation: FieldExplanation::default(),
1003            working_directory,
1004            remotes,
1005            ai: ai_info,
1006            branch_info: Some(BranchInfo {
1007                branch: current_branch,
1008            }),
1009            pr_template,
1010            branch_prs,
1011            commits,
1012        };
1013
1014        // Update field presence based on actual data
1015        repo_view.update_field_presence();
1016
1017        // Output as YAML
1018        let yaml_output = crate::data::to_yaml(&repo_view)?;
1019        println!("{}", yaml_output);
1020
1021        Ok(())
1022    }
1023
1024    /// Read PR template file if it exists
1025    fn read_pr_template() -> Result<String> {
1026        use std::fs;
1027        use std::path::Path;
1028
1029        let template_path = Path::new(".github/pull_request_template.md");
1030        if template_path.exists() {
1031            fs::read_to_string(template_path)
1032                .context("Failed to read .github/pull_request_template.md")
1033        } else {
1034            anyhow::bail!("PR template file does not exist")
1035        }
1036    }
1037
1038    /// Get pull requests for the current branch using gh CLI
1039    fn get_branch_prs(branch_name: &str) -> Result<Vec<crate::data::PullRequest>> {
1040        use serde_json::Value;
1041        use std::process::Command;
1042
1043        // Use gh CLI to get PRs for the branch
1044        let output = Command::new("gh")
1045            .args([
1046                "pr",
1047                "list",
1048                "--head",
1049                branch_name,
1050                "--json",
1051                "number,title,state,url,body",
1052                "--limit",
1053                "50",
1054            ])
1055            .output()
1056            .context("Failed to execute gh command")?;
1057
1058        if !output.status.success() {
1059            anyhow::bail!(
1060                "gh command failed: {}",
1061                String::from_utf8_lossy(&output.stderr)
1062            );
1063        }
1064
1065        let json_str = String::from_utf8_lossy(&output.stdout);
1066        let prs_json: Value =
1067            serde_json::from_str(&json_str).context("Failed to parse PR JSON from gh")?;
1068
1069        let mut prs = Vec::new();
1070        if let Some(prs_array) = prs_json.as_array() {
1071            for pr_json in prs_array {
1072                if let (Some(number), Some(title), Some(state), Some(url), Some(body)) = (
1073                    pr_json.get("number").and_then(|n| n.as_u64()),
1074                    pr_json.get("title").and_then(|t| t.as_str()),
1075                    pr_json.get("state").and_then(|s| s.as_str()),
1076                    pr_json.get("url").and_then(|u| u.as_str()),
1077                    pr_json.get("body").and_then(|b| b.as_str()),
1078                ) {
1079                    prs.push(crate::data::PullRequest {
1080                        number,
1081                        title: title.to_string(),
1082                        state: state.to_string(),
1083                        url: url.to_string(),
1084                        body: body.to_string(),
1085                    });
1086                }
1087            }
1088        }
1089
1090        Ok(prs)
1091    }
1092}