mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-09 05:24:49 +09:00
Introduce a first-class plugin contribution system and SDK, plus frontend integration and installer enhancements. Backend: - Add /plugins/manifest-spec and /plugins/contributions endpoints to expose the plugin.json contract and merged active-plugin UI contributions. - Add app/plugins_sdk for plugin authors (current_user, logger, audit, jwt helpers). - Add ContributionService to aggregate active plugin contributions (nav, routes, page_titles, command_palette, widgets). - Marketplace now hands off to plugin installer when an extension has a source URL and enforces admin-only installs in the marketplace API. - PluginService: run lifecycle hooks (install/uninstall) and install declared template dependencies (best-effort, recorded/logged) during plugin installs; surface template/manifest fields on InstalledPlugin.to_dict; surface linked InstalledPlugin status on ExtensionInstall.to_dict. Frontend: - Add runtime contributions loader (plugins/contributions.js) with useContributions hook and resolveComponent/getPluginModule helpers. - Add SDK re-exports for plugin frontends (plugins/sdk/index.js) and Vite alias 'serverkit-sdk'. - Integrate contributions across UI: Sidebar, CommandPalette, PluginLoader (declared widgets + legacy fallback), PageTitleUpdater, DashboardLayout (refreshContributions), and route injection via new ExtensionRoutes hook loaded in App.jsx. - Add API client getPluginContributions and adjust PluginLoader discovery patterns. Other: - Add .extension-platform-plan/ to .gitignore. Overall this enables declarative plugin-driven UI integration (sidebar items, SPA routes, page titles, command palette entries, and widgets), lifecycle hooks, and template dependency installs while keeping legacy behavior for older plugins.
98 lines
3.8 KiB
Python
98 lines
3.8 KiB
Python
"""Installed plugin tracking model."""
|
|
from datetime import datetime
|
|
from app import db
|
|
import json
|
|
|
|
|
|
class InstalledPlugin(db.Model):
|
|
"""Tracks plugins installed from external sources (zips/URLs)."""
|
|
__tablename__ = 'installed_plugins'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
name = db.Column(db.String(128), nullable=False, unique=True)
|
|
display_name = db.Column(db.String(256), nullable=False)
|
|
slug = db.Column(db.String(128), nullable=False, unique=True)
|
|
version = db.Column(db.String(32), nullable=False)
|
|
description = db.Column(db.Text)
|
|
author = db.Column(db.String(128))
|
|
homepage = db.Column(db.String(512))
|
|
repository = db.Column(db.String(512))
|
|
license = db.Column(db.String(64))
|
|
category = db.Column(db.String(64))
|
|
|
|
# Where it came from
|
|
source_url = db.Column(db.String(1024))
|
|
source_type = db.Column(db.String(32), default='url') # url, local, marketplace
|
|
|
|
# Paths relative to ServerKit root
|
|
backend_path = db.Column(db.String(512)) # e.g. app/plugins/serverkit-ai
|
|
frontend_path = db.Column(db.String(512)) # e.g. src/plugins/serverkit-ai
|
|
|
|
# Blueprint registration info from manifest
|
|
entry_point = db.Column(db.String(256)) # e.g. blueprint:ai_assistant_bp
|
|
url_prefix = db.Column(db.String(256)) # e.g. /api/v1/ai-assistant
|
|
|
|
# Frontend entry from manifest
|
|
frontend_entry = db.Column(db.String(256)) # e.g. components/AiAssistant.jsx
|
|
|
|
# Full manifest stored as JSON
|
|
manifest_json = db.Column(db.Text)
|
|
|
|
# Status
|
|
STATUS_ACTIVE = 'active'
|
|
STATUS_DISABLED = 'disabled'
|
|
STATUS_ERROR = 'error'
|
|
STATUS_INSTALLING = 'installing'
|
|
status = db.Column(db.String(32), default=STATUS_INSTALLING)
|
|
error_message = db.Column(db.Text)
|
|
|
|
# Has frontend component that needs rebuild
|
|
has_frontend = db.Column(db.Boolean, default=False)
|
|
# Has backend blueprint
|
|
has_backend = db.Column(db.Boolean, default=False)
|
|
|
|
installed_by = db.Column(db.Integer, db.ForeignKey('users.id'))
|
|
installed_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
@property
|
|
def manifest(self):
|
|
return json.loads(self.manifest_json) if self.manifest_json else {}
|
|
|
|
@manifest.setter
|
|
def manifest(self, v):
|
|
self.manifest_json = json.dumps(v)
|
|
|
|
def to_dict(self):
|
|
manifest = self.manifest or {}
|
|
return {
|
|
'id': self.id,
|
|
'name': self.name,
|
|
'display_name': self.display_name,
|
|
'slug': self.slug,
|
|
'version': self.version,
|
|
'description': self.description,
|
|
'author': self.author,
|
|
'homepage': self.homepage,
|
|
'repository': self.repository,
|
|
'license': self.license,
|
|
'category': self.category,
|
|
'source_url': self.source_url,
|
|
'source_type': self.source_type,
|
|
'entry_point': self.entry_point,
|
|
'url_prefix': self.url_prefix,
|
|
'has_frontend': self.has_frontend,
|
|
'has_backend': self.has_backend,
|
|
'status': self.status,
|
|
'error_message': self.error_message,
|
|
# Surface declarative manifest fields so the install UI can
|
|
# show the user what they're approving without making a
|
|
# second round-trip to fetch the manifest.
|
|
'permissions': manifest.get('permissions') or [],
|
|
'contributions': manifest.get('contributions') or {},
|
|
'templates': manifest.get('templates') or [],
|
|
'lifecycle': manifest.get('lifecycle') or {},
|
|
'installed_at': self.installed_at.isoformat() if self.installed_at else None,
|
|
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|