Implements jeaddons CLI tool

Adds a CLI tool to copy files or folders from a git repository into the current project based on a registry.

The tool provides commands to:
- `add`: Copies files/folders based on registry entries.
- `list`: Lists available registry entries.

Registry entries are defined in `jeAddons/registry.py`.
The tool supports overriding the registry's ref and destination.
This commit is contained in:
2026-02-11 22:18:38 +01:00
parent 24d8c9c8af
commit d5f1dd6366
12 changed files with 208 additions and 28 deletions
+5
View File
@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="uv (jeAddons)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="uv (jeAddons)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="uv (jeAddons)" project-jdk-type="Python SDK" />
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/jeAddons.iml" filepath="$PROJECT_DIR$/.idea/jeAddons.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+37
View File
@@ -1,2 +1,39 @@
# jeAddons # 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 <name> [--ref <branch-or-tag>] [--dest <target-dir>] [--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",
},
}
```
+57 -11
View File
@@ -1,23 +1,69 @@
import typer import typer
from pathlib import Path from pathlib import Path
from jeAddons.registry import load_registry from jeAddons.copier import fetch_path
from jeAddons.copier import fetch_file 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() @app.command()
def add(name: str): def add(
registry = load_registry() name: str = typer.Argument(..., help="Registry name from jeAddons/registry.py."),
ref: str | None = typer.Option(None, "--ref", "-r", help="Override registry git ref."),
if name not in registry: dest: Path | None = typer.Option(None, "--dest", "-d", help="Override registry destination directory."),
typer.echo(f"❌ Unknown entry: {name}") 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) raise typer.Exit(1)
entry = registry[name] repo = entry.get("repo")
fetch_file(entry["repo"], entry["file"], Path.cwd()) 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", "<missing repo>")
repo_path = entry.get("path", "<missing 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__": if __name__ == "__main__":
+26 -7
View File
@@ -4,13 +4,32 @@ import shutil
from pathlib import Path from pathlib import Path
def fetch_file(repo_url: str, file_path: str, dest: Path): def fetch_path(repo_url: str, repo_path: str, dest: Path, ref: str = "HEAD", force: bool = False):
tmp = tempfile.mkdtemp() 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
src = Path(tmp) / file_path
if not src.exists(): if not src.exists():
raise FileNotFoundError(file_path) raise FileNotFoundError(repo_path)
shutil.copy2(src, dest / src.name) target = dest / src.name
if target.exists():
if not force:
raise FileExistsError(target)
if target.is_dir():
shutil.rmtree(target)
else:
target.unlink()
if src.is_dir():
shutil.copytree(src, target)
return target
shutil.copy2(src, target)
return target
+35 -7
View File
@@ -1,9 +1,37 @@
import json from typing import TypedDict
import urllib.request
REGISTRY_URL = "https://raw.githubusercontent.com/acme/registry/main/registry.json"
def load_registry(): class RegistryEntry(TypedDict, total=False):
with urllib.request.urlopen(REGISTRY_URL) as r: repo: str
return json.load(r) path: str
ref: str
dest: str
# Manage your repositories, files, and folders here.
# Key = addon name you pass to `jeaddons add <name>`.
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]
+9 -1
View File
@@ -1,12 +1,20 @@
[project] [project]
name = "jeaddons" name = "jeaddons"
version = "0.1.0" 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" readme = "README.md"
requires-python = ">=3.14" requires-python = ">=3.14"
dependencies = [ dependencies = [
"typer>=0.21.1", "typer>=0.21.1",
] ]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["jeAddons"]
[project.scripts] [project.scripts]
jeAddons= "jeAddons.cli:app" jeAddons= "jeAddons.cli:app"
jeaddons = "jeAddons.cli:app"
Generated
+1 -1
View File
@@ -26,7 +26,7 @@ wheels = [
[[package]] [[package]]
name = "jeaddons" name = "jeaddons"
version = "0.1.0" version = "0.1.0"
source = { virtual = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "typer" }, { name = "typer" },
] ]