generated from nhcarrigan/template
feat: add Plugin and MCP Server Management panels
Backend (Rust): - Add plugin management commands: list, install, uninstall, enable, disable, update - Add MCP server management commands: list, get details, remove - Integrate with Claude CLI's 'plugin' and 'mcp' subcommands - Export PluginInfo and McpServerInfo types Frontend (Svelte): - Create PluginManagementPanel component with full CRUD operations - Create McpManagementPanel component with server listing and removal - Add buttons to StatusBar for both panels - Beautiful UI with lucide-svelte icons - Theme-aware styling using CSS variables - Real-time loading states and error handling Closes #133 Closes #134
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tauri_plugin_http::reqwest;
|
||||
use tauri_plugin_store::StoreExt;
|
||||
@@ -1257,6 +1258,313 @@ pub async fn get_claude_version() -> Result<String, String> {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Plugin Management Commands ====================
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginInfo {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub description: Option<String>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_plugins() -> Result<Vec<PluginInfo>, String> {
|
||||
tracing::debug!("Listing Claude Code plugins");
|
||||
|
||||
let output = std::process::Command::new("claude")
|
||||
.arg("plugin")
|
||||
.arg("list")
|
||||
.arg("--json")
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
match serde_json::from_str::<Vec<PluginInfo>>(&stdout) {
|
||||
Ok(plugins) => {
|
||||
tracing::info!("Listed {} plugins", plugins.len());
|
||||
Ok(plugins)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to parse plugin list: {}", e);
|
||||
Err(format!("Failed to parse plugin list: {}", e))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
tracing::error!("Failed to list plugins: {}", error);
|
||||
Err(format!("Failed to list plugins: {}", error))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to execute claude plugin list: {}", e);
|
||||
Err(format!("Failed to execute claude plugin list: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_plugin(plugin_name: String) -> Result<String, String> {
|
||||
tracing::debug!("Installing plugin: {}", plugin_name);
|
||||
|
||||
let output = std::process::Command::new("claude")
|
||||
.arg("plugin")
|
||||
.arg("install")
|
||||
.arg(&plugin_name)
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
tracing::info!("Successfully installed plugin: {}", plugin_name);
|
||||
Ok(message)
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
tracing::error!("Failed to install plugin {}: {}", plugin_name, error);
|
||||
Err(format!("Failed to install plugin: {}", error))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to execute claude plugin install: {}", e);
|
||||
Err(format!("Failed to execute claude plugin install: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn uninstall_plugin(plugin_name: String) -> Result<String, String> {
|
||||
tracing::debug!("Uninstalling plugin: {}", plugin_name);
|
||||
|
||||
let output = std::process::Command::new("claude")
|
||||
.arg("plugin")
|
||||
.arg("uninstall")
|
||||
.arg(&plugin_name)
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
tracing::info!("Successfully uninstalled plugin: {}", plugin_name);
|
||||
Ok(message)
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
tracing::error!("Failed to uninstall plugin {}: {}", plugin_name, error);
|
||||
Err(format!("Failed to uninstall plugin: {}", error))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to execute claude plugin uninstall: {}", e);
|
||||
Err(format!("Failed to execute claude plugin uninstall: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn enable_plugin(plugin_name: String) -> Result<String, String> {
|
||||
tracing::debug!("Enabling plugin: {}", plugin_name);
|
||||
|
||||
let output = std::process::Command::new("claude")
|
||||
.arg("plugin")
|
||||
.arg("enable")
|
||||
.arg(&plugin_name)
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
tracing::info!("Successfully enabled plugin: {}", plugin_name);
|
||||
Ok(message)
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
tracing::error!("Failed to enable plugin {}: {}", plugin_name, error);
|
||||
Err(format!("Failed to enable plugin: {}", error))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to execute claude plugin enable: {}", e);
|
||||
Err(format!("Failed to execute claude plugin enable: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn disable_plugin(plugin_name: String) -> Result<String, String> {
|
||||
tracing::debug!("Disabling plugin: {}", plugin_name);
|
||||
|
||||
let output = std::process::Command::new("claude")
|
||||
.arg("plugin")
|
||||
.arg("disable")
|
||||
.arg(&plugin_name)
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
tracing::info!("Successfully disabled plugin: {}", plugin_name);
|
||||
Ok(message)
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
tracing::error!("Failed to disable plugin {}: {}", plugin_name, error);
|
||||
Err(format!("Failed to disable plugin: {}", error))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to execute claude plugin disable: {}", e);
|
||||
Err(format!("Failed to execute claude plugin disable: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_plugin(plugin_name: String) -> Result<String, String> {
|
||||
tracing::debug!("Updating plugin: {}", plugin_name);
|
||||
|
||||
let output = std::process::Command::new("claude")
|
||||
.arg("plugin")
|
||||
.arg("update")
|
||||
.arg(&plugin_name)
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
tracing::info!("Successfully updated plugin: {}", plugin_name);
|
||||
Ok(message)
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
tracing::error!("Failed to update plugin {}: {}", plugin_name, error);
|
||||
Err(format!("Failed to update plugin: {}", error))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to execute claude plugin update: {}", e);
|
||||
Err(format!("Failed to execute claude plugin update: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== MCP Management Commands ====================
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct McpServerInfo {
|
||||
pub name: String,
|
||||
pub command: Option<String>,
|
||||
pub url: Option<String>,
|
||||
pub transport: String, // "stdio", "http", or "sse"
|
||||
pub env: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_mcp_servers() -> Result<Vec<McpServerInfo>, String> {
|
||||
tracing::debug!("Listing MCP servers");
|
||||
|
||||
let output = std::process::Command::new("claude")
|
||||
.arg("mcp")
|
||||
.arg("list")
|
||||
.arg("--json")
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
match serde_json::from_str::<Vec<McpServerInfo>>(&stdout) {
|
||||
Ok(servers) => {
|
||||
tracing::info!("Listed {} MCP servers", servers.len());
|
||||
Ok(servers)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to parse MCP server list: {}", e);
|
||||
Err(format!("Failed to parse MCP server list: {}", e))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
tracing::error!("Failed to list MCP servers: {}", error);
|
||||
Err(format!("Failed to list MCP servers: {}", error))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to execute claude mcp list: {}", e);
|
||||
Err(format!("Failed to execute claude mcp list: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_mcp_server(name: String) -> Result<McpServerInfo, String> {
|
||||
tracing::debug!("Getting MCP server details: {}", name);
|
||||
|
||||
let output = std::process::Command::new("claude")
|
||||
.arg("mcp")
|
||||
.arg("get")
|
||||
.arg(&name)
|
||||
.arg("--json")
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
match serde_json::from_str::<McpServerInfo>(&stdout) {
|
||||
Ok(server) => {
|
||||
tracing::info!("Got MCP server details for: {}", name);
|
||||
Ok(server)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to parse MCP server details: {}", e);
|
||||
Err(format!("Failed to parse MCP server details: {}", e))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
tracing::error!("Failed to get MCP server {}: {}", name, error);
|
||||
Err(format!("Failed to get MCP server: {}", error))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to execute claude mcp get: {}", e);
|
||||
Err(format!("Failed to execute claude mcp get: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remove_mcp_server(name: String) -> Result<String, String> {
|
||||
tracing::debug!("Removing MCP server: {}", name);
|
||||
|
||||
let output = std::process::Command::new("claude")
|
||||
.arg("mcp")
|
||||
.arg("remove")
|
||||
.arg(&name)
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
tracing::info!("Successfully removed MCP server: {}", name);
|
||||
Ok(message)
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
tracing::error!("Failed to remove MCP server {}: {}", name, error);
|
||||
Err(format!("Failed to remove MCP server: {}", error))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to execute claude mcp remove: {}", e);
|
||||
Err(format!("Failed to execute claude mcp remove: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user