"""Main CLI application for gitignore-cli-generator.""" from pathlib import Path from typing import List, Optional import typer from rich import print as rprint from rich.panel import Panel from rich.table import Table from .custom_patterns import get_custom_patterns_manager from .interactive import run_interactive_mode from .pattern_merger import merge_templates from .template_loader import ( TemplateInfo, TemplateLoader, load_multiple_templates, ) app = typer.Typer( name="gitignore", help="Generate .gitignore files for any tech stack, framework, or IDE.", add_completion=False, ) loader = TemplateLoader() def get_output_path(output: Optional[Path] = None) -> Path: """Determine the output path for the .gitignore file.""" if output: return output return Path.cwd() / ".gitignore" def write_gitignore(content: str, output_path: Path) -> None: """Write the generated content to a .gitignore file.""" output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_text(content, encoding="utf-8") def display_preview(content: str) -> None: """Display a preview of the .gitignore content.""" rprint(Panel(content, title="Preview", expand=False)) @app.command("generate") def generate_command( templates: List[str] = typer.Argument( ..., help="Template names to generate .gitignore for (e.g., python react vscode)", ), output: Optional[Path] = typer.Option( None, "--output", "-o", help="Output file path (default: .gitignore in current directory)", ), dry_run: bool = typer.Option( False, "--dry-run", "-d", help="Show preview without writing file", ), include_custom: bool = typer.Option( True, "--include-custom/--no-include-custom", help="Include custom patterns", ), ) -> None: """Generate a .gitignore file from specified templates.""" if not templates: rprint("[bold red]Error:[/bold red] At least one template must be specified.") raise typer.Exit(1) loaded_templates, missing = load_multiple_templates(templates) for name in missing: similar = loader.get_similar_templates(name) if similar: rprint(f"[yellow]Template '{name}' not found. Did you mean: {', '.join(similar)}?[/yellow]") else: rprint(f"[yellow]Template '{name}' not found.[/yellow]") if not loaded_templates: rprint("[bold red]Error:[/bold red] No valid templates provided.") rprint("Use 'gitignore list' to see available templates.") raise typer.Exit(1) pattern_list = [t.patterns for t in loaded_templates] if include_custom: custom_manager = get_custom_patterns_manager() custom_content = custom_manager.get_patterns_content() if custom_content: pattern_list.append(custom_content) merged_content = merge_templates(pattern_list) if dry_run: display_preview(merged_content if merged_content else "# No patterns generated\n") else: output_path = get_output_path(output) write_gitignore(merged_content, output_path) rprint(f"[bold green]✓[/bold green] .gitignore written to {output_path}") rprint(f" Included {len(loaded_templates)} template(s)") @app.command("interactive") def interactive_command() -> None: """Run interactive wizard to select templates.""" selected = run_interactive_mode() if not selected: rprint("[yellow]No templates selected. Exiting.[/yellow]") raise typer.Exit(0) confirm = typer.confirm("\nGenerate .gitignore file with selected templates?", default=True) if not confirm: rprint("[yellow]Generation cancelled.[/yellow]") raise typer.Exit(0) loaded_templates, _ = load_multiple_templates(selected) pattern_list = [t.patterns for t in loaded_templates] custom_manager = get_custom_patterns_manager() custom_content = custom_manager.get_patterns_content() if custom_content: pattern_list.append(custom_content) merged_content = merge_templates(pattern_list) output_path = get_output_path() write_gitignore(merged_content, output_path) rprint(f"\n[bold green]✓[/bold green] .gitignore written to {output_path}") @app.command("list") def list_command( category: Optional[str] = typer.Option( None, "--category", "-c", help="Filter by category (language, framework, ide, os)", ), ) -> None: """List all available templates.""" templates = loader.load_all_templates() if category: templates = {k: v for k, v in templates.items() if v.category == category} if not templates: rprint(f"[yellow]No templates found in category: {category}[/yellow]") raise typer.Exit(0) table = Table(title="Available Templates") table.add_column("Name", style="cyan", no_wrap=True) table.add_column("Category", style="magenta") table.add_column("Description", style="green") for name, template in sorted(templates.items()): table.add_row(name, template.category, template.description) rprint(table) by_category = loader.get_templates_by_category() rprint(f"\n[bold]Total:[/bold] {len(templates)} templates") for cat, cats_templates in sorted(by_category.items()): count = sum(1 for t in cats_templates if t.name in templates) if count > 0: rprint(f" {cat}: {count}") @app.command("search") def search_command( query: str = typer.Argument(..., help="Search query"), ) -> None: """Search for templates matching the query.""" if not query: rprint("[bold red]Error:[/bold red] Search query is required.") raise typer.Exit(1) templates = loader.load_all_templates() query_lower = query.lower() matches: List[TemplateInfo] = [] for template in templates.values(): if (query_lower in template.name.lower() or query_lower in template.description.lower() or query_lower in template.category.lower()): matches.append(template) if not matches: rprint(f"[yellow]No templates found matching '{query}'.[/yellow]") raise typer.Exit(0) table = Table(title=f"Search Results for '{query}'") table.add_column("Name", style="cyan") table.add_column("Category", style="magenta") table.add_column("Description", style="green") for template in matches: table.add_row(template.name, template.category, template.description) rprint(table) rprint(f"\n[bold]Found {len(matches)} matching template(s).[/bold]") @app.command("show") def show_command( template_name: str = typer.Argument(..., help="Template name to show"), ) -> None: """Show the contents of a specific template.""" template = loader.load_template(template_name) if template is None: similar = loader.get_similar_templates(template_name) rprint(f"[bold red]Template '{template_name}' not found.[/bold red]") if similar: rprint(f"[yellow]Did you mean: {', '.join(similar)}?[/yellow]") raise typer.Exit(1) panel = Panel( template.patterns, title=f"{template.name} ({template.category})", expand=False, ) rprint(panel) @app.command("preview") def preview_command( templates: List[str] = typer.Argument( ..., help="Template names to preview", ), ) -> None: """Preview .gitignore content without writing to file.""" loaded_templates, missing = load_multiple_templates(templates) for name in missing: similar = loader.get_similar_templates(name) if similar: rprint(f"[yellow]Template '{name}' not found. Did you mean: {', '.join(similar)}?[/yellow]") else: rprint(f"[yellow]Template '{name}' not found.[/yellow]") if not loaded_templates: rprint("[bold red]Error:[/bold red] No valid templates provided.") raise typer.Exit(1) pattern_list = [t.patterns for t in loaded_templates] merged_content = merge_templates(pattern_list) display_preview(merged_content if merged_content else "# No patterns\n") @app.command("custom-add") def custom_add_command( pattern: str = typer.Argument(..., help="Pattern to add"), description: str = typer.Option("", "--description", "-d", help="Pattern description"), ) -> None: """Add a custom pattern.""" manager = get_custom_patterns_manager() if manager.add_pattern(pattern, description): rprint(f"[bold green]✓[/bold green] Added custom pattern: {pattern}") else: rprint(f"[yellow]Pattern already exists: {pattern}[/yellow]") @app.command("custom-list") def custom_list_command() -> None: """List all custom patterns.""" manager = get_custom_patterns_manager() patterns = manager.list_patterns(include_disabled=True) if not patterns: rprint("[yellow]No custom patterns defined.[/yellow]") rprint("Use 'gitignore custom-add ' to add one.") raise typer.Exit(0) table = Table(title="Custom Patterns") table.add_column("Pattern", style="cyan") table.add_column("Description", style="green") table.add_column("Status", style="magenta") for p in patterns: status = "[green]Enabled[/green]" if p.enabled else "[red]Disabled[/red]" table.add_row(p.pattern, p.description or "-", status) rprint(table) rprint(f"\n[bold]Total:[/bold] {manager.count()} patterns ({manager.count_enabled()} enabled)") @app.command("custom-remove") def custom_remove_command( pattern: str = typer.Argument(..., help="Pattern to remove"), ) -> None: """Remove a custom pattern.""" manager = get_custom_patterns_manager() if manager.remove_pattern(pattern): rprint(f"[bold green]✓[/bold green] Removed custom pattern: {pattern}") else: rprint(f"[red]Pattern not found: {pattern}[/red]") @app.command("custom-toggle") def custom_toggle_command( pattern: str = typer.Argument(..., help="Pattern to toggle"), ) -> None: """Toggle a custom pattern's enabled state.""" manager = get_custom_patterns_manager() new_state = manager.toggle_pattern(pattern) if new_state is None: rprint(f"[red]Pattern not found: {pattern}[/red]") raise typer.Exit(1) status = "enabled" if new_state else "disabled" rprint(f"[bold green]✓[/bold green] Pattern {pattern} is now {status}") @app.callback() def main_callback() -> None: """Main callback for the CLI application.""" pass if __name__ == "__main__": app()