Files
KNELCloudronPackages/scripts/new_app.py

127 lines
4.4 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import datetime
import json
import os
import pathlib
import shutil
from typing import Dict, List
ROOT = pathlib.Path(__file__).resolve().parents[1]
def load_catalog(path: pathlib.Path) -> List[Dict[str, object]]:
with path.open("r", encoding="utf-8") as handle:
return json.load(handle)
def build_app_id(slug: str) -> str:
sanitized = ''.join(ch.lower() if ch.isalnum() else '.' for ch in slug)
sanitized = '.'.join(filter(None, sanitized.split('.')))
return f"com.knel.{sanitized}"
def default_tags() -> List[str]:
return ["custom", "known-element"]
def default_placeholder_map(entry: Dict[str, object]) -> Dict[str, str]:
slug = entry["slug"]
title = entry["title"]
repo = entry["repo"]
issue = entry.get("issue", "")
website = entry.get("website") or repo.rstrip(".git")
description = entry.get("description") or "TODO: Add package description."
tags = entry.get("tags") or default_tags()
http_port = str(entry.get("httpPort", 3000))
base_image = entry.get("baseImage", "cloudron/base:5.0.0")
placeholder_map = {
"{{APP_ID}}": build_app_id(slug),
"{{APP_TITLE}}": title,
"{{APP_SLUG}}": slug,
"{{APP_REPO}}": repo,
"{{APP_ISSUE}}": issue,
"{{APP_WEBSITE}}": website,
"{{APP_DESCRIPTION}}": description,
"{{HTTP_PORT}}": http_port,
"{{BASE_IMAGE}}": base_image,
"{{APP_TAGS}}": json.dumps(tags)
}
return placeholder_map
def render_templates(template_dir: pathlib.Path, destination: pathlib.Path, entry: Dict[str, object]) -> None:
shutil.copytree(template_dir, destination, dirs_exist_ok=True)
placeholders = default_placeholder_map(entry)
for path in destination.rglob('*'):
if path.is_dir():
continue
text = path.read_text(encoding='utf-8')
for key, value in placeholders.items():
text = text.replace(key, value)
path.write_text(text, encoding='utf-8')
# Ensure critical scripts are executable
for relpath in ["start.sh", "test/smoke.sh"]:
target = destination / relpath
if target.exists():
mode = target.stat().st_mode
target.chmod(mode | 0o111)
# Drop metadata file for bookkeeping
metadata = {
"slug": entry["slug"],
"title": entry["title"],
"issue": entry.get("issue"),
"repo": entry.get("repo"),
"additionalRepos": entry.get("additionalRepos", []),
"created": datetime.datetime.utcnow().replace(microsecond=0).isoformat() + "Z",
"notes": entry.get("notes", "TODO: capture packaging notes")
}
with (destination / "metadata.json").open("w", encoding="utf-8") as handle:
json.dump(metadata, handle, indent=2)
def ensure_app_directory(entry: Dict[str, object], template_dir: pathlib.Path, apps_dir: pathlib.Path, force: bool) -> pathlib.Path:
app_dir = apps_dir / entry["slug"]
if app_dir.exists():
if not force:
raise FileExistsError(f"Destination {app_dir} already exists. Use --force to overwrite.")
shutil.rmtree(app_dir)
render_templates(template_dir, app_dir, entry)
return app_dir
def main() -> None:
parser = argparse.ArgumentParser(description="Scaffold Cloudron app packages from catalog entries")
parser.add_argument("--slug", help="Generate scaffold for a single slug", default=None)
parser.add_argument("--catalog", default=str(ROOT / "apps" / "catalog.json"), help="Path to catalog JSON")
parser.add_argument("--template", default=str(ROOT / "templates" / "cloudron-app"), help="Template directory")
parser.add_argument("--apps-dir", default=str(ROOT / "apps"), help="Destination apps directory")
parser.add_argument("--force", action="store_true", help="Overwrite existing app directory")
args = parser.parse_args()
catalog = load_catalog(pathlib.Path(args.catalog))
entries = catalog
if args.slug:
entries = [entry for entry in catalog if entry["slug"] == args.slug]
if not entries:
raise SystemExit(f"Slug {args.slug} not found in catalog")
template_dir = pathlib.Path(args.template)
apps_dir = pathlib.Path(args.apps_dir)
apps_dir.mkdir(parents=True, exist_ok=True)
for entry in entries:
app_dir = ensure_app_directory(entry, template_dir, apps_dir, args.force)
print(f"Created {app_dir.relative_to(ROOT)}")
if __name__ == "__main__":
main()