Add source code files (detectors, generators, CLI)

This commit is contained in:
2026-02-01 20:15:27 +00:00
parent 0327262991
commit 495ad92501

268
src/cli.py Normal file
View File

@@ -0,0 +1,268 @@
"""CLI interface for auto-gitignore."""
import sys
from pathlib import Path
from typing import Optional, Tuple
import click
from src.detectors.framework import FrameworkDetector
from src.detectors.ide import IDEDetector
from src.detectors.language import LanguageDetector
from src.generators.merge import PatternMerger
from src.generators.pattern import PatternGenerator
LANGUAGE_CHOICES = [
"python", "nodejs", "go", "java", "rust", "csharp", "cpp",
"ruby", "php", "dart", "swift", "kotlin", "scala", "perl",
"r", "elixir", "clojure", "lua", "haskell",
]
FRAMEWORK_CHOICES = [
"django", "flask", "fastapi", "react", "vue", "angular",
"express", "nextjs", "nuxt", "svelte", "gatsby", "astro",
"gin", "spring", "rails", "laravel", "dotnet", "quasar",
"sveltekit", "remix", "vite", "nestjs",
]
IDE_CHOICES = [
"vscode", "jetbrains", "pycharm", "webstorm", "intellij",
"eclipse", "netbeans", "sublime", "vim", "emacs", "atom", "spacemacs",
]
OS_CHOICES = ["macos", "windows", "linux"]
@click.command()
@click.option(
"--path",
"-p",
type=click.Path(exists=True, file_okay=False, dir_okay=True),
default=".",
help="Path to project directory (default: current directory)",
)
@click.option(
"--language",
"-l",
multiple=True,
type=click.Choice(LANGUAGE_CHOICES),
help="Force specific language(s)",
)
@click.option(
"--framework",
"-f",
multiple=True,
type=click.Choice(FRAMEWORK_CHOICES),
help="Add framework-specific patterns",
)
@click.option(
"--ide",
multiple=True,
type=click.Choice(IDE_CHOICES),
help="Add IDE-specific patterns",
)
@click.option(
"--os",
multiple=True,
type=click.Choice(OS_CHOICES),
help="Add OS-specific patterns",
)
@click.option(
"--custom",
"-c",
multiple=True,
help="Add custom pattern(s)",
)
@click.option(
"--output",
"-o",
type=click.Path(dir_okay=False),
help="Output file path (default: .gitignore in project path)",
)
@click.option(
"--merge/--no-merge",
default=True,
help="Merge patterns from multiple sources",
)
@click.option(
"--dry-run",
is_flag=True,
default=False,
help="Show generated content without writing file",
)
@click.option(
"--force",
is_flag=True,
default=False,
help="Overwrite existing .gitignore file",
)
@click.option(
"--verbose",
"-v",
is_flag=True,
default=False,
help="Show detected stack and other details",
)
def main(
path: str,
language: Tuple[str, ...],
framework: Tuple[str, ...],
ide: Tuple[str, ...],
os: Tuple[str, ...],
custom: Tuple[str, ...],
output: Optional[str],
merge: bool,
dry_run: bool,
force: bool,
verbose: bool,
) -> None:
"""Auto-generate .gitignore files based on project type and stack."""
project_path = Path(path).resolve()
if output is None:
output_path = project_path / ".gitignore"
else:
output_path = Path(output)
languages = list(language)
frameworks = list(framework)
ides = list(ide)
os_patterns_list = list(os)
custom_patterns = list(custom) if custom else []
if not languages or not frameworks:
detected = detect_project_stack(project_path)
if not languages:
languages = detected.get("languages", [])
if not frameworks:
frameworks = detected.get("frameworks", [])
if not ides:
ides = detected.get("ides", [])
if not os_patterns_list:
os_patterns_list = detected.get("os", ["macos", "windows", "linux"])
if verbose:
click.echo(f"Project path: {project_path}")
if languages:
click.echo(f"Detected languages: {', '.join(languages)}")
if frameworks:
click.echo(f"Detected frameworks: {', '.join(frameworks)}")
if ides:
click.echo(f"Detected IDEs: {', '.join(ides)}")
click.echo("")
generator = PatternGenerator()
merger = PatternMerger()
if merge and (languages or frameworks or ides):
patterns_by_category: dict = {}
if languages:
patterns_by_category["language"] = []
for lang in languages:
patterns_by_category["language"].extend(
generator.language_patterns.get(lang, [])
)
if frameworks:
patterns_by_category["framework"] = []
for fw in frameworks:
patterns_by_category["framework"].extend(
generator.framework_patterns.get(fw, [])
)
if ides:
patterns_by_category["ide"] = []
for ide_name in ides:
patterns_by_category["ide"].extend(
generator.ide_patterns.get(ide_name, [])
)
if os_patterns_list:
patterns_by_category["os"] = []
for os_name in os_patterns_list:
patterns_by_category["os"].extend(
generator.os_patterns.get(os_name, [])
)
merged_patterns, conflicts = merger.merge(patterns_by_category, custom_patterns)
if conflicts and verbose:
click.echo("Conflicts resolved:", err=True)
for conflict in conflicts:
click.echo(f" - {conflict}", err=True)
content = "\n".join(merged_patterns)
else:
content = generator.generate(
languages=languages,
frameworks=frameworks,
ides=ides,
os_list=list(os_patterns_list),
custom_patterns=custom_patterns,
)
if not content.strip():
content = "# No patterns generated. Use --language, --framework, or --custom to add patterns."
if dry_run:
click.echo(content)
return
if output_path.exists() and not force:
click.echo(f"Error: {output_path} already exists. Use --force to overwrite.", err=True)
sys.exit(1)
try:
output_path.write_text(content)
click.echo(f"Generated .gitignore at {output_path}")
if verbose:
line_count = len(content.splitlines())
click.echo(f"Total lines: {line_count}")
except PermissionError:
click.echo(f"Error: Permission denied writing to {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)
def detect_project_stack(path: Path) -> dict:
"""Detect project languages, frameworks, and IDEs.
Args:
path: Project root path.
Returns:
Dict with detected languages, frameworks, and ides.
"""
language_detector = LanguageDetector()
framework_detector = FrameworkDetector()
ide_detector = IDEDetector()
languages = language_detector.detect(path)
frameworks = framework_detector.detect(path)
frameworks_from_content = framework_detector.detect_from_content(path)
for fw in frameworks_from_content:
if fw not in frameworks:
frameworks.append(fw)
package_frameworks = framework_detector.detect_package_json_frameworks(path)
for fw in package_frameworks:
if fw not in frameworks:
frameworks.append(fw)
ides = ide_detector.detect(path)
if not ides:
if ide_detector.detect_vscode(path):
ides.append("vscode")
if ide_detector.detect_jetbrains(path):
if "pycharm" not in ides:
ides.append("jetbrains")
return {
"languages": languages,
"frameworks": frameworks,
"ides": ides,
"os": ["macos", "windows", "linux"],
}