This commit is contained in:
401
src/cli.py
Normal file
401
src/cli.py
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user