Add source code files (detectors, generators, CLI)
This commit is contained in:
268
src/cli.py
Normal file
268
src/cli.py
Normal 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"],
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user