//! Plugin management commands use crate::commands::error::ApiError; use crate::state::AppState; use serde::Serialize; use std::sync::Arc; use tauri::State; use tracing::instrument; /// Summary of a loaded plugin and its capabilities. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct PluginResponse { pub id: String, pub name: String, pub description: String, pub supports_pagination: bool, pub supports_search: bool, pub requires_auth: bool, } /// A single configuration field descriptor for the frontend form. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ConfigFieldResponse { pub key: String, pub label: String, pub description: Option, pub field_type: String, pub required: bool, pub default: Option, pub options: Vec, pub placeholder: Option, } /// Full plugin configuration schema sent to the frontend for dynamic form rendering. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct PluginSchemaResponse { pub id: String, pub name: String, pub description: String, pub fields: Vec, } /// List all loaded plugins with their capabilities. #[tauri::command] #[instrument(skip_all)] pub async fn list_plugins( state: State<'_, Arc>, ) -> Result, ApiError> { let plugins = state.orchestrator.plugins(); let plugins = plugins.read().await; let plugin_ids = plugins.list_plugins(); let mut responses = Vec::new(); for id in plugin_ids { if let Some((plugin_id, name, _path)) = plugins.get_plugin_info(&id) { let caps = plugins.get_capabilities(&id); let description = plugins .get_config_schema(&id) .map(|s| s.description) .unwrap_or_default(); let (supports_pagination, supports_search, requires_auth) = match caps { Some(c) => (c.supports_pagination, c.supports_search, c.requires_auth), None => (false, false, false), }; responses.push(PluginResponse { id: plugin_id, name, description, supports_pagination, supports_search, requires_auth, }); } } Ok(responses) } /// Get the full configuration schema for a plugin (fields, types, defaults). #[tauri::command] #[instrument(skip_all)] pub async fn get_plugin_schema( state: State<'_, Arc>, id: String, ) -> Result { let plugins = state.orchestrator.plugins(); let plugins = plugins.read().await; let (_plugin_id, name, _path) = plugins .get_plugin_info(&id) .ok_or_else(|| ApiError::not_found(format!("Plugin {} not found", id)))?; let schema = plugins .get_config_schema(&id) .ok_or_else(|| ApiError::not_found(format!("Plugin {} schema not found", id)))?; // Convert internal ConfigFieldType enums to string tags for the frontend. // Uses into_iter() to move fields out of the owned schema instead of cloning. let fields: Vec = schema .fields .into_iter() .map(|f| { let field_type = match f.field_type { bb_interface::ConfigFieldType::Text => "text", bb_interface::ConfigFieldType::TextArea => "textarea", bb_interface::ConfigFieldType::Secret => "secret", bb_interface::ConfigFieldType::Url => "url", bb_interface::ConfigFieldType::Number => "number", bb_interface::ConfigFieldType::Toggle => "toggle", bb_interface::ConfigFieldType::Select => "select", }; ConfigFieldResponse { key: f.key, label: f.label, description: f.description, field_type: field_type.to_string(), required: f.required, default: f.default, options: f.options, placeholder: f.placeholder, } }) .collect(); Ok(PluginSchemaResponse { id, name, description: schema.description, fields, }) }