Files
gitignore-cli-generator/src/gitignore_cli/main.py
7000pctAUTO 8333fdc573
Some checks failed
CI / test (3.10) (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.9) (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / type-check (push) Has been cancelled
CI / build (push) Has been cancelled
Initial upload: gitignore-cli-generator v1.0.0
2026-01-29 15:42:15 +00:00

335 lines
10 KiB
Python

"""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 <pattern>' 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()