diff --git a/src/cli.py b/src/cli.py new file mode 100644 index 0000000..e122dd6 --- /dev/null +++ b/src/cli.py @@ -0,0 +1,401 @@ +"""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)