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" },
]