Skip to main content

palladium_cli/commands/
actor.rs

1use clap::{Args, Subcommand};
2use schemars::JsonSchema;
3use serde::Serialize;
4use serde_json::{json, Value};
5
6use crate::client::ControlPlaneClient;
7use crate::client::Endpoint;
8use crate::output;
9use crate::CliResult;
10
11#[derive(Subcommand, Debug, Serialize, JsonSchema)]
12#[serde(rename_all = "kebab-case")]
13pub enum ActorCommand {
14    /// List actors, optionally filtered by path prefix or state.
15    List(ActorListArgs),
16    /// Show detailed info for a single actor.
17    Info(ActorInfoArgs),
18    /// Stop an actor by path.
19    Stop(ActorStopArgs),
20    /// Restart an actor by path.
21    Restart(ActorRestartArgs),
22    /// Kill an actor immediately by path.
23    Kill(ActorKillArgs),
24    /// Inspect consensus binding for an actor.
25    #[command(subcommand)]
26    Binding(ActorBindingCommand),
27    /// Spawn a new actor by type at a specific path.
28    Spawn(ActorSpawnArgs),
29}
30
31#[derive(Subcommand, Debug, Serialize, JsonSchema)]
32#[serde(rename_all = "kebab-case")]
33pub enum ActorBindingCommand {
34    /// Show consensus binding for an actor path.
35    Get(ActorBindingGetArgs),
36}
37
38#[derive(Args, Debug, Serialize, JsonSchema)]
39#[serde(rename_all = "kebab-case")]
40pub struct ActorListArgs {
41    /// Filter by path prefix (e.g. /user).
42    #[arg(long)]
43    pub path: Option<String>,
44    /// Filter by state: Running or Stopped.
45    #[arg(long)]
46    pub state: Option<String>,
47    /// Output in JSON format.
48    #[arg(long)]
49    pub json: bool,
50}
51
52#[derive(Args, Debug, Serialize, JsonSchema)]
53#[serde(rename_all = "kebab-case")]
54pub struct ActorInfoArgs {
55    /// Actor path (e.g. /user/my-actor).
56    pub path: String,
57    /// Output in JSON format.
58    #[arg(long)]
59    pub json: bool,
60}
61
62#[derive(Args, Debug, Serialize, JsonSchema)]
63#[serde(rename_all = "kebab-case")]
64pub struct ActorStopArgs {
65    /// Actor path.
66    pub path: String,
67    /// Send a forced stop (brutally terminates the actor task).
68    #[arg(long)]
69    pub force: bool,
70}
71
72#[derive(Args, Debug, Serialize, JsonSchema)]
73#[serde(rename_all = "kebab-case")]
74pub struct ActorRestartArgs {
75    /// Actor path.
76    pub path: String,
77    /// Output in JSON format.
78    #[arg(long)]
79    pub json: bool,
80}
81
82#[derive(Args, Debug, Serialize, JsonSchema)]
83#[serde(rename_all = "kebab-case")]
84pub struct ActorKillArgs {
85    /// Actor path.
86    pub path: String,
87    /// Output in JSON format.
88    #[arg(long)]
89    pub json: bool,
90}
91
92#[derive(Args, Debug, Serialize, JsonSchema)]
93#[serde(rename_all = "kebab-case")]
94pub struct ActorBindingGetArgs {
95    /// Actor path (e.g. /user/my-actor).
96    pub path: String,
97    /// Output in JSON format.
98    #[arg(long)]
99    pub json: bool,
100}
101
102#[derive(Args, Debug, Serialize, JsonSchema)]
103#[serde(rename_all = "kebab-case")]
104pub struct ActorSpawnArgs {
105    /// Actor type name registered by a plugin.
106    pub type_name: String,
107    /// Actor path (e.g. /user/my-actor or /user/core0/my-actor).
108    #[arg(long)]
109    pub path: String,
110    /// Optional plugin name (advisory; type_name remains authoritative).
111    #[arg(long)]
112    pub plugin: Option<String>,
113    /// Actor config JSON (passed verbatim to the plugin).
114    #[arg(long)]
115    pub config: Option<String>,
116    /// Output in JSON format.
117    #[arg(long)]
118    pub json: bool,
119}
120
121pub fn run(cmd: ActorCommand, endpoint: &Endpoint) -> CliResult {
122    let mut client = ControlPlaneClient::connect_endpoint(endpoint)?;
123    match cmd {
124        ActorCommand::List(args) => {
125            let mut params = serde_json::Map::new();
126            if let Some(prefix) = &args.path {
127                params.insert("path_prefix".into(), Value::String(prefix.clone()));
128            }
129            if let Some(state) = &args.state {
130                params.insert("state".into(), Value::String(state.clone()));
131            }
132            let result = client.call("actor.list", Value::Object(params))?;
133            let actors = result.as_array().cloned().unwrap_or_default();
134            if args.json {
135                println!("{}", serde_json::to_string_pretty(&Value::Array(actors))?);
136            } else {
137                print!("{}", output::format_actor_table(&actors));
138            }
139        }
140        ActorCommand::Info(args) => {
141            let result = client.call("actor.info", json!({"path": args.path}))?;
142            if args.json {
143                println!("{}", serde_json::to_string_pretty(&result)?);
144            } else {
145                print!("{}", output::format_actor_info(&result));
146            }
147        }
148        ActorCommand::Stop(args) => {
149            // --force is accepted; brutal stop wiring is Phase 5.
150            client.call("actor.stop", json!({"path": args.path}))?;
151            println!("Actor {} stopped.", args.path);
152        }
153        ActorCommand::Restart(args) => {
154            let result = client.call("actor.restart", json!({"path": args.path}))?;
155            if args.json {
156                println!("{}", serde_json::to_string_pretty(&result)?);
157            } else {
158                println!("Actor {} restart requested.", args.path);
159            }
160        }
161        ActorCommand::Kill(args) => {
162            let result = client.call("actor.kill", json!({"path": args.path}))?;
163            if args.json {
164                println!("{}", serde_json::to_string_pretty(&result)?);
165            } else {
166                println!("Actor {} killed.", args.path);
167            }
168        }
169        ActorCommand::Binding(cmd) => match cmd {
170            ActorBindingCommand::Get(args) => {
171                let result = client.call("actor.binding.get", json!({"path": args.path}))?;
172                if args.json {
173                    println!("{}", serde_json::to_string_pretty(&result)?);
174                } else {
175                    let group = result
176                        .get("group")
177                        .and_then(|v| v.as_str())
178                        .unwrap_or("unknown");
179                    println!("Actor {} is bound to consensus group {}.", args.path, group);
180                }
181            }
182        },
183        ActorCommand::Spawn(args) => {
184            let mut params = serde_json::Map::new();
185            params.insert("path".into(), Value::String(args.path.clone()));
186            params.insert("type_name".into(), Value::String(args.type_name.clone()));
187            if let Some(plugin) = &args.plugin {
188                params.insert("plugin".into(), Value::String(plugin.clone()));
189            }
190            if let Some(raw) = &args.config {
191                let parsed: Value = serde_json::from_str(raw)?;
192                params.insert("config".into(), parsed);
193            }
194            let result = client.call("actor.spawn", Value::Object(params))?;
195            if args.json {
196                println!("{}", serde_json::to_string_pretty(&result)?);
197            } else {
198                let path = result
199                    .get("path")
200                    .and_then(|v| v.as_str())
201                    .unwrap_or(&args.path);
202                println!("Actor spawned at {}.", path);
203            }
204        }
205    }
206    Ok(())
207}