diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/jeAddons.iml b/.idea/jeAddons.iml new file mode 100644 index 0000000..a6da54f --- /dev/null +++ b/.idea/jeAddons.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..9d480d2 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..1eaac31 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index ce0fc70..1d30be3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,39 @@ # jeAddons +Clone a file or folder from Git repositories into your current project. + +`add` is registry-based (like `shadcn add`): you manage entries in a dict. + +## Usage with uvx + +1. Edit `jeAddons/registry.py` and add entries to `REGISTRY`. +2. Run: + +```bash +uvx jeaddons add [--ref ] [--dest ] [--force] +``` + +3. See available names: + +```bash +uvx jeaddons list +``` + +## Registry example + +```python +REGISTRY = { + "button": { + "repo": "https://github.com/user/templates.git", + "path": "src/components/Button.tsx", + "ref": "main", + "dest": "src/components", + }, + "ui": { + "repo": "https://github.com/user/templates.git", + "path": "src/components/ui", + "ref": "v1.2.0", + "dest": "src/components", + }, +} +``` diff --git a/jeAddons/cli.py b/jeAddons/cli.py index 0c83fa3..8f58464 100644 --- a/jeAddons/cli.py +++ b/jeAddons/cli.py @@ -1,23 +1,69 @@ import typer from pathlib import Path -from jeAddons.registry import load_registry -from jeAddons.copier import fetch_file +from jeAddons.copier import fetch_path +from jeAddons.registry import load_registry, get_registry_entry -app = typer.Typer() +app = typer.Typer(no_args_is_help=True) + + +@app.callback() +def main(): + """Copy files or folders from a git repository into your current project.""" + return @app.command() -def add(name: str): - registry = load_registry() - - if name not in registry: - typer.echo(f"❌ Unknown entry: {name}") +def add( + name: str = typer.Argument(..., help="Registry name from jeAddons/registry.py."), + ref: str | None = typer.Option(None, "--ref", "-r", help="Override registry git ref."), + dest: Path | None = typer.Option(None, "--dest", "-d", help="Override registry destination directory."), + force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing file/folder with same name."), +): + try: + entry = get_registry_entry(name) + except KeyError: + typer.echo(f"Unknown registry entry: '{name}'.") + typer.echo("Run `jeaddons list` to show available entries.") raise typer.Exit(1) - entry = registry[name] - fetch_file(entry["repo"], entry["file"], Path.cwd()) + repo = entry.get("repo") + repo_path = entry.get("path") + if not repo or not repo_path: + typer.echo(f"Registry entry '{name}' is missing required keys: 'repo' and/or 'path'.") + raise typer.Exit(1) - typer.echo(f"✅ Added {entry['file']}") + effective_ref = ref or entry.get("ref", "HEAD") + effective_dest = dest or Path.cwd() / entry.get("dest", ".") + + try: + target = fetch_path(repo, repo_path, effective_dest, ref=effective_ref, force=force) + except FileNotFoundError: + typer.echo(f"Path '{repo_path}' was not found in repository '{repo}'.") + raise typer.Exit(1) + except FileExistsError as exc: + typer.echo(f"Target already exists: {exc}. Use --force to overwrite.") + raise typer.Exit(1) + except Exception as exc: + typer.echo(f"Failed to copy from repository: {exc}") + raise typer.Exit(1) + + typer.echo(f"Added: {target}") + + +@app.command("list") +def list_entries(): + """List available registry entries.""" + registry = load_registry() + if not registry: + typer.echo("Registry is empty. Add entries in jeAddons/registry.py") + return + + for name, entry in registry.items(): + repo = entry.get("repo", "") + repo_path = entry.get("path", "") + ref = entry.get("ref", "HEAD") + dest = entry.get("dest", ".") + typer.echo(f"{name}: repo={repo} path={repo_path} ref={ref} dest={dest}") if __name__ == "__main__": diff --git a/jeAddons/copier.py b/jeAddons/copier.py index 3d4b647..88d743f 100644 --- a/jeAddons/copier.py +++ b/jeAddons/copier.py @@ -4,13 +4,32 @@ import shutil from pathlib import Path -def fetch_file(repo_url: str, file_path: str, dest: Path): - tmp = tempfile.mkdtemp() +def fetch_path(repo_url: str, repo_path: str, dest: Path, ref: str = "HEAD", force: bool = False): + with tempfile.TemporaryDirectory(prefix="jeaddons-") as tmp: + tmp_path = Path(tmp) + subprocess.check_call( + ["git", "clone", "--filter=blob:none", "--no-checkout", "--depth", "1", repo_url, str(tmp_path)] + ) + subprocess.check_call(["git", "-C", str(tmp_path), "sparse-checkout", "init", "--cone"]) + subprocess.check_call(["git", "-C", str(tmp_path), "sparse-checkout", "set", repo_path]) + subprocess.check_call(["git", "-C", str(tmp_path), "checkout", ref]) - subprocess.check_call(["git", "clone", "--depth", "1", repo_url, tmp]) + src = tmp_path / repo_path + if not src.exists(): + raise FileNotFoundError(repo_path) - src = Path(tmp) / file_path - if not src.exists(): - raise FileNotFoundError(file_path) + target = dest / src.name + if target.exists(): + if not force: + raise FileExistsError(target) + if target.is_dir(): + shutil.rmtree(target) + else: + target.unlink() - shutil.copy2(src, dest / src.name) + if src.is_dir(): + shutil.copytree(src, target) + return target + + shutil.copy2(src, target) + return target diff --git a/jeAddons/registry.py b/jeAddons/registry.py index 7cf5d78..6f5539f 100644 --- a/jeAddons/registry.py +++ b/jeAddons/registry.py @@ -1,9 +1,37 @@ -import json -import urllib.request - -REGISTRY_URL = "https://raw.githubusercontent.com/acme/registry/main/registry.json" +from typing import TypedDict -def load_registry(): - with urllib.request.urlopen(REGISTRY_URL) as r: - return json.load(r) +class RegistryEntry(TypedDict, total=False): + repo: str + path: str + ref: str + dest: str + + +# Manage your repositories, files, and folders here. +# Key = addon name you pass to `jeaddons add `. +REGISTRY: dict[str, RegistryEntry] = { + # "button": { + # "repo": "https://github.com/your-org/templates.git", + # "path": "src/components/Button.tsx", + # "ref": "main", + # "dest": "src/components", + # }, + # "ui": { + # "repo": "https://github.com/your-org/templates.git", + # "path": "src/components/ui", + # "ref": "v1.2.0", + # "dest": "src/components", + # }, +} + + +def load_registry() -> dict[str, RegistryEntry]: + return REGISTRY + + +def get_registry_entry(name: str) -> RegistryEntry: + registry = load_registry() + if name not in registry: + raise KeyError(name) + return registry[name] diff --git a/pyproject.toml b/pyproject.toml index 087836c..bb70e38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,20 @@ [project] name = "jeaddons" version = "0.1.0" -description = "Add your description here" +description = "CLI to copy files or folders from a git repo into your current project" readme = "README.md" requires-python = ">=3.14" dependencies = [ "typer>=0.21.1", ] +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["jeAddons"] + [project.scripts] -jeAddons= "jeAddons.cli:app" \ No newline at end of file +jeAddons= "jeAddons.cli:app" +jeaddons = "jeAddons.cli:app" diff --git a/uv.lock b/uv.lock index 1303330..1440ceb 100644 --- a/uv.lock +++ b/uv.lock @@ -26,7 +26,7 @@ wheels = [ [[package]] name = "jeaddons" version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "typer" }, ]