Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ia01590cdeed872cc8ebd16f6ca95f3cc6a6a6964
227 lines
6.4 KiB
Rust
227 lines
6.4 KiB
Rust
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<Arc<PluginManager>, 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<AppState>,
|
|
) -> Result<Json<Vec<PluginResponse>>, 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<AppState>,
|
|
Path(id): Path<String>,
|
|
) -> Result<Json<PluginResponse>, 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<AppState>,
|
|
Json(req): Json<InstallPluginRequest>,
|
|
) -> Result<Json<PluginResponse>, 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<AppState>,
|
|
Path(id): Path<String>,
|
|
) -> Result<Json<serde_json::Value>, 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<AppState>,
|
|
Path(id): Path<String>,
|
|
Json(req): Json<TogglePluginRequest>,
|
|
) -> Result<Json<serde_json::Value>, 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<AppState>,
|
|
) -> Result<Json<Vec<PluginUiPageEntry>>, 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<AppState>,
|
|
) -> Result<Json<Vec<PluginUiWidgetEntry>>, 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<AppState>,
|
|
Json(req): Json<PluginEventRequest>,
|
|
) -> Result<Json<serde_json::Value>, 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<AppState>,
|
|
) -> Result<Json<HashMap<String, String>>, 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<AppState>,
|
|
Path(id): Path<String>,
|
|
) -> Result<Json<serde_json::Value>, 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})))
|
|
}
|