Add CLI commands and interactive mode
Some checks failed
CI / test (push) Has been cancelled

This commit is contained in:
2026-02-02 15:54:20 +00:00
parent 2ef0e059a0
commit f3c33681ff

View File

@@ -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}")