277 lines
6.9 KiB
Python
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.")
|