Skip to main content

palladium_cli/commands/
plugin.rs

1//! `pd plugin` subcommands — manage loaded plugins via the control plane.
2
3use clap::{Args, Subcommand};
4use schemars::JsonSchema;
5use serde::Serialize;
6use serde_json::{json, Value};
7
8use crate::client::ControlPlaneClient;
9use crate::client::Endpoint;
10use crate::CliResult;
11
12#[derive(Subcommand, Debug, Serialize, JsonSchema)]
13#[serde(rename_all = "kebab-case")]
14pub enum PluginCommand {
15    /// List all currently loaded plugins.
16    List(PluginListArgs),
17    /// Show details for a loaded plugin by name.
18    Info(PluginInfoArgs),
19    /// Load a plugin from a file path (native .so/.dylib or .wasm).
20    Load(PluginLoadArgs),
21    /// Unload a plugin by name.
22    Unload(PluginUnloadArgs),
23    /// Reload a plugin by name (unload + re-load from the same path).
24    Reload(PluginReloadArgs),
25}
26
27#[derive(Args, Debug, Serialize, JsonSchema)]
28#[serde(rename_all = "kebab-case")]
29pub struct PluginListArgs {
30    /// Output in JSON format.
31    #[arg(long)]
32    pub json: bool,
33}
34
35#[derive(Args, Debug, Serialize, JsonSchema)]
36#[serde(rename_all = "kebab-case")]
37pub struct PluginInfoArgs {
38    /// Name of the plugin.
39    pub name: String,
40    /// Output in JSON format.
41    #[arg(long)]
42    pub json: bool,
43}
44
45#[derive(Args, Debug, Serialize, JsonSchema)]
46#[serde(rename_all = "kebab-case")]
47pub struct PluginLoadArgs {
48    /// Path to the plugin file (.so, .dylib, or .wasm).
49    pub path: String,
50    /// Output in JSON format.
51    #[arg(long)]
52    pub json: bool,
53}
54
55#[derive(Args, Debug, Serialize, JsonSchema)]
56#[serde(rename_all = "kebab-case")]
57pub struct PluginUnloadArgs {
58    /// Name of the plugin to unload.
59    pub name: String,
60    /// Output in JSON format.
61    #[arg(long)]
62    pub json: bool,
63}
64
65#[derive(Args, Debug, Serialize, JsonSchema)]
66#[serde(rename_all = "kebab-case")]
67pub struct PluginReloadArgs {
68    /// Name of the plugin to reload.
69    pub name: String,
70    /// Output in JSON format.
71    #[arg(long)]
72    pub json: bool,
73}
74
75pub fn run(cmd: PluginCommand, endpoint: &Endpoint) -> CliResult {
76    let mut client = ControlPlaneClient::connect_endpoint(endpoint)?;
77    match cmd {
78        PluginCommand::List(args) => {
79            let result = client.call("plugin.list", Value::Null)?;
80            let plugins = result.as_array().cloned().unwrap_or_default();
81            if args.json {
82                println!("{}", serde_json::to_string_pretty(&Value::Array(plugins))?);
83            } else {
84                print_plugin_table(&plugins);
85            }
86        }
87        PluginCommand::Info(args) => {
88            let result = client.call("plugin.list", Value::Null)?;
89            let plugins = result.as_array().cloned().unwrap_or_default();
90            let plugin = plugins
91                .into_iter()
92                .find(|p| p.get("name").and_then(|v| v.as_str()) == Some(args.name.as_str()))
93                .ok_or_else(|| format!("plugin not found: {}", args.name))?;
94
95            if args.json {
96                println!("{}", serde_json::to_string_pretty(&plugin)?);
97            } else {
98                let name = plugin.get("name").and_then(|v| v.as_str()).unwrap_or("?");
99                let version = plugin
100                    .get("version")
101                    .and_then(|v| v.as_str())
102                    .unwrap_or("?");
103                let kind = plugin.get("kind").and_then(|v| v.as_str()).unwrap_or("?");
104                let path = plugin.get("path").and_then(|v| v.as_str());
105                let actor_types = parse_actor_types(&plugin);
106
107                println!("Name: {name}");
108                println!("Version: {version}");
109                println!("Kind: {kind}");
110                match path {
111                    Some(p) => {
112                        let file = std::path::Path::new(p)
113                            .file_name()
114                            .and_then(|n| n.to_str())
115                            .unwrap_or(p);
116                        println!("Path: {p}");
117                        println!("Filename: {file}");
118                    }
119                    None => {
120                        println!("Path: (unknown)");
121                    }
122                }
123                if actor_types.is_empty() {
124                    println!("Actor Types: (none)");
125                } else {
126                    println!("Actor Types: {}", actor_types.join(", "));
127                }
128            }
129        }
130        PluginCommand::Load(args) => {
131            let result = client.call("plugin.load", json!({"path": args.path}))?;
132            if args.json {
133                println!("{}", serde_json::to_string_pretty(&result)?);
134            } else {
135                let name = result.get("name").and_then(|v| v.as_str()).unwrap_or("?");
136                let version = result
137                    .get("version")
138                    .and_then(|v| v.as_str())
139                    .unwrap_or("?");
140                let kind = result.get("kind").and_then(|v| v.as_str()).unwrap_or("?");
141                println!("Plugin '{name}' v{version} ({kind}) loaded.");
142            }
143        }
144        PluginCommand::Unload(args) => {
145            client.call("plugin.unload", json!({"name": args.name}))?;
146            if args.json {
147                println!(
148                    "{}",
149                    serde_json::json!({
150                        "status": "unloaded",
151                        "name": args.name
152                    })
153                );
154            } else {
155                println!("Plugin '{}' unloaded.", args.name);
156            }
157        }
158        PluginCommand::Reload(args) => {
159            let result = client.call("plugin.reload", json!({"name": args.name}))?;
160            if args.json {
161                println!("{}", serde_json::to_string_pretty(&result)?);
162            } else {
163                let name = result.get("name").and_then(|v| v.as_str()).unwrap_or("?");
164                let version = result
165                    .get("version")
166                    .and_then(|v| v.as_str())
167                    .unwrap_or("?");
168                println!("Plugin '{name}' v{version} reloaded.");
169            }
170        }
171    }
172    Ok(())
173}
174
175fn print_plugin_table(plugins: &[Value]) {
176    if plugins.is_empty() {
177        println!("No plugins loaded.");
178        return;
179    }
180    println!("{:<30} {:<10} {:<8} ACTOR TYPES", "NAME", "VERSION", "KIND");
181    println!("{}", "-".repeat(95));
182    for p in plugins {
183        let name = p.get("name").and_then(|v| v.as_str()).unwrap_or("?");
184        let version = p.get("version").and_then(|v| v.as_str()).unwrap_or("?");
185        let kind = p.get("kind").and_then(|v| v.as_str()).unwrap_or("?");
186        let actor_types = parse_actor_types(p);
187        let type_text = if actor_types.is_empty() {
188            p.get("actor_type_count")
189                .and_then(|v| v.as_u64())
190                .map(|n| n.to_string())
191                .unwrap_or_else(|| "0".to_string())
192        } else {
193            actor_types.join(",")
194        };
195        println!("{:<30} {:<10} {:<8} {}", name, version, kind, type_text);
196    }
197}
198
199fn parse_actor_types(plugin: &Value) -> Vec<String> {
200    plugin
201        .get("actor_types")
202        .and_then(|v| v.as_array())
203        .map(|arr| {
204            arr.iter()
205                .filter_map(|v| v.as_str().map(str::to_string))
206                .collect::<Vec<_>>()
207        })
208        .unwrap_or_default()
209}