use std::{collections::HashMap, sync::Arc}; use axum::{ Json, extract::{Path, State}, }; use pinakes_core::plugin::PluginManager; use crate::{ dto::{ InstallPluginRequest, PluginEventRequest, PluginResponse, PluginUiPageEntry, PluginUiWidgetEntry, TogglePluginRequest, }, error::ApiError, state::AppState, }; fn require_plugin_manager( state: &AppState, ) -> Result, ApiError> { state.plugin_manager.clone().ok_or_else(|| { ApiError(pinakes_core::error::PinakesError::InvalidOperation( "Plugin system is not enabled".to_string(), )) }) } /// List all installed plugins pub async fn list_plugins( State(state): State, ) -> Result>, ApiError> { let plugin_manager = require_plugin_manager(&state)?; let plugins = plugin_manager.list_plugins().await; let mut responses = Vec::with_capacity(plugins.len()); for meta in plugins { let enabled = plugin_manager.is_plugin_enabled(&meta.id).await; responses.push(PluginResponse::new(meta, enabled)); } Ok(Json(responses)) } /// Get a specific plugin by ID pub async fn get_plugin( State(state): State, Path(id): Path, ) -> Result, ApiError> { let plugin_manager = require_plugin_manager(&state)?; let plugin = plugin_manager.get_plugin(&id).await.ok_or_else(|| { ApiError(pinakes_core::error::PinakesError::NotFound(format!( "Plugin not found: {id}" ))) })?; let enabled = plugin_manager.is_plugin_enabled(&id).await; Ok(Json(PluginResponse::new(plugin, enabled))) } /// Install a plugin from URL or file path pub async fn install_plugin( State(state): State, Json(req): Json, ) -> Result, ApiError> { let plugin_manager = require_plugin_manager(&state)?; let plugin_id = plugin_manager .install_plugin(&req.source) .await .map_err(|e| { ApiError(pinakes_core::error::PinakesError::InvalidOperation( format!("Failed to install plugin: {e}"), )) })?; let plugin = plugin_manager.get_plugin(&plugin_id).await.ok_or_else(|| { ApiError(pinakes_core::error::PinakesError::NotFound( "Plugin installed but not found".to_string(), )) })?; let enabled = plugin_manager.is_plugin_enabled(&plugin_id).await; Ok(Json(PluginResponse::new(plugin, enabled))) } /// Uninstall a plugin pub async fn uninstall_plugin( State(state): State, Path(id): Path, ) -> Result, ApiError> { let plugin_manager = require_plugin_manager(&state)?; plugin_manager.uninstall_plugin(&id).await.map_err(|e| { ApiError(pinakes_core::error::PinakesError::InvalidOperation( format!("Failed to uninstall plugin: {e}"), )) })?; Ok(Json(serde_json::json!({"uninstalled": true}))) } /// Enable or disable a plugin pub async fn toggle_plugin( State(state): State, Path(id): Path, Json(req): Json, ) -> Result, ApiError> { let plugin_manager = require_plugin_manager(&state)?; if req.enabled { plugin_manager.enable_plugin(&id).await.map_err(|e| { ApiError(pinakes_core::error::PinakesError::InvalidOperation( format!("Failed to enable plugin: {e}"), )) })?; } else { plugin_manager.disable_plugin(&id).await.map_err(|e| { ApiError(pinakes_core::error::PinakesError::InvalidOperation( format!("Failed to disable plugin: {e}"), )) })?; } // Re-discover capabilities after toggle so cached data stays current if let Some(ref pipeline) = state.plugin_pipeline && let Err(e) = pipeline.discover_capabilities().await { tracing::warn!( plugin_id = %id, error = %e, "failed to re-discover capabilities after plugin toggle" ); } Ok(Json(serde_json::json!({ "id": id, "enabled": req.enabled }))) } /// List all UI pages provided by loaded plugins pub async fn list_plugin_ui_pages( State(state): State, ) -> Result>, ApiError> { let plugin_manager = require_plugin_manager(&state)?; let pages = plugin_manager.list_ui_pages_with_endpoints().await; let entries = pages .into_iter() .map(|(plugin_id, page, allowed_endpoints)| { PluginUiPageEntry { plugin_id, page, allowed_endpoints, } }) .collect(); Ok(Json(entries)) } /// List all UI widgets provided by loaded plugins pub async fn list_plugin_ui_widgets( State(state): State, ) -> Result>, ApiError> { let plugin_manager = require_plugin_manager(&state)?; let widgets = plugin_manager.list_ui_widgets().await; let entries = widgets .into_iter() .map(|(plugin_id, widget)| PluginUiWidgetEntry { plugin_id, widget }) .collect(); Ok(Json(entries)) } /// Receive a plugin event emitted from the UI and dispatch it to interested /// server-side event-handler plugins via the pipeline. pub async fn emit_plugin_event( State(state): State, Json(req): Json, ) -> Result, ApiError> { tracing::info!(event = %req.event, "plugin UI event received"); state.emit_plugin_event(&req.event, &req.payload); Ok(Json( serde_json::json!({ "received": true, "event": req.event }), )) } /// List merged CSS custom property overrides from all enabled plugins pub async fn list_plugin_ui_theme_extensions( State(state): State, ) -> Result>, ApiError> { let plugin_manager = require_plugin_manager(&state)?; Ok(Json(plugin_manager.list_ui_theme_extensions().await)) } /// Reload a plugin (for development) pub async fn reload_plugin( State(state): State, Path(id): Path, ) -> Result, ApiError> { let plugin_manager = require_plugin_manager(&state)?; plugin_manager.reload_plugin(&id).await.map_err(|e| { ApiError(pinakes_core::error::PinakesError::InvalidOperation( format!("Failed to reload plugin: {e}"), )) })?; // Re-discover capabilities after reload so cached data stays current if let Some(ref pipeline) = state.plugin_pipeline && let Err(e) = pipeline.discover_capabilities().await { tracing::warn!( plugin_id = %id, error = %e, "failed to re-discover capabilities after plugin reload" ); } Ok(Json(serde_json::json!({"reloaded": true}))) }