"""CLI interface for gitignore-generator.""" import os import sys from pathlib import Path from typing import List, Optional import click from . import __version__ from .config import Config from .detector import ProjectDetector from .generator import GitignoreGenerator @click.group() @click.version_option(version=__version__, prog_name="gitignore-generator") @click.option( "--config", "-c", type=click.Path(exists=True, dir_okay=False, readable=True), help="Path to configuration file", ) @click.pass_context def main(ctx: click.Context, config: Optional[str]) -> None: """A CLI tool that generates .gitignore files for any project type.""" ctx.ensure_object(dict) config_obj = None if config: config_obj = Config.load(config) ctx.obj["config"] = config_obj @main.command("generate") @click.argument( "types", nargs=-1, type=click.Choice( [ "python", "javascript", "typescript", "java", "go", "rust", "dotnet", "php", "ruby", "django", "flask", "react", "vue", "angular", "rails", "laravel", "spring", "vscode", "jetbrains", "visualstudiocode", "linux", "macos", "windows", "docker", "gradle", "maven", "jupyter", "terraform", ] ), ) @click.option( "--output", "-o", type=click.Path(dir_okay=False, writable=True), default=".gitignore", help="Output file path (default: .gitignore)", ) @click.option( "--append/--overwrite", default=True, help="Append to existing file or overwrite (default: append)", ) @click.option( "--language", "-l", multiple=True, type=click.Choice( [ "python", "javascript", "typescript", "java", "go", "rust", "dotnet", "php", "ruby", ] ), help="Programming language(s) to include", ) @click.option( "--framework", "-f", multiple=True, type=click.Choice( [ "django", "flask", "react", "vue", "angular", "rails", "laravel", "spring", ] ), help="Framework(s) to include", ) @click.option( "--ide", "-i", multiple=True, type=click.Choice(["vscode", "jetbrains", "visualstudiocode"]), help="IDE(s) to include", ) @click.option( "--os", multiple=True, type=click.Choice(["linux", "macos", "windows"]), help="Operating system(s) to include", ) @click.option( "--tools", "-t", multiple=True, type=click.Choice( ["docker", "gradle", "maven", "jupyter", "terraform"] ), help="Tool(s) to include", ) @click.option( "--stdout", is_flag=True, default=False, help="Output to stdout instead of writing to file", ) @click.pass_context def generate( ctx: click.Context, types: tuple, output: str, append: bool, language: tuple, framework: tuple, ide: tuple, os_opts: tuple, tools: tuple, stdout: bool, ) -> None: """Generate a .gitignore file for the specified types.""" config = ctx.obj.get("config") generator = GitignoreGenerator(config) if types: for t in types: generator.add_template(t) else: selected_types = list(language) + list(framework) + list(ide) selected_types.extend(list(os_opts)) selected_types.extend(list(tools)) if not selected_types: detector = ProjectDetector(Path.cwd()) detected = detector.detect() for t in detected: generator.add_template(t) if not detected: click.echo( "No project type detected. Please specify types manually.", err=True, ) click.echo( "Use --language, --framework, --ide, --os, or --tools option.", err=True, ) sys.exit(1) else: for t in selected_types: generator.add_template(t) content = generator.generate() if stdout: click.echo(content) else: output_path = Path(output) if output_path.exists() and append: mode = "a" else: mode = "w" try: with open(output_path, mode) as f: f.write(content) click.echo(f"Successfully wrote to {output_path}") except PermissionError: click.echo(f"Permission denied: {output_path}", err=True) sys.exit(1) except OSError as e: click.echo(f"Error writing to {output_path}: {e}", err=True) sys.exit(1) @main.command("detect") @click.argument( "path", type=click.Path(exists=True, file_okay=False, readable=True), default=".", ) @click.pass_context def detect(ctx: click.Context, path: str) -> None: """Detect project type(s) from directory structure.""" detector = ProjectDetector(Path(path)) detected = detector.detect() if detected: click.echo("Detected project types:") for t in detected: click.echo(f" - {t}") else: click.echo("No known project type detected.") @main.command("list") @click.option( "--category", "-c", type=click.Choice( ["language", "framework", "ide", "os", "tools", "all"] ), default="all", help="Category to list (default: all)", ) def list_templates(category: str) -> None: """List available template types.""" templates = { "language": [ "python", "javascript", "typescript", "java", "go", "rust", "dotnet", "php", "ruby", ], "framework": [ "django", "flask", "react", "vue", "angular", "rails", "laravel", "spring", ], "ide": ["vscode", "jetbrains", "visualstudiocode"], "os": ["linux", "macos", "windows"], "tools": ["docker", "gradle", "maven", "jupyter", "terraform"], } if category == "all": for cat, items in templates.items(): click.echo(f"{cat.capitalize()}:") for item in items: click.echo(f" - {item}") elif category in templates: click.echo(f"{category.capitalize()}:") for item in templates[category]: click.echo(f" - {item}") @main.command("wizard") @click.pass_context def wizard(ctx: click.Context) -> None: """Interactive wizard to generate .gitignore.""" click.echo("=== gitignore-generator Wizard ===") click.echo("") languages = [ "python", "javascript", "typescript", "java", "go", "rust", "dotnet", "php", "ruby", ] frameworks = [ "django", "flask", "react", "vue", "angular", "rails", "laravel", "spring", ] ides = ["vscode", "jetbrains", "visualstudiocode"] oss = ["linux", "macos", "windows"] tools = ["docker", "gradle", "maven", "jupyter", "terraform"] selected = [] click.echo("Select programming languages (space-separated, Enter to skip):") for idx, lang in enumerate(languages, 1): click.echo(f" {idx}. {lang}") selected_langs = click.prompt( "Languages", default="", type=str ).split() for lang in selected_langs: if lang in languages: selected.append(lang) click.echo("") click.echo("Select frameworks (space-separated, Enter to skip):") for idx, fw in enumerate(frameworks, 1): click.echo(f" {idx}. {fw}") selected_fws = click.prompt( "Frameworks", default="", type=str ).split() for fw in selected_fws: if fw in frameworks: selected.append(fw) click.echo("") click.echo("Select IDEs (space-separated, Enter to skip):") for idx, ide in enumerate(ides, 1): click.echo(f" {idx}. {ide}") selected_ides = click.prompt("IDEs", default="", type=str).split() for ide in selected_ides: if ide in ides: selected.append(ide) click.echo("") click.echo("Select operating systems (space-separated, Enter to skip):") for idx, os_name in enumerate(oss, 1): click.echo(f" {idx}. {os_name}") selected_oss = click.prompt("OS", default="", type=str).split() for os_name in selected_oss: if os_name in oss: selected.append(os_name) click.echo("") click.echo("Select tools (space-separated, Enter to skip):") for idx, tool in enumerate(tools, 1): click.echo(f" {idx}. {tool}") selected_tools = click.prompt("Tools", default="", type=str).split() for tool in selected_tools: if tool in tools: selected.append(tool) if not selected: click.echo("No templates selected.") sys.exit(0) config = ctx.obj.get("config") generator = GitignoreGenerator(config) for t in selected: generator.add_template(t) content = generator.generate() click.echo("") click.echo("Preview of generated .gitignore (first 50 lines):") click.echo("-" * 50) for idx, line in enumerate(content.split("\n")[:50]): click.echo(line) if len(content.split("\n")) > 50: click.echo(f"... and {len(content.split('\\n')) - 50} more lines") click.echo("-" * 50) if click.confirm("Write to .gitignore?"): output_path = Path(".gitignore") mode = "a" if output_path.exists() else "w" try: with open(output_path, mode) as f: f.write(content) click.echo(f"Successfully wrote to {output_path}") except PermissionError: click.echo(f"Permission denied: {output_path}", err=True) sys.exit(1) except OSError as e: click.echo(f"Error writing to {output_path}: {e}", err=True) sys.exit(1) elif click.confirm("Output to stdout instead?"): click.echo(content)