Files
ServerKit/backend/app/models/plugin.py
T
Juan Denis ddcf0b2f5f Add plugin contributions system, SDK, and integration
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.
2026-05-03 13:54:40 -04:00

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,
}