Files

277 lines
6.9 KiB
Python

"""CLI interface for gitignore-generator."""
import builtins
import sys
from typing import Optional
import click
from . import __version__
from .api import get_list, get_patterns, GitignoreIOError
from .cache import CacheManager
from .detector import ProjectDetector
from .generator import GitignoreGenerator
def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> None:
"""Print version information."""
if not value or ctx.resilient_parsing:
return
click.echo(f"gitignore-generator-cli v{__version__}")
ctx.exit()
@click.group()
@click.option(
'--version',
is_flag=True,
callback=print_version,
expose_value=False,
help='Show version information'
)
@click.option(
'--cache-dir',
type=click.Path(),
help='Custom cache directory'
)
@click.pass_context
def main(ctx: click.Context, cache_dir: Optional[str]) -> None:
"""Generate .gitignore files for your projects."""
ctx.ensure_object(dict)
if cache_dir:
ctx.obj['cache_dir'] = cache_dir
@main.command()
@click.argument('techs', nargs=-1, required=True)
@click.option(
'-o', '--output',
type=click.Path(),
help='Output file path (default: stdout)'
)
@click.option(
'--preview',
is_flag=True,
help='Preview without writing to file'
)
@click.option(
'--add-pattern',
multiple=True,
help='Add custom pattern'
)
@click.option(
'--force-refresh',
is_flag=True,
help='Force refresh from API, bypass cache'
)
@click.option(
'--merge',
is_flag=True,
help='Merge with existing .gitignore'
)
@click.pass_context
def generate(
ctx: click.Context,
techs: tuple,
output: Optional[str],
preview: bool,
add_pattern: tuple,
force_refresh: bool,
merge: bool
) -> None:
"""Generate .gitignore for specified tech stacks.
Examples:
gitignore-generator generate node python django
gitignore-generator generate node -o .gitignore --preview
gitignore-generator generate node --add-pattern "*.log"
"""
tech_list = [t.lower() for t in techs]
if preview and output:
click.echo("Error: --preview and -o cannot be used together", err=True)
sys.exit(1)
generator = GitignoreGenerator()
if merge and output:
try:
content = generator.merge_with_existing(
output, tech_list, builtins.list(add_pattern), force_refresh
)
except FileNotFoundError:
click.echo(f"Error: File not found: {output}", err=True)
sys.exit(1)
else:
try:
content = generator.generate_file(
tech_list, output, preview, builtins.list(add_pattern), force_refresh
)
except GitignoreIOError as e:
click.echo(f"Error: {e}", err=True)
sys.exit(1)
if preview:
click.echo(content)
if output and not preview:
click.echo(f"Created: {output}")
@main.command()
@click.option(
'-o', '--output',
type=click.Path(),
help='Output file path'
)
@click.option(
'--preview',
is_flag=True,
help='Preview without writing'
)
@click.option(
'--force-refresh',
is_flag=True,
help='Force refresh from API'
)
@click.pass_context
def detect(
ctx: click.Context,
output: Optional[str],
preview: bool,
force_refresh: bool
) -> None:
"""Auto-detect project type and suggest gitignore.
Scans current directory for project markers and suggests
appropriate gitignore technologies.
"""
detector = ProjectDetector()
suggestions = detector.suggest_gitignore()
details = detector.get_detection_details()
if not suggestions:
click.echo("No project type detected. Please specify tech stacks manually.")
return
click.echo("Detected technologies:")
for detail in details:
tech = detail['technology']
matched = ', '.join(detail['matched_files'][:3])
click.echo(f" - {tech}: {matched}")
click.echo(f"\nSuggested .gitignore for: {', '.join(suggestions)}")
if preview or output:
generator = GitignoreGenerator()
try:
content = generator.generate_file(
suggestions, output, preview, None, force_refresh
)
except GitignoreIOError as e:
click.echo(f"Error: {e}", err=True)
sys.exit(1)
if preview:
click.echo("\n--- Generated .gitignore ---")
click.echo(content)
if output and not preview:
click.echo(f"\nCreated: {output}")
@main.command()
@click.argument('search', required=False)
@click.option(
'--force-refresh',
is_flag=True,
help='Force refresh from API'
)
@click.pass_context
def list(
ctx: click.Context,
search: Optional[str],
force_refresh: bool
) -> None:
"""List all supported tech stacks.
Optionally filter by search term.
"""
try:
techs = get_list(force_refresh)
except GitignoreIOError as e:
click.echo(f"Error: {e}", err=True)
sys.exit(1)
if search:
search_lower = search.lower()
techs = [t for t in techs if search_lower in t.lower()]
click.echo(f"Matching '{search}':")
else:
click.echo(f"Supported technologies ({len(techs)}):")
for tech in sorted(techs):
click.echo(f" - {tech}")
@main.command()
@click.argument('tech')
@click.option(
'--force-refresh',
is_flag=True,
help='Force refresh from API'
)
@click.pass_context
def show(ctx: click.Context, tech: str, force_refresh: bool) -> None:
"""Show gitignore patterns for a specific tech.
Useful for previewing patterns before adding to your .gitignore.
"""
try:
patterns = get_patterns(tech.lower(), force_refresh)
click.echo(patterns)
except GitignoreIOError as e:
click.echo(f"Error: {e}", err=True)
sys.exit(1)
@main.command()
@click.pass_context
def cache(ctx: click.Context) -> None:
"""Manage cache.
Show cache statistics or clear cache.
"""
cache = CacheManager()
stats = cache.get_stats()
click.echo("Cache Statistics:")
click.echo(f" Total items: {stats['total_items']}")
click.echo(f" Valid items: {stats['valid_items']}")
click.echo(f" Expired items: {stats['expired_items']}")
click.echo(f" Cache directory: {stats['cache_dir']}")
@main.command()
@click.pass_context
def clear_cache(ctx: click.Context) -> None:
"""Clear all cached patterns."""
cache = CacheManager()
count = cache.clear()
click.echo(f"Cleared {count} cached items.")
@main.command()
@click.option(
'--days',
type=int,
default=7,
help='Remove entries older than DAYS (default: 7)'
)
@click.pass_context
def cleanup(ctx: click.Context, days: int) -> None:
"""Remove expired cache entries."""
cache = CacheManager(expiry_days=days)
count = cache.cleanup_expired()
click.echo(f"Cleaned up {count} expired entries.")