From ec699368e939dc7b388987ee437b02bf0ab1c011 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sat, 17 Oct 2026 11:56:09 +0000
Subject: [PATCH] feat: scaffold OpenROM Plugin Store repository architecture
- Create WinGet-style hierarchical manifests directory structure
- Add publisher seed profile (publishers/m5dev.json) and seed launch manifests for Xbox 360, Wii U, and Saturn plugins
- Implement validation script (scripts/validate_plugin.py) and catalog compiler (scripts/build_catalog.py)
- Build responsive cyberpunk/retro storefront UI in public/ index.html and styles.css with 1-click install links and verified publisher badges
- Add Cloudflare D1 SQL schema in schema.sql
- Add GitHub Actions CI workflow for validation and catalog generation (.github/workflows/build_catalog.yml)
Co-authored-by: ClausValcaTD <193546948+ClausValcaTD@users.noreply.github.com>
---
.github/workflows/build_catalog.yml | 42 ++
.../m/m5dev/saturn-ssp/1.0.0/manifest.json | 20 +
.../m/m5dev/wiiu-nkit/3.1.0/manifest.json | 22 +
.../m/m5dev/xbox360-tools/1.0.0/manifest.json | 21 +
packages/.gitkeep | 0
public/api/catalog.json | 91 ++++
public/index.html | 201 +++++++++
public/styles.css | 387 ++++++++++++++++++
publishers/m5dev.json | 7 +
schema.sql | 36 ++
scripts/build_catalog.py | 75 ++++
scripts/validate_plugin.py | 94 +++++
12 files changed, 996 insertions(+)
create mode 100644 .github/workflows/build_catalog.yml
create mode 100644 manifests/m/m5dev/saturn-ssp/1.0.0/manifest.json
create mode 100644 manifests/m/m5dev/wiiu-nkit/3.1.0/manifest.json
create mode 100644 manifests/m/m5dev/xbox360-tools/1.0.0/manifest.json
create mode 100644 packages/.gitkeep
create mode 100644 public/api/catalog.json
create mode 100644 public/index.html
create mode 100644 public/styles.css
create mode 100644 publishers/m5dev.json
create mode 100644 schema.sql
create mode 100644 scripts/build_catalog.py
create mode 100644 scripts/validate_plugin.py
diff --git a/.github/workflows/build_catalog.yml b/.github/workflows/build_catalog.yml
new file mode 100644
index 0000000..ed652e0
--- /dev/null
+++ b/.github/workflows/build_catalog.yml
@@ -0,0 +1,42 @@
+name: Build & Validate OpenROM Catalog
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+permissions:
+ contents: write
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Validate Plugin Manifests
+ run: python scripts/validate_plugin.py
+
+ - name: Build Catalog API
+ run: python scripts/build_catalog.py
+
+ - name: Commit and Push Catalog Changes
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ run: |
+ git config --global user.name "github-actions[bot]"
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git add public/api/catalog.json
+ if git diff --staged --quiet; then
+ echo "No catalog changes to commit."
+ else
+ git commit -m "build: update catalog.json [skip ci]"
+ git push
+ fi
diff --git a/manifests/m/m5dev/saturn-ssp/1.0.0/manifest.json b/manifests/m/m5dev/saturn-ssp/1.0.0/manifest.json
new file mode 100644
index 0000000..a49487f
--- /dev/null
+++ b/manifests/m/m5dev/saturn-ssp/1.0.0/manifest.json
@@ -0,0 +1,20 @@
+{
+ "id": "saturn-ssp",
+ "name": "Sega Saturn Patcher (SSP)",
+ "version": "1.0.0",
+ "author": "m5dev",
+ "description": "Specialized Sega Saturn Patcher disc modification tool.",
+ "conversions": [
+ "SSP"
+ ],
+ "tags": [
+ "saturn",
+ "ssp",
+ "patcher",
+ "sega"
+ ],
+ "icon": "https://raw.githubusercontent.com/M5Devs/OpenROM-Plugins-Store/main/packages/saturn.png",
+ "download_url": "https://github.com/M5Devs/OpenROM-Plugins-Store/releases/download/v1.0.0/saturn-ssp.orplug",
+ "homepage": "https://github.com/M5Devs",
+ "license": "MIT"
+}
diff --git a/manifests/m/m5dev/wiiu-nkit/3.1.0/manifest.json b/manifests/m/m5dev/wiiu-nkit/3.1.0/manifest.json
new file mode 100644
index 0000000..26254f1
--- /dev/null
+++ b/manifests/m/m5dev/wiiu-nkit/3.1.0/manifest.json
@@ -0,0 +1,22 @@
+{
+ "id": "wiiu-nkit",
+ "name": "Wii U NKit & ZArchive Suite",
+ "version": "3.1.0",
+ "author": "m5dev",
+ "description": "Standalone NKit v3.1.0 suite for Wii U ZArchive compression.",
+ "conversions": [
+ "WUA",
+ "WUD",
+ "WUX"
+ ],
+ "tags": [
+ "wiiu",
+ "nkit",
+ "zarchive",
+ "wua"
+ ],
+ "icon": "https://raw.githubusercontent.com/M5Devs/OpenROM-Plugins-Store/main/packages/wiiu.png",
+ "download_url": "https://github.com/M5Devs/OpenROM-Plugins-Store/releases/download/v3.1.0/wiiu-nkit.orplug",
+ "homepage": "https://github.com/M5Devs",
+ "license": "GPL-3.0"
+}
diff --git a/manifests/m/m5dev/xbox360-tools/1.0.0/manifest.json b/manifests/m/m5dev/xbox360-tools/1.0.0/manifest.json
new file mode 100644
index 0000000..b639db0
--- /dev/null
+++ b/manifests/m/m5dev/xbox360-tools/1.0.0/manifest.json
@@ -0,0 +1,21 @@
+{
+ "id": "xbox360-tools",
+ "name": "Xbox 360 ZAR & XISO Tools",
+ "version": "1.0.0",
+ "author": "m5dev",
+ "description": "Extract XISO and compress to .zar for Xenia Edge.",
+ "conversions": [
+ "ZAR",
+ "XISO"
+ ],
+ "tags": [
+ "xbox360",
+ "xiso",
+ "zar",
+ "xenia"
+ ],
+ "icon": "https://raw.githubusercontent.com/M5Devs/OpenROM-Plugins-Store/main/packages/xbox360.png",
+ "download_url": "https://github.com/M5Devs/OpenROM-Plugins-Store/releases/download/v1.0.0/xbox360-tools.orplug",
+ "homepage": "https://github.com/M5Devs",
+ "license": "MIT"
+}
diff --git a/packages/.gitkeep b/packages/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/public/api/catalog.json b/public/api/catalog.json
new file mode 100644
index 0000000..e810b32
--- /dev/null
+++ b/public/api/catalog.json
@@ -0,0 +1,91 @@
+{
+ "version": "1.0",
+ "generated_at": "2026-10-17T11:54:08.867684+00:00",
+ "total_plugins": 3,
+ "plugins": [
+ {
+ "id": "saturn-ssp",
+ "name": "Sega Saturn Patcher (SSP)",
+ "version": "1.0.0",
+ "author": "m5dev",
+ "description": "Specialized Sega Saturn Patcher disc modification tool.",
+ "conversions": [
+ "SSP"
+ ],
+ "tags": [
+ "saturn",
+ "ssp",
+ "patcher",
+ "sega"
+ ],
+ "icon": "https://raw.githubusercontent.com/M5Devs/OpenROM-Plugins-Store/main/packages/saturn.png",
+ "download_url": "https://github.com/M5Devs/OpenROM-Plugins-Store/releases/download/v1.0.0/saturn-ssp.orplug",
+ "homepage": "https://github.com/M5Devs",
+ "license": "MIT",
+ "publisher": {
+ "id": "m5dev",
+ "name": "Yusuf Ezz (M5 Dev)",
+ "github": "https://github.com/M5Devs",
+ "is_verified": true,
+ "badge": "Core Architect"
+ }
+ },
+ {
+ "id": "wiiu-nkit",
+ "name": "Wii U NKit & ZArchive Suite",
+ "version": "3.1.0",
+ "author": "m5dev",
+ "description": "Standalone NKit v3.1.0 suite for Wii U ZArchive compression.",
+ "conversions": [
+ "WUA",
+ "WUD",
+ "WUX"
+ ],
+ "tags": [
+ "wiiu",
+ "nkit",
+ "zarchive",
+ "wua"
+ ],
+ "icon": "https://raw.githubusercontent.com/M5Devs/OpenROM-Plugins-Store/main/packages/wiiu.png",
+ "download_url": "https://github.com/M5Devs/OpenROM-Plugins-Store/releases/download/v3.1.0/wiiu-nkit.orplug",
+ "homepage": "https://github.com/M5Devs",
+ "license": "GPL-3.0",
+ "publisher": {
+ "id": "m5dev",
+ "name": "Yusuf Ezz (M5 Dev)",
+ "github": "https://github.com/M5Devs",
+ "is_verified": true,
+ "badge": "Core Architect"
+ }
+ },
+ {
+ "id": "xbox360-tools",
+ "name": "Xbox 360 ZAR & XISO Tools",
+ "version": "1.0.0",
+ "author": "m5dev",
+ "description": "Extract XISO and compress to .zar for Xenia Edge.",
+ "conversions": [
+ "ZAR",
+ "XISO"
+ ],
+ "tags": [
+ "xbox360",
+ "xiso",
+ "zar",
+ "xenia"
+ ],
+ "icon": "https://raw.githubusercontent.com/M5Devs/OpenROM-Plugins-Store/main/packages/xbox360.png",
+ "download_url": "https://github.com/M5Devs/OpenROM-Plugins-Store/releases/download/v1.0.0/xbox360-tools.orplug",
+ "homepage": "https://github.com/M5Devs",
+ "license": "MIT",
+ "publisher": {
+ "id": "m5dev",
+ "name": "Yusuf Ezz (M5 Dev)",
+ "github": "https://github.com/M5Devs",
+ "is_verified": true,
+ "badge": "Core Architect"
+ }
+ }
+ ]
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..60497e1
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,201 @@
+
+
+
+
+
+ OpenROM Plugin Store
+
+
+
+
+
+
+
+
OpenROM Plugin Store
+
Discover, install, and compress retro ROM archives effortlessly. Community-driven conversion extensions for OpenROM ecosystem.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/styles.css b/public/styles.css
new file mode 100644
index 0000000..f986c17
--- /dev/null
+++ b/public/styles.css
@@ -0,0 +1,387 @@
+:root {
+ --bg-dark: #0b0e14;
+ --bg-card: #151a23;
+ --bg-card-hover: #1c2330;
+ --text-main: #e6edf3;
+ --text-muted: #8b949e;
+ --accent-cyan: #00f2ff;
+ --accent-purple: #9d4edd;
+ --accent-blue: #38bdf8;
+ --border-color: #30363d;
+ --badge-bg: rgba(0, 242, 255, 0.1);
+ --badge-border: rgba(0, 242, 255, 0.3);
+ --btn-green: #238636;
+ --btn-green-hover: #2ea043;
+ --btn-blue: #1f6feb;
+ --btn-blue-hover: #388bfd;
+ --font-mono: 'Fira Code', 'Courier New', monospace;
+ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ background-color: var(--bg-dark);
+ color: var(--text-main);
+ font-family: var(--font-sans);
+ line-height: 1.6;
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
+
+/* Header */
+header {
+ background-color: rgba(21, 26, 35, 0.8);
+ backdrop-filter: blur(10px);
+ border-bottom: 1px solid var(--border-color);
+ position: sticky;
+ top: 0;
+ z-index: 100;
+}
+
+.nav-container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 1rem 1.5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.logo {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ font-size: 1.25rem;
+ font-weight: 700;
+ color: #fff;
+ text-decoration: none;
+ font-family: var(--font-mono);
+ letter-spacing: -0.5px;
+}
+
+.logo-icon {
+ background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple));
+ color: #000;
+ width: 36px;
+ height: 36px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 900;
+ font-size: 1.2rem;
+ box-shadow: 0 0 15px rgba(0, 242, 255, 0.4);
+}
+
+.logo span {
+ color: var(--accent-cyan);
+}
+
+.nav-links {
+ display: flex;
+ gap: 1.5rem;
+}
+
+.nav-links a {
+ color: var(--text-muted);
+ text-decoration: none;
+ font-weight: 500;
+ font-size: 0.95rem;
+ transition: color 0.2s;
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+}
+
+.nav-links a:hover {
+ color: var(--accent-cyan);
+}
+
+/* Hero Section */
+.hero {
+ background: radial-gradient(circle at 50% 20%, rgba(157, 78, 221, 0.15), transparent 70%);
+ padding: 4rem 1.5rem 3rem;
+ text-align: center;
+ border-bottom: 1px solid rgba(48, 54, 61, 0.5);
+}
+
+.hero-content {
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+.hero h1 {
+ font-size: 2.75rem;
+ font-weight: 800;
+ margin-bottom: 1rem;
+ background: linear-gradient(90deg, #ffffff, var(--accent-cyan));
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ line-height: 1.2;
+}
+
+.hero p {
+ font-size: 1.15rem;
+ color: var(--text-muted);
+ margin-bottom: 2rem;
+}
+
+/* Search and Controls */
+.controls-container {
+ max-width: 1200px;
+ margin: -1.5rem auto 2rem;
+ padding: 0 1.5rem;
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+}
+
+.search-bar {
+ flex: 1;
+ position: relative;
+}
+
+.search-bar input {
+ width: 100%;
+ padding: 0.85rem 1.25rem 0.85rem 2.8rem;
+ background-color: var(--bg-card);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ color: var(--text-main);
+ font-size: 1rem;
+ outline: none;
+ transition: border-color 0.2s, box-shadow 0.2s;
+}
+
+.search-bar input:focus {
+ border-color: var(--accent-cyan);
+ box-shadow: 0 0 10px rgba(0, 242, 255, 0.2);
+}
+
+.search-icon {
+ position: absolute;
+ left: 1rem;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--text-muted);
+}
+
+/* Main Grid */
+main {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 1.5rem 4rem;
+ flex: 1;
+ width: 100%;
+}
+
+.plugin-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
+ gap: 1.5rem;
+}
+
+/* Plugin Card */
+.plugin-card {
+ background-color: var(--bg-card);
+ border: 1px solid var(--border-color);
+ border-radius: 12px;
+ padding: 1.5rem;
+ display: flex;
+ flex-direction: column;
+ transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s;
+ position: relative;
+ overflow: hidden;
+}
+
+.plugin-card:hover {
+ transform: translateY(-3px);
+ border-color: rgba(0, 242, 255, 0.4);
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
+}
+
+.card-header {
+ display: flex;
+ align-items: flex-start;
+ gap: 1rem;
+ margin-bottom: 1rem;
+}
+
+.plugin-icon {
+ width: 52px;
+ height: 52px;
+ border-radius: 10px;
+ background-color: #21262d;
+ object-fit: cover;
+ border: 1px solid var(--border-color);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: bold;
+ color: var(--accent-cyan);
+ font-size: 1.4rem;
+}
+
+.card-title-group {
+ flex: 1;
+}
+
+.plugin-title {
+ font-size: 1.15rem;
+ font-weight: 700;
+ color: #fff;
+ line-height: 1.3;
+}
+
+.plugin-version {
+ font-size: 0.8rem;
+ font-family: var(--font-mono);
+ color: var(--text-muted);
+ background-color: #21262d;
+ padding: 0.15rem 0.4rem;
+ border-radius: 4px;
+ display: inline-block;
+ margin-top: 0.25rem;
+}
+
+.publisher-info {
+ display: flex;
+ align-items: center;
+ gap: 0.35rem;
+ font-size: 0.85rem;
+ color: var(--text-muted);
+ margin-top: 0.4rem;
+}
+
+.verified-badge {
+ color: var(--accent-blue);
+ font-size: 0.9rem;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.1rem;
+}
+
+.badge-tag {
+ background-color: rgba(157, 78, 221, 0.15);
+ color: var(--accent-purple);
+ border: 1px solid rgba(157, 78, 221, 0.3);
+ font-size: 0.7rem;
+ padding: 0.1rem 0.4rem;
+ border-radius: 10px;
+ font-weight: 600;
+ margin-left: 0.2rem;
+}
+
+.plugin-description {
+ color: var(--text-muted);
+ font-size: 0.9rem;
+ margin-bottom: 1.25rem;
+ flex: 1;
+}
+
+.tag-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.4rem;
+ margin-bottom: 1.25rem;
+}
+
+.tag {
+ background-color: var(--badge-bg);
+ color: var(--accent-cyan);
+ border: 1px solid var(--badge-border);
+ font-size: 0.75rem;
+ font-family: var(--font-mono);
+ padding: 0.2rem 0.5rem;
+ border-radius: 6px;
+}
+
+.conversions-label {
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-bottom: 0.3rem;
+ font-weight: 600;
+}
+
+.card-actions {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 0.75rem;
+ margin-top: auto;
+}
+
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.4rem;
+ padding: 0.6rem 0.8rem;
+ border-radius: 6px;
+ font-weight: 600;
+ font-size: 0.85rem;
+ text-decoration: none;
+ cursor: pointer;
+ transition: background-color 0.2s, border-color 0.2s, transform 0.1s;
+ border: none;
+}
+
+.btn:active {
+ transform: scale(0.98);
+}
+
+.btn-install {
+ background-color: var(--btn-green);
+ color: #fff;
+}
+
+.btn-install:hover {
+ background-color: var(--btn-green-hover);
+}
+
+.btn-download {
+ background-color: #21262d;
+ color: var(--text-main);
+ border: 1px solid var(--border-color);
+}
+
+.btn-download:hover {
+ background-color: #30363d;
+ border-color: var(--text-muted);
+}
+
+/* Footer */
+footer {
+ border-top: 1px solid var(--border-color);
+ background-color: var(--bg-card);
+ padding: 2rem 1.5rem;
+ text-align: center;
+ color: var(--text-muted);
+ font-size: 0.85rem;
+ margin-top: auto;
+}
+
+footer a {
+ color: var(--accent-cyan);
+ text-decoration: none;
+}
+
+footer a:hover {
+ text-decoration: underline;
+}
+
+@media (max-width: 600px) {
+ .hero h1 {
+ font-size: 2rem;
+ }
+ .card-actions {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/publishers/m5dev.json b/publishers/m5dev.json
new file mode 100644
index 0000000..bd7616e
--- /dev/null
+++ b/publishers/m5dev.json
@@ -0,0 +1,7 @@
+{
+ "id": "m5dev",
+ "name": "Yusuf Ezz (M5 Dev)",
+ "github": "https://github.com/M5Devs",
+ "is_verified": true,
+ "badge": "Core Architect"
+}
diff --git a/schema.sql b/schema.sql
new file mode 100644
index 0000000..edd9d55
--- /dev/null
+++ b/schema.sql
@@ -0,0 +1,36 @@
+-- Cloudflare D1 Database Schema for OpenROM Plugin Store
+
+DROP TABLE IF EXISTS plugins;
+DROP TABLE IF EXISTS publishers;
+
+-- Publishers table
+CREATE TABLE publishers (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ github TEXT,
+ is_verified INTEGER DEFAULT 0 NOT NULL,
+ badge TEXT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+-- Plugins table
+CREATE TABLE plugins (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ version TEXT NOT NULL,
+ author_id TEXT NOT NULL,
+ description TEXT NOT NULL,
+ conversions TEXT NOT NULL, -- JSON array string or comma-separated list
+ tags TEXT, -- JSON array string or comma-separated list
+ icon TEXT,
+ download_url TEXT NOT NULL,
+ homepage TEXT,
+ license TEXT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ FOREIGN KEY (author_id) REFERENCES publishers(id) ON DELETE CASCADE
+);
+
+-- Indexes for performance
+CREATE INDEX idx_publishers_verified ON publishers(is_verified);
+CREATE INDEX idx_plugins_author ON plugins(author_id);
diff --git a/scripts/build_catalog.py b/scripts/build_catalog.py
new file mode 100644
index 0000000..23830a9
--- /dev/null
+++ b/scripts/build_catalog.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+from datetime import datetime, timezone
+import json
+
+from pathlib import Path
+import sys
+
+def load_publishers() -> dict:
+ publishers = {}
+ pub_dir = Path("publishers")
+ if pub_dir.exists():
+ for p_file in pub_dir.glob("*.json"):
+ try:
+ with open(p_file, "r", encoding="utf-8") as f:
+ data = json.load(f)
+ if "id" in data:
+ publishers[data["id"]] = data
+ except Exception as e:
+ print(f"⚠️ Warning: Failed to load publisher {p_file}: {e}")
+ return publishers
+
+def build_catalog():
+ manifests_dir = Path("manifests")
+ if not manifests_dir.exists():
+ print("❌ 'manifests/' directory not found.")
+ sys.exit(1)
+
+ publishers = load_publishers()
+ plugins = []
+
+ manifest_files = sorted(list(manifests_dir.rglob("manifest.json")))
+ print(f"📦 Found {len(manifest_files)} manifest(s) for catalog generation.")
+
+ for m_file in manifest_files:
+ try:
+ with open(m_file, "r", encoding="utf-8") as f:
+ manifest = json.load(f)
+
+ author_id = manifest.get("author", "")
+ publisher_data = publishers.get(author_id, {
+ "id": author_id,
+ "name": author_id,
+ "github": "",
+ "is_verified": False,
+ "badge": None
+ })
+
+ plugin_entry = {
+ **manifest,
+ "publisher": publisher_data
+ }
+ plugins.append(plugin_entry)
+ except Exception as e:
+ print(f"❌ Error processing {m_file}: {e}")
+ sys.exit(1)
+
+ catalog_data = {
+ "version": "1.0",
+ "generated_at": datetime.now(timezone.utc).isoformat(),
+ "total_plugins": len(plugins),
+ "plugins": plugins
+ }
+
+ output_dir = Path("public/api")
+ output_dir.mkdir(parents=True, exist_ok=True)
+ catalog_file = output_dir / "catalog.json"
+
+ with open(catalog_file, "w", encoding="utf-8") as f:
+ json.dump(catalog_data, f, indent=2, ensure_ascii=False)
+ f.write("\n")
+
+ print(f"✨ Successfully generated catalog at {catalog_file} with {len(plugins)} plugin(s).")
+
+if __name__ == "__main__":
+ build_catalog()
diff --git a/scripts/validate_plugin.py b/scripts/validate_plugin.py
new file mode 100644
index 0000000..a28a6a2
--- /dev/null
+++ b/scripts/validate_plugin.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+import json
+import sys
+from pathlib import Path
+from urllib.parse import urlparse
+
+REQUIRED_FIELDS = ["id", "name", "version", "author", "conversions"]
+
+def validate_url(url_str: str, field_name: str, file_path: Path) -> bool:
+ if not isinstance(url_str, str) or not url_str.strip():
+ print(f"❌ Error in {file_path}: '{field_name}' must be a non-empty string.")
+ return False
+ parsed = urlparse(url_str)
+ if parsed.scheme not in ("http", "https") or not parsed.netloc:
+ print(f"❌ Error in {file_path}: '{field_name}' URL '{url_str}' is invalid.")
+ return False
+ return True
+
+def validate_manifest(file_path: Path) -> bool:
+ try:
+ with open(file_path, "r", encoding="utf-8") as f:
+ data = json.load(f)
+ except Exception as e:
+ print(f"❌ Error reading {file_path}: {e}")
+ return False
+
+ is_valid = True
+
+ for field in REQUIRED_FIELDS:
+ if field not in data or data[field] is None:
+ print(f"❌ Error in {file_path}: Missing mandatory field '{field}'.")
+ is_valid = False
+
+ if "id" in data and isinstance(data["id"], str):
+ plugin_id = data["id"]
+ if not plugin_id.strip():
+ print(f"❌ Error in {file_path}: Field 'id' must not be empty.")
+ is_valid = False
+
+ if "author" in data and isinstance(data["author"], str):
+ author_id = data["author"]
+ publisher_path = Path("publishers") / f"{author_id}.json"
+ if not publisher_path.exists():
+ print(f"❌ Error in {file_path}: Author/Publisher '{author_id}' record not found at {publisher_path}.")
+ is_valid = False
+
+ if "conversions" in data:
+ if not isinstance(data["conversions"], list) or len(data["conversions"]) == 0:
+ print(f"❌ Error in {file_path}: Field 'conversions' must be a non-empty list.")
+ is_valid = False
+
+ if "download_url" in data and data["download_url"]:
+ if not validate_url(data["download_url"], "download_url", file_path):
+ is_valid = False
+
+ if "icon" in data and data["icon"]:
+ if not validate_url(data["icon"], "icon", file_path):
+ is_valid = False
+
+ if "homepage" in data and data["homepage"]:
+ if not validate_url(data["homepage"], "homepage", file_path):
+ is_valid = False
+
+ return is_valid
+
+def main():
+ manifests_dir = Path("manifests")
+ if not manifests_dir.exists():
+ print("❌ 'manifests/' directory not found.")
+ sys.exit(1)
+
+ manifest_files = list(manifests_dir.rglob("manifest.json"))
+ if not manifest_files:
+ print("⚠️ No manifest.json files found.")
+ sys.exit(0)
+
+ print(f"🔍 Validating {len(manifest_files)} plugin manifest(s)...")
+ failed_count = 0
+
+ for manifest_path in manifest_files:
+ if validate_manifest(manifest_path):
+ print(f"✅ Valid: {manifest_path}")
+ else:
+ failed_count += 1
+
+ if failed_count > 0:
+ print(f"❌ Manifest validation failed with {failed_count} error(s).")
+ sys.exit(1)
+ else:
+ print("🎉 All plugin manifests validated successfully!")
+ sys.exit(0)
+
+if __name__ == "__main__":
+ main()