"""CLI commands for gitignore-generator.""" import os import sys from pathlib import Path from typing import List, Optional import click from gitignore_generator import __version__ from gitignore_generator.config import config from gitignore_generator.template_loader import template_loader from gitignore_generator.validator import validator from gitignore_generator.cli.interactive import run_simple_interactive def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> None: """Print version and exit.""" if not value or ctx.resilient_parsing: return click.echo(f"gitignore-generator v{__version__}") ctx.exit() @click.group() @click.option("--version", is_flag=True, callback=print_version, expose_value=False, help="Show version") @click.option("--config", "-c", type=click.Path(exists=False), help="Path to config file") @click.pass_context def main(ctx: click.Context, config: Optional[str] = None) -> None: """Generate optimized .gitignore files for your projects.""" ctx.ensure_object(dict) @main.command("generate") @click.argument("templates", nargs=-1) @click.option("--output", "-o", default=None, help="Output file (default: .gitignore)") @click.option("--ide", multiple=True, help="Add IDE-specific templates") @click.option("--validate/--no-validate", default=True, help="Validate output syntax") @click.option("--overwrite", "-f", is_flag=True, default=False, help="Overwrite existing file") def generate( templates: tuple, output: Optional[str], ide: tuple, validate: bool, overwrite: bool ) -> None: """Generate .gitignore from templates.""" output_file = output or config.default_output if os.path.exists(output_file) and not overwrite: if not click.confirm(f"File '{output_file}' exists. Overwrite?"): return selected_templates = list(templates) for ide_template in ide: selected_templates.append(f"ide:{ide_template}") if not selected_templates: click.echo("Error: No templates specified. Use 'gitignore generate python' or 'gitignore list' to see templates.") sys.exit(1) content_parts = [] errors = [] for template_ref in selected_templates: if template_ref.startswith("ide:"): template_name = template_ref[4:] content = template_loader.get_template_content(template_name) category = "ides" else: template_name = template_ref content = template_loader.get_template_content(template_name) category = None if content: content_parts.append(f"# --- {template_name} ---\n") content_parts.append(content.rstrip()) content_parts.append("\n") else: errors.append(template_name) if errors: click.echo(f"Error: Templates not found: {', '.join(errors)}") click.echo("Use 'gitignore list' to see available templates.") sys.exit(1) if not content_parts: click.echo("Error: No content generated.") sys.exit(1) final_content = "\n".join(content_parts).rstrip() + "\n" if validate: summary = validator.get_summary(final_content) if summary["errors"] > 0: click.echo(f"\nValidation errors found:") for issue in summary["issues"]: if issue.severity == "error": click.echo(f" Line {issue.line_number}: {issue.message}") if not click.confirm("Generate anyway?"): sys.exit(1) if summary["warnings"] > 0: click.echo(f"\nValidation warnings:") for issue in summary["issues"]: if issue.severity == "warning": click.echo(f" Line {issue.line_number}: {issue.message}") try: with open(output_file, "w") as f: f.write(final_content) click.echo(f"Successfully generated '{output_file}' with {len(selected_templates)} template(s).") except IOError as e: click.echo(f"Error writing file: {e}") sys.exit(1) @main.command("list") @click.option("--category", "-c", type=click.Choice(["languages", "ides", "custom", "all"]), default="all") def list_templates(category: str) -> None: """List available templates.""" templates_by_cat = template_loader.get_templates_by_category() click.echo("Available Templates:\n") if category in ["all", "languages"]: langs = templates_by_cat.get("languages", []) click.echo(f"Languages ({len(langs)}):") for i, template in enumerate(langs, 1): click.echo(f" {template}", nl=False) if i % 4 == 0: click.echo() click.echo() if category in ["all", "ides"]: ides = templates_by_cat.get("ides", []) click.echo(f"IDEs ({len(ides)}):") for i, template in enumerate(ides, 1): click.echo(f" {template}", nl=False) if i % 4 == 0: click.echo() click.echo() if category in ["all", "custom"]: customs = templates_by_cat.get("custom", []) click.echo(f"Custom ({len(customs)}):") if customs: for template in customs: click.echo(f" {template}") else: click.echo(" (no custom templates)") @main.command("search") @click.argument("query") def search_templates(query: str) -> None: """Search for templates.""" results = template_loader.search_templates(query) if results: click.echo(f"Found {len(results)} template(s) matching '{query}':") for template in results: click.echo(f" - {template}") else: click.echo(f"No templates found matching '{query}'.") @main.command("interactive") @click.option("--output", "-o", default=None, help="Output file (default: .gitignore)") def interactive(output: Optional[str]) -> None: """Launch interactive wizard to build .gitignore.""" run_simple_interactive() @main.command("validate") @click.argument("file", type=click.Path(exists=True)) def validate_file(file: str) -> None: """Validate a .gitignore file.""" with open(file, "r") as f: content = f.read() summary = validator.get_summary(content) click.echo(f"Validation Results for '{file}':") click.echo(f" Status: {'Valid' if summary['valid'] else 'Invalid'}") click.echo(f" Errors: {summary['errors']}") click.echo(f" Warnings: {summary['warnings']}") if summary["issues"]: click.echo("\nIssues:") for issue in summary["issues"]: severity = "ERROR" if issue.severity == "error" else "WARNING" click.echo(f" [{severity}] Line {issue.line_number}: {issue.message}") click.echo(f" Pattern: {issue.line}") @main.command("template-add") @click.argument("name") @click.argument("file", type=click.Path(exists=True)) def template_add(name: str, file: str) -> None: """Add a custom template from file.""" with open(file, "r") as f: content = f.read() if template_loader.save_custom_template(name, content): click.echo(f"Template '{name}' added successfully.") else: click.echo(f"Error: Failed to add template '{name}'.") sys.exit(1) @main.command("template-remove") @click.argument("name") def template_remove(name: str) -> None: """Remove a custom template.""" if template_loader.delete_custom_template(name): click.echo(f"Template '{name}' removed successfully.") else: click.echo(f"Error: Template '{name}' not found or could not be removed.") sys.exit(1) @main.command("template-export") @click.argument("name") @click.argument("output", type=click.Path(exists=False)) def template_export(name: str, output: str) -> None: """Export a template to file.""" content = template_loader.get_template_content(name) if content is None: click.echo(f"Error: Template '{name}' not found.") sys.exit(1) try: with open(output, "w") as f: f.write(content) click.echo(f"Template '{name}' exported to '{output}'.") except IOError as e: click.echo(f"Error writing file: {e}") sys.exit(1) @main.command("info") @click.argument("template") def template_info(template: str) -> None: """Show information about a template.""" content = template_loader.get_template_content(template) if content is None: click.echo(f"Error: Template '{template}' not found.") sys.exit(1) lines = [l for l in content.splitlines() if l.strip() and not l.strip().startswith("#")] click.echo(f"Template: {template}") click.echo(f"Lines: {len(content.splitlines())}") click.echo(f"Patterns: {len(lines)}") click.echo("\nFirst 10 patterns:") for line in lines[:10]: click.echo(f" {line}")