palladium_cli/commands/
plugin.rs1use 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(PluginListArgs),
17 Info(PluginInfoArgs),
19 Load(PluginLoadArgs),
21 Unload(PluginUnloadArgs),
23 Reload(PluginReloadArgs),
25}
26
27#[derive(Args, Debug, Serialize, JsonSchema)]
28#[serde(rename_all = "kebab-case")]
29pub struct PluginListArgs {
30 #[arg(long)]
32 pub json: bool,
33}
34
35#[derive(Args, Debug, Serialize, JsonSchema)]
36#[serde(rename_all = "kebab-case")]
37pub struct PluginInfoArgs {
38 pub name: String,
40 #[arg(long)]
42 pub json: bool,
43}
44
45#[derive(Args, Debug, Serialize, JsonSchema)]
46#[serde(rename_all = "kebab-case")]
47pub struct PluginLoadArgs {
48 pub path: String,
50 #[arg(long)]
52 pub json: bool,
53}
54
55#[derive(Args, Debug, Serialize, JsonSchema)]
56#[serde(rename_all = "kebab-case")]
57pub struct PluginUnloadArgs {
58 pub name: String,
60 #[arg(long)]
62 pub json: bool,
63}
64
65#[derive(Args, Debug, Serialize, JsonSchema)]
66#[serde(rename_all = "kebab-case")]
67pub struct PluginReloadArgs {
68 pub name: String,
70 #[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}