diff --git a/app/plugins/registry.py b/app/plugins/registry.py index 75dbe77..a387b81 100644 --- a/app/plugins/registry.py +++ b/app/plugins/registry.py @@ -211,12 +211,53 @@ class PluginRegistry: ) return warnings + # ── Version Comparison & Update Path ── + + async def _check_and_run_version_migrations( + self, db: AsyncSession, name: str, record: PluginModel + ) -> bool: + """Compare manifest version with DB version and run migrations if different. + + If the plugin's manifest version differs from the stored DB version, + re-run all migrations to bring the schema up to date, then update + the DB record's version field. + + Returns True if migrations were run, False otherwise. + """ + plugin = self.get_plugin(name) + if plugin is None: + return False + + manifest_version = plugin.manifest.version + db_version = record.version + + if manifest_version == db_version: + return False + + logger.info( + f"Plugin '{name}' version mismatch: DB={db_version}, manifest={manifest_version}. " + f"Running migrations to update." + ) + + # Re-run migrations to apply any new migration files + if plugin.manifest.migrations: + await self.migration_runner.run_all_migrations(db, name, plugin.manifest.migrations) + + # Update DB version to match manifest + record.version = manifest_version + await db.flush() + self._db_status[name] = record + + logger.info(f"Plugin '{name}' updated to version {manifest_version}.") + return True + # ── Install / Activate / Deactivate / Uninstall ── async def install(self, db: AsyncSession, name: str) -> PluginModel: """Install a plugin: run migrations and create DB record. - Idempotent: if already installed, returns existing record. + Idempotent: if already installed, checks for version updates and + returns existing record. Checks that all declared dependencies are installed first. """ plugin = self.get_plugin(name) @@ -226,6 +267,8 @@ class PluginRegistry: # Check if already installed in DB existing = await self._get_plugin_record(db, name) if existing is not None: + # Check for version update — run migrations if version changed + await self._check_and_run_version_migrations(db, name, existing) return existing # Check dependencies are installed @@ -256,7 +299,8 @@ class PluginRegistry: async def activate(self, db: AsyncSession, name: str) -> PluginModel: """Activate a plugin: register routes, event listeners, set status=active. - Idempotent: if already active, returns existing record without error. + Idempotent: if already active, checks for version updates and + returns existing record without error. Checks that all declared dependencies are active first. Performs a soft permission check and logs warnings for unknown permissions. """ @@ -268,6 +312,9 @@ class PluginRegistry: if record is None: raise ValueError(f"Plugin '{name}' is not installed — install first") + # Check for version update — run migrations if version changed + await self._check_and_run_version_migrations(db, name, record) + # Idempotent: already active if record.active and record.status == "active": return record