Skip to main content

max / balanced_breakfast

4.3 KB · 136 lines History Blame Raw
1 //! Plugin management commands
2 use crate::commands::error::ApiError;
3 use crate::state::AppState;
4 use serde::Serialize;
5 use std::sync::Arc;
6 use tauri::State;
7 use tracing::instrument;
8
9 /// Summary of a loaded plugin and its capabilities.
10 #[derive(Debug, Clone, Serialize)]
11 #[serde(rename_all = "camelCase")]
12 pub struct PluginResponse {
13 pub id: String,
14 pub name: String,
15 pub description: String,
16 pub supports_pagination: bool,
17 pub supports_search: bool,
18 pub requires_auth: bool,
19 }
20
21 /// A single configuration field descriptor for the frontend form.
22 #[derive(Debug, Clone, Serialize)]
23 #[serde(rename_all = "camelCase")]
24 pub struct ConfigFieldResponse {
25 pub key: String,
26 pub label: String,
27 pub description: Option<String>,
28 pub field_type: String,
29 pub required: bool,
30 pub default: Option<String>,
31 pub options: Vec<String>,
32 pub placeholder: Option<String>,
33 }
34
35 /// Full plugin configuration schema sent to the frontend for dynamic form rendering.
36 #[derive(Debug, Clone, Serialize)]
37 #[serde(rename_all = "camelCase")]
38 pub struct PluginSchemaResponse {
39 pub id: String,
40 pub name: String,
41 pub description: String,
42 pub fields: Vec<ConfigFieldResponse>,
43 }
44
45 /// List all loaded plugins with their capabilities.
46 #[tauri::command]
47 #[instrument(skip_all)]
48 pub async fn list_plugins(
49 state: State<'_, Arc<AppState>>,
50 ) -> Result<Vec<PluginResponse>, ApiError> {
51 let plugins = state.orchestrator.plugins();
52 let plugins = plugins.read().await;
53
54 let plugin_ids = plugins.list_plugins();
55 let mut responses = Vec::new();
56
57 for id in plugin_ids {
58 if let Some((plugin_id, name, _path)) = plugins.get_plugin_info(&id) {
59 let caps = plugins.get_capabilities(&id);
60 let description = plugins
61 .get_config_schema(&id)
62 .map(|s| s.description)
63 .unwrap_or_default();
64 let (supports_pagination, supports_search, requires_auth) = match caps {
65 Some(c) => (c.supports_pagination, c.supports_search, c.requires_auth),
66 None => (false, false, false),
67 };
68 responses.push(PluginResponse {
69 id: plugin_id,
70 name,
71 description,
72 supports_pagination,
73 supports_search,
74 requires_auth,
75 });
76 }
77 }
78
79 Ok(responses)
80 }
81
82 /// Get the full configuration schema for a plugin (fields, types, defaults).
83 #[tauri::command]
84 #[instrument(skip_all)]
85 pub async fn get_plugin_schema(
86 state: State<'_, Arc<AppState>>,
87 id: String,
88 ) -> Result<PluginSchemaResponse, ApiError> {
89 let plugins = state.orchestrator.plugins();
90 let plugins = plugins.read().await;
91
92 let (_plugin_id, name, _path) = plugins
93 .get_plugin_info(&id)
94 .ok_or_else(|| ApiError::not_found(format!("Plugin {} not found", id)))?;
95
96 let schema = plugins
97 .get_config_schema(&id)
98 .ok_or_else(|| ApiError::not_found(format!("Plugin {} schema not found", id)))?;
99
100 // Convert internal ConfigFieldType enums to string tags for the frontend.
101 // Uses into_iter() to move fields out of the owned schema instead of cloning.
102 let fields: Vec<ConfigFieldResponse> = schema
103 .fields
104 .into_iter()
105 .map(|f| {
106 let field_type = match f.field_type {
107 bb_interface::ConfigFieldType::Text => "text",
108 bb_interface::ConfigFieldType::TextArea => "textarea",
109 bb_interface::ConfigFieldType::Secret => "secret",
110 bb_interface::ConfigFieldType::Url => "url",
111 bb_interface::ConfigFieldType::Number => "number",
112 bb_interface::ConfigFieldType::Toggle => "toggle",
113 bb_interface::ConfigFieldType::Select => "select",
114 };
115
116 ConfigFieldResponse {
117 key: f.key,
118 label: f.label,
119 description: f.description,
120 field_type: field_type.to_string(),
121 required: f.required,
122 default: f.default,
123 options: f.options,
124 placeholder: f.placeholder,
125 }
126 })
127 .collect();
128
129 Ok(PluginSchemaResponse {
130 id,
131 name,
132 description: schema.description,
133 fields,
134 })
135 }
136