From 8333fdc57374e96261e3d2d1dcb8f201a1812727 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Thu, 29 Jan 2026 15:42:15 +0000 Subject: [PATCH] Initial upload: gitignore-cli-generator v1.0.0 --- src/gitignore_cli/main.py | 334 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 334 insertions(+) create mode 100644 src/gitignore_cli/main.py diff --git a/src/gitignore_cli/main.py b/src/gitignore_cli/main.py new file mode 100644 index 0000000..b76f748 --- /dev/null +++ b/src/gitignore_cli/main.py @@ -0,0 +1,334 @@ +"""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()