feat: Replace cookiecutter with built-in MiniJinja templating

- Add MiniJinja dependency for Jinja2-compatible templating
- Create new project_template.rs module for template rendering
- Bundle snowflake template in templates/snowflake/ directory
- Remove dependency on external cookiecutter Python tool
- Handle Gitea publishing natively in Rust instead of Python hooks
- Update default template path to use bundled template with fallback
- Update README to reflect new built-in templating
- All tests pass

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-09-13 08:37:18 -05:00
parent 981cd853b9
commit caa0001922
41 changed files with 789 additions and 53 deletions
@@ -0,0 +1,78 @@
"""Post-generation hook: optionally publish the new project to Gitea.
Cookiecutter renders this file as a Jinja template before running it, so a
doubled opening brace anywhere outside a cookiecutter variable is read as the
start of a Jinja expression and breaks the render. The JSON body below is
therefore built with ``json.dumps`` rather than written out with literal braces.
"""
import json
import os
import subprocess
import sys
# This one comes from cookiecutter.json.
PROJECT_NAME = "{{cookiecutter.project_name}}"
GITEA_URL = os.environ.get("GITEA_URL") # e.g. https://gitea.example.com
GITEA_USER = os.environ.get("GITEA_USER")
GITEA_TOKEN = os.environ.get("GITEA_TOKEN")
def run(cmd, **kwargs):
"""Run a command given as an argument list, raising if it fails."""
return subprocess.run(cmd, check=True, **kwargs)
def create_gitea_repo():
# 1. Create the repo via the Gitea API. `--fail` makes curl exit non-zero on
# an HTTP error, so a bad token surfaces here instead of silently
# "succeeding" and leaving a push to fail later.
api_url = f"{GITEA_URL.rstrip('/')}/api/v1/user/repos"
payload = json.dumps({"name": PROJECT_NAME, "private": False, "auto_init": False})
run([
"curl", "-sS", "--fail", "-X", "POST", api_url,
"-H", "Content-Type: application/json",
"-H", f"Authorization: token {GITEA_TOKEN}",
"-d", payload,
])
# 2. Init git and push. `-b main` matters because the push below names
# `main`, while git's built-in default is still `master`.
repo_url = f"{GITEA_URL.rstrip('/')}/{GITEA_USER}/{PROJECT_NAME}.git"
auth_url = repo_url.replace("://", f"://{GITEA_USER}:{GITEA_TOKEN}@", 1)
run(["git", "init", "-b", "main"])
run(["git", "add", "."])
run(["git", "commit", "-m", "Initial commit from cookiecutter"])
# The stored remote keeps the plain URL: embedding the token would write it
# in cleartext into .git/config for the life of the project. It is passed to
# this one push only.
run(["git", "remote", "add", "origin", repo_url])
run(["git", "push", auth_url, "main"])
run(["git", "fetch", "origin"])
run(["git", "branch", "--set-upstream-to=origin/main", "main"])
print(f"✅ Repo created and pushed: {repo_url}")
if __name__ == "__main__":
missing = [
name
for name, value in (
("GITEA_URL", GITEA_URL),
("GITEA_USER", GITEA_USER),
("GITEA_TOKEN", GITEA_TOKEN),
)
if not value
]
if missing:
# Publishing is opt-in. Without credentials the project is still
# perfectly good on disk, so skip rather than fail the generation and
# have cookiecutter throw the whole tree away.
print(f"ℹ Skipping Gitea publish; not set: {', '.join(missing)}")
sys.exit(0)
try:
create_gitea_repo()
except subprocess.CalledProcessError as e:
print(f"❌ Error: {e}", file=sys.stderr)
sys.exit(1)