initial commit
This commit is contained in:
		
							
								
								
									
										872
									
								
								clients/openrpc/cmd/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										872
									
								
								clients/openrpc/cmd/main.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,872 @@
 | 
			
		||||
//! Interactive CLI for Hero Supervisor OpenRPC Client
 | 
			
		||||
//!
 | 
			
		||||
//! This CLI provides an interactive interface to explore and test OpenRPC methods
 | 
			
		||||
//! with arrow key navigation, parameter input, and response display.
 | 
			
		||||
 | 
			
		||||
use clap::Parser;
 | 
			
		||||
use crossterm::{
 | 
			
		||||
    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
 | 
			
		||||
    execute,
 | 
			
		||||
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
 | 
			
		||||
};
 | 
			
		||||
use ratatui::{
 | 
			
		||||
    backend::CrosstermBackend,
 | 
			
		||||
    layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
 | 
			
		||||
    style::{Color, Modifier, Style},
 | 
			
		||||
    text::{Line, Span, Text},
 | 
			
		||||
    widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap},
 | 
			
		||||
    Frame, Terminal,
 | 
			
		||||
};
 | 
			
		||||
use serde_json::json;
 | 
			
		||||
use std::io;
 | 
			
		||||
use chrono;
 | 
			
		||||
 | 
			
		||||
use hero_supervisor_openrpc_client::{SupervisorClient, RunnerConfig, RunnerType, ProcessManagerType};
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
#[derive(Parser)]
 | 
			
		||||
#[command(name = "openrpc-cli")]
 | 
			
		||||
#[command(about = "Interactive CLI for Hero Supervisor OpenRPC")]
 | 
			
		||||
struct Cli {
 | 
			
		||||
    /// OpenRPC server URL
 | 
			
		||||
    #[arg(short, long, default_value = "http://127.0.0.1:3030")]
 | 
			
		||||
    url: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
struct RpcMethod {
 | 
			
		||||
    name: String,
 | 
			
		||||
    description: String,
 | 
			
		||||
    params: Vec<RpcParam>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
struct RpcParam {
 | 
			
		||||
    name: String,
 | 
			
		||||
    param_type: String,
 | 
			
		||||
    required: bool,
 | 
			
		||||
    description: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct App {
 | 
			
		||||
    client: SupervisorClient,
 | 
			
		||||
    methods: Vec<RpcMethod>,
 | 
			
		||||
    list_state: ListState,
 | 
			
		||||
    current_screen: Screen,
 | 
			
		||||
    selected_method: Option<RpcMethod>,
 | 
			
		||||
    param_inputs: Vec<String>,
 | 
			
		||||
    current_param_index: usize,
 | 
			
		||||
    response: Option<String>,
 | 
			
		||||
    error_message: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, PartialEq)]
 | 
			
		||||
enum Screen {
 | 
			
		||||
    MethodList,
 | 
			
		||||
    ParamInput,
 | 
			
