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
335 lines
10 KiB
Python
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()
|