From f3c33681ffffabcb53374f4edefe6dde354c7190 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Mon, 2 Feb 2026 15:54:20 +0000 Subject: [PATCH] Add CLI commands and interactive mode --- gitignore_generator/cli/commands.py | 260 ++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 gitignore_generator/cli/commands.py diff --git a/gitignore_generator/cli/commands.py b/gitignore_generator/cli/commands.py new file mode 100644 index 0000000..2abe93e --- /dev/null +++ b/gitignore_generator/cli/commands.py @@ -0,0 +1,260 @@ +"""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}")