		||||
    Response,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl App {
 | 
			
		||||
    async fn new(url: String) -> Result<Self, Box<dyn std::error::Error>> {
 | 
			
		||||
        let client = SupervisorClient::new(&url)?;
 | 
			
		||||
        
 | 
			
		||||
        // Test connection to OpenRPC server using the standard rpc.discover method
 | 
			
		||||
        // This is the proper OpenRPC way to test server connectivity and discover available methods
 | 
			
		||||
        let discovery_result = client.discover().await;
 | 
			
		||||
        match discovery_result {
 | 
			
		||||
            Ok(discovery_info) => {
 | 
			
		||||
                println!("✓ Connected to OpenRPC server at {}", url);
 | 
			
		||||
                if let Some(info) = discovery_info.get("info") {
 | 
			
		||||
                    if let Some(title) = info.get("title").and_then(|t| t.as_str()) {
 | 
			
		||||
                        println!("  Server: {}", title);
 | 
			
		||||
                    }
 | 
			
		||||
                    if let Some(version) = info.get("version").and_then(|v| v.as_str()) {
 | 
			
		||||
                        println!("  Version: {}", version);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                return Err(format!("Failed to connect to OpenRPC server at {}: {}\nMake sure the supervisor is running with OpenRPC enabled.", url, e).into());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        let methods = vec![
 | 
			
		||||
            RpcMethod {
 | 
			
		||||
                name: "list_runners".to_string(),
 | 
			
		||||
                description: "List all registered runners".to_string(),
 | 
			
		||||
                params: vec![],
 | 
			
		||||
            },
 | 
			
		||||
            RpcMethod {
 | 
			
		||||
                name: "register_runner".to_string(),
 | 
			
		||||
                description: "Register a new runner to the supervisor with secret authentication".to_string(),
 | 
			
		||||
                params: vec![
 | 
			
		||||
                    RpcParam {
 | 
			
		||||
                        name: "secret".to_string(),
 | 
			
		||||
                        param_type: "String".to_string(),
 | 
			
		||||
                        required: true,
 | 
			
		||||
                        description: "Secret required for runner registration".to_string(),
 | 
			
		||||
                    },
 | 
			
		||||
                    RpcParam {
 | 
			
		||||
                        name: "name".to_string(),
 | 
			
		||||
                        param_type: "String".to_string(),
 | 
			
		||||
                        required: true,
 | 
			
		||||
                        description: "Name of the runner".to_string(),
 | 
			
		||||
                    },
 | 
			
		||||
                    RpcParam {
 | 
			
		||||
                        name: "queue".to_string(),
 | 
			
		||||
                        param_type: "String".to_string(),
 | 
			
		||||
                        required: true,
 | 
			
		||||
                        description: "Queue name for the runner to listen to".to_string(),
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
            RpcMethod {
 | 
			
		||||
                name: "run_job".to_string(),
 | 
			
		||||
                description: "Run a job on the appropriate runner".to_string(),
 | 
			
		||||
                params: vec![
 | 
			
		||||
                    RpcParam {
 | 
			
		||||
                        name: "secret".to_string(),
 | 
			
		||||
                        param_type: "String".to_string(),
 | 
			
		||||
                        required: true,
 | 
			
		||||
                        description: "Secret required for job execution".to_string(),
 | 
			
		||||
                    },
 | 
			
		||||
                    RpcParam {
 | 
			
		||||
                        name: "job_id".to_string(),
 | 
			
		||||
                        param_type: "String".to_string(),
 | 
			
		||||
                        required: true,
 | 
			
		||||
                        description: "Job ID".to_string(),
 | 
			
		||||
                    },
 | 
			
		||||
                    RpcParam {
 | 
			
		||||
                        name: "runner_name".to_string(),
 | 
			
		||||
                        param_type: "String".to_string(),
 | 
			
		||||
                        required: true,
 | 
			
		||||
                        description: "Name of the runner to execute the job".to_string(),
 | 
			
		||||
                    },
 | 
			
		||||
                    RpcParam {
 | 
			
		||||
                        name: "payload".to_string(),
 | 
			
		||||
                        param_type: "String".to_string(),
 | 
			
		||||
                        required: true,
 | 
			
		||||
                        description: "Job payload/script content".to_string(),
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
            RpcMethod {
 | 
			
		||||
                name: "remove_runner".to_string(),
 | 
			
		||||
                description: "Remove a runner from the supervisor".to_string(),
 | 
			
		||||
                params: vec![
 | 
			
		||||
                    RpcParam {
 | 
			
		||||
                        name: "actor_id".to_string(),
 | 
			
		||||
                        param_type: "String".to_string(),
 | 
			
		||||
                        required: true,
 | 
			
		||||
                        description: "ID of the runner to remove".to_string(),
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
            RpcMethod {
 | 
			
		||||
                name: "start_runner".to_string(),
 | 
			
		||||
                description: "Start a specific runner".to_string(),
 | 
			
		||||
                params: vec![
 | 
			
		||||
                    RpcParam {
 | 
			
		||||
                        name: "actor_id".to_string(),
 | 
			
		||||
                        param_type: "String".to_string(),
 | 
			
		||||
                        required: true,
 | 
			
		||||
                        description: "ID of the runner to start".to_string(),
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
            RpcMethod {
 | 
			
		||||
                name: "stop_runner".to_string(),
 | 
			
		||||
                description: "Stop a specific runner".to_string(),
 | 
			
		||||
                params: vec![
 | 
			
		||||
                    RpcParam {
 | 
			
		||||
                        name: "actor_id".to_string(),
 | 
			
		||||
                        param_type: "String".to_string(),
 | 
			
		||||
                        required: true,
 | 
			
		||||
                        description: "ID of the runner to stop".to_string(),
 | 
			
		||||
                    },
 | 
			
		||||
                    RpcParam {
 | 
			
		||||
                        name: "force".to_string(),
 | 
			
		||||
                        param_type: "bool".to_string(),
 | 
			
		||||
                        required: true,
 | 
			
		||||
                        description: "Whether to force stop the runner".to_string(),
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
            RpcMethod {
 | 
			
		||||
                name: "get_runner_status".to_string(),
 | 
			
		||||
                description: "Get the status of a specific runner".to_string(),
 | 
			
		||||
                params: vec![
 | 
			
		||||
                    RpcParam {
 | 
			
		||||
                        name: "actor_id".to_string(),
 | 
			
		||||
                        param_type: "String".to_string(),
 | 
			
		||||
                        required: true,
 | 
			
		||||
                        description: "ID of the runner".to_string(),
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
            RpcMethod {
 | 
			
		||||
                name: "get_all_runner_status".to_string(),
 | 
			
		||||
                description: "Get status of all runners".to_string(),
 | 
			
		||||
                params: vec![],
 | 
			
		||||
            },
 | 
			
		||||
            RpcMethod {
 | 
			
		||||
                name: "start_all".to_string(),
 | 
			
		||||
                description: "Start all runners".to_string(),
 | 
			
		||||
                params: vec![],
 | 
			
		||||
            },
 | 
			
		||||
            RpcMethod {
 | 
			
		||||
                name: "stop_all".to_string(),
 | 
			
		||||
                description: "Stop all runners".to_string(),
 | 
			
		||||
                params: vec![
 | 
			
		||||
                    RpcParam {
 | 
			
		||||
                        name: "force".to_string(),
 | 
			
		||||
                        param_type: "bool".to_string(),
 | 
			
		||||
                        required: true,
 | 
			
		||||
                        description: "Whether to force stop all runners".to_string(),
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
            RpcMethod {
 | 
			
		||||
                name: "get_all_status".to_string(),
 | 
			
		||||
                description: "Get status of all components".to_string(),
 | 
			
		||||
                params: vec![],
 | 
			
		||||
            },
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        let mut list_state = ListState::default();
 | 
			
		||||
        list_state.select(Some(0));
 | 
			
		||||
 | 
			
		||||
        Ok(App {
 | 
			
		||||
            client,
 | 
			
		||||
            methods,
 | 
			
		||||
            list_state,
 | 
			
		||||
            current_screen: Screen::MethodList,
 | 
			
		||||
            selected_method: None,
 | 
			
		||||
            param_inputs: vec![],
 | 
			
		||||
            current_param_index: 0,
 | 
			
		||||
            response: None,
 | 
			
		||||
            error_message: None,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn next_method(&mut self) {
 | 
			
		||||
        let i = match self.list_state.selected() {
 | 
			
		||||
            Some(i) => {
 | 
			
		||||
                if i >= self.methods.len() - 1 {
 | 
			
		||||
                    0
 | 
			
		||||
                } else {
 | 
			
		||||
                    i + 1
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            None => 0,
 | 
			
		||||
        };
 | 
			
		||||
        self.list_state.select(Some(i));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn previous_method(&mut self) {
 | 
			
		||||
        let i = match self.list_state.selected() {
 | 
			
		||||
            Some(i) => {
 | 
			
		||||
                if i == 0 {
 | 
			
		||||
                    self.methods.len() - 1
 | 
			
		||||
                } else {
 | 
			
		||||
                    i - 1
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            None => 0,
 | 
			
		||||
        };
 | 
			
		||||
        self.list_state.select(Some(i));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn select_method(&mut self) {
 | 
			
		||||
        if let Some(i) = self.list_state.selected() {
 | 
			
		||||
            let method = self.methods[i].clone();
 | 
			
		||||
            if method.params.is_empty() {
 | 
			
		||||
                // No parameters needed, call directly
 | 
			
		||||
                self.selected_method = Some(method);
 | 
			
		||||
                self.current_screen = Screen::Response;
 | 
			
		||||
            } else {
 | 
			
		||||
                // Parameters needed, go to input screen
 | 
			
		||||
                self.selected_method = Some(method.clone());
 | 
			
		||||
                self.param_inputs = vec!["".to_string(); method.params.len()];
 | 
			
		||||
                self.current_param_index = 0;
 | 
			
		||||
                self.current_screen = Screen::ParamInput;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn next_param(&mut self) {
 | 
			
		||||
        if let Some(method) = &self.selected_method {
 | 
			
		||||
            if self.current_param_index < method.params.len() - 1 {
 | 
			
		||||
                self.current_param_index += 1;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn previous_param(&mut self) {
 | 
			
		||||
        if self.current_param_index > 0 {
 | 
			
		||||
            self.current_param_index -= 1;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn add_char_to_current_param(&mut self, c: char) {
 | 
			
		||||
        if self.current_param_index < self.param_inputs.len() {
 | 
			
		||||
            self.param_inputs[self.current_param_index].push(c);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn remove_char_from_current_param(&mut self) {
 | 
			
		||||
        if self.current_param_index < self.param_inputs.len() {
 | 
			
		||||
            self.param_inputs[self.current_param_index].pop();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn execute_method(&mut self) {
 | 
			
		||||
        if let Some(method) = &self.selected_method {
 | 
			
		||||
            self.error_message = None;
 | 
			
		||||
            self.response = None;
 | 
			
		||||
 | 
			
		||||
            // Build parameters
 | 
			
		||||
            let mut params = json!({});
 | 
			
		||||
            
 | 
			
		||||
            if !method.params.is_empty() {
 | 
			
		||||
                for (i, param) in method.params.iter().enumerate() {
 | 
			
		||||
                    let input = &self.param_inputs[i];
 | 
			
		||||
                    if input.is_empty() && param.required {
 | 
			
		||||
                        self.error_message = Some(format!("Required parameter '{}' is empty", param.name));
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    if !input.is_empty() {
 | 
			
		||||
                        let value = match param.param_type.as_str() {
 | 
			
		||||
                            "bool" => {
 | 
			
		||||
                                match input.to_lowercase().as_str() {
 | 
			
		||||
                                    "true" | "1" | "yes" => json!(true),
 | 
			
		||||
                                    "false" | "0" | "no" => json!(false),
 | 
			
		||||
                                    _ => {
 | 
			
		||||
                                        self.error_message = Some(format!("Invalid boolean value for '{}': {}", param.name, input));
 | 
			
		||||
                                        return;
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            "i32" | "i64" | "u32" | "u64" => {
 | 
			
		||||
                                match input.parse::<i64>() {
 | 
			
		||||
                                    Ok(n) => json!(n),
 | 
			
		||||
                                    Err(_) => {
 | 
			
		||||
                                        self.error_message = Some(format!("Invalid number for '{}': {}", param.name, input));
 | 
			
		||||
                                        return;
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            _ => json!(input),
 | 
			
		||||
                        };
 | 
			
		||||
                        
 | 
			
		||||
                        if method.name == "register_runner" {
 | 
			
		||||
                            // Special handling for register_runner method
 | 
			
		||||
                            match param.name.as_str() {
 | 
			
		||||
                                "secret" => params["secret"] = value,
 | 
			
		||||
                                "name" => params["name"] = value,
 | 
			
		||||
                                "queue" => params["queue"] = value,
 | 
			
		||||
                                _ => {}
 | 
			
		||||
                            }
 | 
			
		||||
                        } else if method.name == "run_job" {
 | 
			
		||||
                            // Special handling for run_job method
 | 
			
		||||
                            match param.name.as_str() {
 | 
			
		||||
                                "secret" => params["secret"] = value,
 | 
			
		||||
                                "job_id" => params["job_id"] = value,
 | 
			
		||||
                                "runner_name" => params["runner_name"] = value,
 | 
			
		||||
                                "payload" => params["payload"] = value,
 | 
			
		||||
                                _ => {}
 | 
			
		||||
                            }
 | 
			
		||||
                        } else {
 | 
			
		||||
                            params[¶m.name] = value;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Execute the method
 | 
			
		||||
            let result: Result<serde_json::Value, hero_supervisor_openrpc_client::ClientError> = match method.name.as_str() {
 | 
			
		||||
                "list_runners" => {
 | 
			
		||||
                    match self.client.list_runners().await {
 | 
			
		||||
                        Ok(response) => {
 | 
			
		||||
                            match serde_json::to_value(response) {
 | 
			
		||||
                                Ok(value) => Ok(value),
 | 
			
		||||
                                Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                        Err(e) => Err(e),
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                "get_all_runner_status" => {
 | 
			
		||||
                    match self.client.get_all_runner_status().await {
 | 
			
		||||
                        Ok(response) => {
 | 
			
		||||
                            match serde_json::to_value(response) {
 | 
			
		||||
                                Ok(value) => Ok(value),
 | 
			
		||||
                                Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                        Err(e) => Err(e),
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                "start_all" => {
 | 
			
		||||
                    match self.client.start_all().await {
 | 
			
		||||
                        Ok(response) => {
 | 
			
		||||
                            match serde_json::to_value(response) {
 | 
			
		||||
                                Ok(value) => Ok(value),
 | 
			
		||||
                                Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                        Err(e) => Err(e),
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                "get_all_status" => {
 | 
			
		||||
                    match self.client.get_all_status().await {
 | 
			
		||||
                        Ok(response) => {
 | 
			
		||||
                            match serde_json::to_value(response) {
 | 
			
		||||
                                Ok(value) => Ok(value),
 | 
			
		||||
                                Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                        Err(e) => Err(e),
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                "stop_all" => {
 | 
			
		||||
                    let force = params.get("force").and_then(|v| v.as_bool()).unwrap_or(false);
 | 
			
		||||
                    match self.client.stop_all(force).await {
 | 
			
		||||
                        Ok(response) => {
 | 
			
		||||
                            match serde_json::to_value(response) {
 | 
			
		||||
                                Ok(value) => Ok(value),
 | 
			
		||||
                                Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                        Err(e) => Err(e),
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                "start_runner" => {
 | 
			
		||||
                    if let Some(actor_id) = params.get("actor_id").and_then(|v| v.as_str()) {
 | 
			
		||||
                        match self.client.start_runner(actor_id).await {
 | 
			
		||||
                            Ok(response) => {
 | 
			
		||||
                                match serde_json::to_value(response) {
 | 
			
		||||
                                    Ok(value) => Ok(value),
 | 
			
		||||
                                    Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
 | 
			
		||||
                                }
 | 
			
		||||
                            },
 | 
			
		||||
                            Err(e) => Err(e),
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        Err(hero_supervisor_openrpc_client::ClientError::from(
 | 
			
		||||
                            serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing actor_id parameter"))
 | 
			
		||||
                        ))
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                "stop_runner" => {
 | 
			
		||||
                    if let (Some(actor_id), Some(force)) = (
 | 
			
		||||
                        params.get("actor_id").and_then(|v| v.as_str()),
 | 
			
		||||
                        params.get("force").and_then(|v| v.as_bool())
 | 
			
		||||
                    ) {
 | 
			
		||||
                        match self.client.stop_runner(actor_id, force).await {
 | 
			
		||||
                            Ok(response) => {
 | 
			
		||||
                                match serde_json::to_value(response) {
 | 
			
		||||
                                    Ok(value) => Ok(value),
 | 
			
		||||
                                    Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
 | 
			
		||||
                                }
 | 
			
		||||
                            },
 | 
			
		||||
                            Err(e) => Err(e),
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        Err(hero_supervisor_openrpc_client::ClientError::from(
 | 
			
		||||
                            serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing parameters"))
 | 
			
		||||
                        ))
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                "remove_runner" => {
 | 
			
		||||
                    if let Some(actor_id) = params.get("actor_id").and_then(|v| v.as_str()) {
 | 
			
		||||
                        match self.client.remove_runner(actor_id).await {
 | 
			
		||||
                            Ok(response) => {
 | 
			
		||||
                                match serde_json::to_value(response) {
 | 
			
		||||
                                    Ok(value) => Ok(value),
 | 
			
		||||
                                    Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
 | 
			
		||||
                                }
 | 
			
		||||
                            },
 | 
			
		||||
                            Err(e) => Err(e),
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        Err(hero_supervisor_openrpc_client::ClientError::from(
 | 
			
		||||
                            serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing actor_id parameter"))
 | 
			
		||||
                        ))
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                "get_runner_status" => {
 | 
			
		||||
                    if let Some(actor_id) = params.get("actor_id").and_then(|v| v.as_str()) {
 | 
			
		||||
                        match self.client.get_runner_status(actor_id).await {
 | 
			
		||||
                            Ok(response) => {
 | 
			
		||||
                                match serde_json::to_value(response) {
 | 
			
		||||
                                    Ok(value) => Ok(value),
 | 
			
		||||
                                    Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
 | 
			
		||||
                                }
 | 
			
		||||
                            },
 | 
			
		||||
                            Err(e) => Err(e),
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        Err(hero_supervisor_openrpc_client::ClientError::from(
 | 
			
		||||
                            serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing actor_id parameter"))
 | 
			
		||||
                        ))
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                "register_runner" => {
 | 
			
		||||
                    if let (Some(secret), Some(name), Some(queue)) = (
 | 
			
		||||
                        params.get("secret").and_then(|v| v.as_str()),
 | 
			
		||||
                        params.get("name").and_then(|v| v.as_str()),
 | 
			
		||||
                        params.get("queue").and_then(|v| v.as_str())
 | 
			
		||||
                    ) {
 | 
			
		||||
                        match self.client.register_runner(secret, name, queue).await {
 | 
			
		||||
                            Ok(response) => {
 | 
			
		||||
                                match serde_json::to_value(response) {
 | 
			
		||||
                                    Ok(value) => Ok(value),
 | 
			
		||||
                                    Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
 | 
			
		||||
                                }
 | 
			
		||||
                            },
 | 
			
		||||
                            Err(e) => Err(e),
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        Err(hero_supervisor_openrpc_client::ClientError::from(
 | 
			
		||||
                            serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing required parameters: secret, name, queue"))
 | 
			
		||||
                        ))
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                "run_job" => {
 | 
			
		||||
                    if let (Some(secret), Some(job_id), Some(runner_name), Some(payload)) = (
 | 
			
		||||
                        params.get("secret").and_then(|v| v.as_str()),
 | 
			
		||||
                        params.get("job_id").and_then(|v| v.as_str()),
 | 
			
		||||
                        params.get("runner_name").and_then(|v| v.as_str()),
 | 
			
		||||
                        params.get("payload").and_then(|v| v.as_str())
 | 
			
		||||
                    ) {
 | 
			
		||||
                        // Create a job object
 | 
			
		||||
                        let job = serde_json::json!({
 | 
			
		||||
                            "id": job_id,
 | 
			
		||||
                            "caller_id": "cli_user",
 | 
			
		||||
                            "context_id": "cli_context",
 | 
			
		||||
                            "payload": payload,
 | 
			
		||||
                            "job_type": "SAL",
 | 
			
		||||
                            "runner_name": runner_name,
 | 
			
		||||
                            "timeout": 30000000000u64, // 30 seconds in nanoseconds
 | 
			
		||||
                            "env_vars": {},
 | 
			
		||||
                            "created_at": chrono::Utc::now().to_rfc3339(),
 | 
			
		||||
                            "updated_at": chrono::Utc::now().to_rfc3339()
 | 
			
		||||
                        });
 | 
			
		||||
                        
 | 
			
		||||
                        match self.client.run_job(secret, job).await {
 | 
			
		||||
                            Ok(response) => {
 | 
			
		||||
                                match serde_json::to_value(response) {
 | 
			
		||||
                                    Ok(value) => Ok(value),
 | 
			
		||||
                                    Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)),
 | 
			
		||||
                                }
 | 
			
		||||
                            },
 | 
			
		||||
                            Err(e) => Err(e),
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        Err(hero_supervisor_openrpc_client::ClientError::from(
 | 
			
		||||
                            serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing required parameters: secret, job_id, runner_name, payload"))
 | 
			
		||||
                        ))
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                _ => Err(hero_supervisor_openrpc_client::ClientError::from(
 | 
			
		||||
                    serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Method not implemented in CLI"))
 | 
			
		||||
                )),
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            match result {
 | 
			
		||||
                Ok(response) => {
 | 
			
		||||
                    self.response = Some(format!("{:#}", response));
 | 
			
		||||
                }
 | 
			
		||||
                Err(e) => {
 | 
			
		||||
                    self.error_message = Some(format!("Error: {}", e));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            self.current_screen = Screen::Response;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn back_to_methods(&mut self) {
 | 
			
		||||
        self.current_screen = Screen::MethodList;
 | 
			
		||||
        self.selected_method = None;
 | 
			
		||||
        self.param_inputs.clear();
 | 
			
		||||
        self.current_param_index = 0;
 | 
			
		||||
        self.response = None;
 | 
			
		||||
        self.error_message = None;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn ui(f: &mut Frame, app: &mut App) {
 | 
			
		||||
    match app.current_screen {
 | 
			
		||||
        Screen::MethodList => draw_method_list(f, app),
 | 
			
		||||
        Screen::ParamInput => draw_param_input(f, app),
 | 
			
		||||
        Screen::Response => draw_response(f, app),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn draw_method_list(f: &mut Frame, app: &mut App) {
 | 
			
		||||
    let chunks = Layout::default()
 | 
			
		||||
        .direction(Direction::Vertical)
 | 
			
		||||
        .margin(1)
 | 
			
		||||
        .constraints([Constraint::Min(0)].as_ref())
 | 
			
		||||
        .split(f.area());
 | 
			
		||||
 | 
			
		||||
    let items: Vec<ListItem> = app
 | 
			
		||||
        .methods
 | 
			
		||||
        .iter()
 | 
			
		||||
        .map(|method| {
 | 
			
		||||
            let content = vec![Line::from(vec![
 | 
			
		||||
                Span::styled(&method.name, Style::default().fg(Color::Yellow)),
 | 
			
		||||
                Span::raw(" - "),
 | 
			
		||||
                Span::raw(&method.description),
 | 
			
		||||
            ])];
 | 
			
		||||
            ListItem::new(content)
 | 
			
		||||
        })
 | 
			
		||||
        .collect();
 | 
			
		||||
 | 
			
		||||
    let items = List::new(items)
 | 
			
		||||
        .block(
 | 
			
		||||
            Block::default()
 | 
			
		||||
                .borders(Borders::ALL)
 | 
			
		||||
                .title("OpenRPC Methods (↑↓ to navigate, Enter to select, q to quit)"),
 | 
			
		||||
        )
 | 
			
		||||
        .highlight_style(
 | 
			
		||||
            Style::default()
 | 
			
		||||
                .bg(Color::LightGreen)
 | 
			
		||||
                .fg(Color::Black)
 | 
			
		||||
                .add_modifier(Modifier::BOLD),
 | 
			
		||||
        )
 | 
			
		||||
        .highlight_symbol(">> ");
 | 
			
		||||
 | 
			
		||||
    f.render_stateful_widget(items, chunks[0], &mut app.list_state);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn draw_param_input(f: &mut Frame, app: &mut App) {
 | 
			
		||||
    if let Some(method) = &app.selected_method {
 | 
			
		||||
        let chunks = Layout::default()
 | 
			
		||||
            .direction(Direction::Vertical)
 | 
			
		||||
            .margin(1)
 | 
			
		||||
            .constraints([
 | 
			
		||||
                Constraint::Length(3),
 | 
			
		||||
                Constraint::Min(0),
 | 
			
		||||
                Constraint::Length(3),
 | 
			
		||||
            ])
 | 
			
		||||
            .split(f.area());
 | 
			
		||||
 | 
			
		||||
        // Title
 | 
			
		||||
        let title = Paragraph::new(format!("Parameters for: {}", method.name))
 | 
			
		||||
            .block(Block::default().borders(Borders::ALL).title("Method"));
 | 
			
		||||
        f.render_widget(title, chunks[0]);
 | 
			
		||||
 | 
			
		||||
        // Parameters - create proper form layout with separate label and input areas
 | 
			
		||||
        let param_chunks = Layout::default()
 | 
			
		||||
            .direction(Direction::Vertical)
 | 
			
		||||
            .constraints(vec![Constraint::Length(5); method.params.len()])
 | 
			
		||||
            .split(chunks[1]);
 | 
			
		||||
 | 
			
		||||
        for (i, param) in method.params.iter().enumerate() {
 | 
			
		||||
            let is_current = i == app.current_param_index;
 | 
			
		||||
            
 | 
			
		||||
            // Split each parameter into label and input areas
 | 
			
		||||
            let param_layout = Layout::default()
 | 
			
		||||
                .direction(Direction::Vertical)
 | 
			
		||||
                .constraints([Constraint::Length(2), Constraint::Length(3)])
 | 
			
		||||
                .split(param_chunks[i]);
 | 
			
		||||
 | 
			
		||||
            // Parameter label and description
 | 
			
		||||
            let label_style = if is_current {
 | 
			
		||||
                Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
 | 
			
		||||
            } else {
 | 
			
		||||
                Style::default().fg(Color::White)
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let label_text = vec![
 | 
			
		||||
                Line::from(vec![
 | 
			
		||||
                    Span::styled(¶m.name, label_style),
 | 
			
		||||
                    Span::raw(if param.required { " (required)" } else { " (optional)" }),
 | 
			
		||||
                    Span::raw(format!(" [{}]", param.param_type)),
 | 
			
		||||
                ]),
 | 
			
		||||
                Line::from(Span::styled(¶m.description, Style::default().fg(Color::Gray))),
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            let label_widget = Paragraph::new(label_text)
 | 
			
		||||
                .block(Block::default().borders(Borders::NONE));
 | 
			
		||||
            f.render_widget(label_widget, param_layout[0]);
 | 
			
		||||
 | 
			
		||||
            // Input field
 | 
			
		||||
            let empty_string = String::new();
 | 
			
		||||
            let input_value = app.param_inputs.get(i).unwrap_or(&empty_string);
 | 
			
		||||
            
 | 
			
		||||
            let input_display = if is_current {
 | 
			
		||||
                if input_value.is_empty() {
 | 
			
		||||
                    "█".to_string() // Show cursor when active and empty
 | 
			
		||||
                } else {
 | 
			
		||||
                    format!("{}█", input_value) // Show cursor at end when active
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                if input_value.is_empty() {
 | 
			
		||||
                    " ".to_string() // Empty space for inactive empty fields
 | 
			
		||||
                } else {
 | 
			
		||||
                    input_value.clone()
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let input_style = if is_current {
 | 
			
		||||
                Style::default().fg(Color::Black).bg(Color::Cyan)
 | 
			
		||||
            } else {
 | 
			
		||||
                Style::default().fg(Color::White).bg(Color::DarkGray)
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let border_style = if is_current {
 | 
			
		||||
                Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)
 | 
			
		||||
            } else {
 | 
			
		||||
                Style::default().fg(Color::Gray)
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let input_widget = Paragraph::new(Line::from(Span::styled(input_display, input_style)))
 | 
			
		||||
                .block(
 | 
			
		||||
                    Block::default()
 | 
			
		||||
                        .borders(Borders::ALL)
 | 
			
		||||
                        .border_style(border_style)
 | 
			
		||||
                        .title(if is_current { " INPUT " } else { "" }),
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
            f.render_widget(input_widget, param_layout[1]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Instructions
 | 
			
		||||
        let instructions = Paragraph::new("↑↓ to navigate params, type to edit, Enter to execute, Esc to go back")
 | 
			
		||||
            .block(Block::default().borders(Borders::ALL).title("Instructions"));
 | 
			
		||||
        f.render_widget(instructions, chunks[2]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn draw_response(f: &mut Frame, app: &mut App) {
 | 
			
		||||
    let chunks = Layout::default()
 | 
			
		||||
        .direction(Direction::Vertical)
 | 
			
		||||
        .margin(1)
 | 
			
		||||
        .constraints([
 | 
			
		||||
            Constraint::Length(3),
 | 
			
		||||
            Constraint::Min(0),
 | 
			
		||||
            Constraint::Length(3),
 | 
			
		||||
        ])
 | 
			
		||||
        .split(f.area());
 | 
			
		||||
 | 
			
		||||
    // Title
 | 
			
		||||
    let method_name = app.selected_method.as_ref().map(|m| m.name.as_str()).unwrap_or("Unknown");
 | 
			
		||||
    let title = Paragraph::new(format!("Response for: {}", method_name))
 | 
			
		||||
        .block(Block::default().borders(Borders::ALL).title("Response"));
 | 
			
		||||
    f.render_widget(title, chunks[0]);
 | 
			
		||||
 | 
			
		||||
    // Response content
 | 
			
		||||
    let content = if let Some(error) = &app.error_message {
 | 
			
		||||
        Text::from(error.clone()).style(Style::default().fg(Color::Red))
 | 
			
		||||
    } else if let Some(response) = &app.response {
 | 
			
		||||
        Text::from(response.clone()).style(Style::default().fg(Color::Green))
 | 
			
		||||
    } else {
 | 
			
		||||
        Text::from("Executing...").style(Style::default().fg(Color::Yellow))
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let response_widget = Paragraph::new(content)
 | 
			
		||||
        .block(Block::default().borders(Borders::ALL))
 | 
			
		||||
        .wrap(Wrap { trim: true });
 | 
			
		||||
    f.render_widget(response_widget, chunks[1]);
 | 
			
		||||
 | 
			
		||||
    // Instructions
 | 
			
		||||
    let instructions = Paragraph::new("Esc to go back to methods")
 | 
			
		||||
        .block(Block::default().borders(Borders::ALL).title("Instructions"));
 | 
			
		||||
    f.render_widget(instructions, chunks[2]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[tokio::main]
 | 
			
		||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
 | 
			
		||||
    let cli = Cli::parse();
 | 
			
		||||
 | 
			
		||||
    // Setup terminal
 | 
			
		||||
    enable_raw_mode()?;
 | 
			
		||||
    let mut stdout = io::stdout();
 | 
			
		||||
    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
 | 
			
		||||
    let backend = CrosstermBackend::new(stdout);
 | 
			
		||||
    let mut terminal = Terminal::new(backend)?;
 | 
			
		||||
 | 
			
		||||
    // Create app
 | 
			
		||||
    let mut app = match App::new(cli.url).await {
 | 
			
		||||
        Ok(app) => app,
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            // Cleanup terminal before showing error
 | 
			
		||||
            disable_raw_mode()?;
 | 
			
		||||
            execute!(
 | 
			
		||||
                terminal.backend_mut(),
 | 
			
		||||
                LeaveAlternateScreen,
 | 
			
		||||
                DisableMouseCapture
 | 
			
		||||
            )?;
 | 
			
		||||
            terminal.show_cursor()?;
 | 
			
		||||
            
 | 
			
		||||
            eprintln!("Failed to connect to OpenRPC server: {}", e);
 | 
			
		||||
            eprintln!("Make sure the supervisor is running with OpenRPC enabled.");
 | 
			
		||||
            std::process::exit(1);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Main loop
 | 
			
		||||
    loop {
 | 
			
		||||
        terminal.draw(|f| ui(f, &mut app))?;
 | 
			
		||||
 | 
			
		||||
        if let Event::Key(key) = event::read()? {
 | 
			
		||||
            if key.kind == KeyEventKind::Press {
 | 
			
		||||
                match app.current_screen {
 | 
			
		||||
                    Screen::MethodList => {
 | 
			
		||||
                        match key.code {
 | 
			
		||||
                            KeyCode::Char('q') => break,
 | 
			
		||||
                            KeyCode::Down => app.next_method(),
 | 
			
		||||
                            KeyCode::Up => app.previous_method(),
 | 
			
		||||
                            KeyCode::Enter => {
 | 
			
		||||
                                app.select_method();
 | 
			
		||||
                                // If the selected method has no parameters, execute it immediately
 | 
			
		||||
                                if let Some(method) = &app.selected_method {
 | 
			
		||||
                                    if method.params.is_empty() {
 | 
			
		||||
                                        app.execute_method().await;
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            },
 | 
			
		||||
                            _ => {}
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    Screen::ParamInput => {
 | 
			
		||||
                        match key.code {
 | 
			
		||||
                            KeyCode::Esc => app.back_to_methods(),
 | 
			
		||||
                            KeyCode::Up => app.previous_param(),
 | 
			
		||||
                            KeyCode::Down => app.next_param(),
 | 
			
		||||
                            KeyCode::Enter => {
 | 
			
		||||
                                app.execute_method().await;
 | 
			
		||||
                            }
 | 
			
		||||
                            KeyCode::Backspace => app.remove_char_from_current_param(),
 | 
			
		||||
                            KeyCode::Char(c) => app.add_char_to_current_param(c),
 | 
			
		||||
                            _ => {}
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    Screen::Response => {
 | 
			
		||||
                        match key.code {
 | 
			
		||||
                            KeyCode::Esc => app.back_to_methods(),
 | 
			
		||||
                            _ => {}
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Restore terminal
 | 
			
		||||
    disable_raw_mode()?;
 | 
			
		||||
    execute!(
 | 
			
		||||
        terminal.backend_mut(),
 | 
			
		||||
        LeaveAlternateScreen,
 | 
			
		||||
        DisableMouseCapture
 | 
			
		||||
    )?;
 | 
			
		||||
    terminal.show_cursor()?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user