"""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.")