use anyhow::Result; use clap::{Parser, Subcommand}; use colored::*; use dialoguer::{Input, Select, Confirm, MultiSelect}; use hero_job::ScriptType; use hero_openrpc_client::{ AuthHelper, ClientTransport, HeroOpenRpcClient, JobParams, }; use std::path::PathBuf; use tracing::{error, info, Level}; use tracing_subscriber; #[derive(Parser)] #[command(name = "hero-openrpc-client")] #[command(about = "Hero OpenRPC Client - Interactive JSON-RPC client")] struct Cli { #[command(subcommand)] command: Commands, /// Private key for authentication (hex format) #[arg(long)] private_key: Option, /// Generate a new private key and exit #[arg(long)] generate_key: bool, /// Log level #[arg(long, default_value = "info")] log_level: String, } #[derive(Subcommand)] enum Commands { /// Connect to WebSocket server Websocket { /// Server URL #[arg(long, default_value = "ws://127.0.0.1:9944")] url: String, }, /// Connect to Unix socket server Unix { /// Unix socket path #[arg(long, default_value = "/tmp/hero-openrpc.sock")] socket_path: PathBuf, }, } /// Available RPC methods with descriptions #[derive(Debug, Clone)] struct RpcMethod { name: &'static str, description: &'static str, requires_auth: bool, } const RPC_METHODS: &[RpcMethod] = &[ RpcMethod { name: "fetch_nonce", description: "Fetch a nonce for authentication", requires_auth: false, }, RpcMethod { name: "authenticate", description: "Authenticate with public key and signature", requires_auth: false, }, RpcMethod { name: "whoami", description: "Get authentication status and user information", requires_auth: true, }, RpcMethod { name: "play", description: "Execute a Rhai script immediately", requires_auth: true, }, RpcMethod { name: "create_job", description: "Create a new job without starting it", requires_auth: true, }, RpcMethod { name: "start_job", description: "Start a previously created job", requires_auth: true, }, RpcMethod { name: "run_job", description: "Create and run a job, returning result when complete", requires_auth: true, }, RpcMethod { name: "get_job_status", description: "Get the current status of a job", requires_auth: true, }, RpcMethod { name: "get_job_output", description: "Get the output of a completed job", requires_auth: true, }, RpcMethod { name: "get_job_logs", description: "Get the logs of a job", requires_auth: true, }, RpcMethod { name: "list_jobs", description: "List all jobs in the system", requires_auth: true, }, RpcMethod { name: "stop_job", description: "Stop a running job", requires_auth: true, }, RpcMethod { name: "delete_job", description: "Delete a job from the system", requires_auth: true, }, RpcMethod { name: "clear_all_jobs", description: "Clear all jobs from the system", requires_auth: true, }, ]; #[tokio::main] async fn main() -> Result<()> { let cli = Cli::parse(); // Initialize tracing let log_level = match cli.log_level.to_lowercase().as_str() { "trace" => Level::TRACE, "debug" => Level::DEBUG, "info" => Level::INFO, "warn" => Level::WARN, "error" => Level::ERROR, _ => Level::INFO, }; tracing_subscriber::fmt() .with_max_level(log_level) .init(); // Handle key generation if cli.generate_key { let auth_helper = AuthHelper::generate()?; println!("{}", "Generated new private key:".green().bold()); println!("Private Key: {}", auth_helper.private_key_hex().yellow()); println!("Public Key: {}", auth_helper.public_key_hex().cyan()); println!(); println!("{}", "Save the private key securely and use it with --private-key".bright_yellow()); return Ok(()); } let transport = match cli.command { Commands::Websocket { url } => { println!("{} {}", "Connecting to WebSocket server:".green(), url.cyan()); ClientTransport::WebSocket(url) } Commands::Unix { socket_path } => { println!("{} {:?}", "Connecting to Unix socket server:".green(), socket_path); ClientTransport::Unix(socket_path) } }; // Connect to the server let client = HeroOpenRpcClient::connect(transport).await?; println!("{}", "Connected successfully!".green().bold()); // Handle authentication if private key is provided let mut authenticated = false; if let Some(private_key) = cli.private_key { println!("{}", "Authenticating...".yellow()); match client.authenticate_with_key(&private_key).await { Ok(true) => { println!("{}", "Authentication successful!".green().bold()); authenticated = true; } Ok(false) => { println!("{}", "Authentication failed!".red().bold()); } Err(e) => { error!("Authentication error: {}", e); println!("{} {}", "Authentication error:".red().bold(), e); } } } else { println!("{}", "No private key provided. Some methods will require authentication.".yellow()); println!("{}", "Use --generate-key to create a new key or --private-key to use an existing one.".bright_yellow()); } println!(); // Interactive loop loop { // Filter methods based on authentication status let available_methods: Vec<&RpcMethod> = RPC_METHODS .iter() .filter(|method| !method.requires_auth || authenticated) .collect(); if available_methods.is_empty() { println!("{}", "No methods available. Please authenticate first.".red()); break; } // Display method selection let method_names: Vec = available_methods .iter() .map(|method| { if method.requires_auth && !authenticated { format!("{} {} (requires auth)", method.name.red(), method.description) } else { format!("{} {}", method.name.green(), method.description) } }) .collect(); let selection = Select::new() .with_prompt("Select an RPC method to call") .items(&method_names) .default(0) .interact_opt()?; let Some(selection) = selection else { println!("{}", "Goodbye!".cyan()); break; }; let selected_method = available_methods[selection]; println!(); println!("{} {}", "Selected method:".bold(), selected_method.name.green()); // Handle method-specific parameter collection and execution match execute_method(&client, selected_method.name).await { Ok(_) => {} Err(e) => { error!("Method execution failed: {}", e); println!("{} {}", "Error:".red().bold(), e); } } println!(); // Ask if user wants to continue if !Confirm::new() .with_prompt("Do you want to call another method?") .default(true) .interact()? { break; } println!(); } println!("{}", "Goodbye!".cyan().bold()); Ok(()) } async fn execute_method(client: &HeroOpenRpcClient, method_name: &str) -> Result<()> { match method_name { "fetch_nonce" => { let pubkey: String = Input::new() .with_prompt("Public key (hex)") .interact_text()?; let result = client.fetch_nonce(pubkey).await?; println!("{} {}", "Nonce:".green().bold(), result.yellow()); } "authenticate" => { let pubkey: String = Input::new() .with_prompt("Public key (hex)") .interact_text()?; let signature: String = Input::new() .with_prompt("Signature (hex)") .interact_text()?; let result = client.authenticate(pubkey, signature, nonce).await?; println!("{} {}", "Authentication result:".green().bold(), if result { "Success".green() } else { "Failed".red() }); } "whoami" => { let result = client.whoami().await?; println!("{} {}", "User info:".green().bold(), serde_json::to_string_pretty(&result)?.cyan()); } "play" => { let script: String = Input::new() .with_prompt("Rhai script to execute") .interact_text()?; let result = client.play(script).await?; println!("{} {}", "Script output:".green().bold(), result.output.cyan()); } "create_job" => { let script: String = Input::new() .with_prompt("Script content") .interact_text()?; let script_types = ["HeroScript", "RhaiSAL", "RhaiDSL"]; let script_type_selection = Select::new() .with_prompt("Script type") .items(&script_types) .default(0) .interact()?; let script_type = match script_type_selection { 0 => ScriptType::HeroScript, 1 => ScriptType::RhaiSAL, 2 => ScriptType::RhaiDSL, _ => ScriptType::HeroScript, }; let add_prerequisites = Confirm::new() .with_prompt("Add prerequisites?") .default(false) .interact()?; let prerequisites = if add_prerequisites { let prereq_input: String = Input::new() .with_prompt("Prerequisites (comma-separated job IDs)") .interact_text()?; Some(prereq_input.split(',').map(|s| s.trim().to_string()).collect()) } else { None }; let job_params = JobParams { script, script_type, prerequisites, }; let result = client.create_job(job_params).await?; println!("{} {}", "Created job ID:".green().bold(), result.yellow()); } "start_job" => { let job_id: String = Input::new() .with_prompt("Job ID to start") .interact_text()?; let result = client.start_job(job_id).await?; println!("{} {}", "Start result:".green().bold(), if result.success { "Success".green() } else { "Failed".red() }); } "run_job" => { let script: String = Input::new() .with_prompt("Script content") .interact_text()?; let script_types = ["HeroScript", "RhaiSAL", "RhaiDSL"]; let script_type_selection = Select::new() .with_prompt("Script type") .items(&script_types) .default(0) .interact()?; let script_type = match script_type_selection { 0 => ScriptType::HeroScript, 1 => ScriptType::RhaiSAL, 2 => ScriptType::RhaiDSL, _ => ScriptType::HeroScript, }; let add_prerequisites = Confirm::new() .with_prompt("Add prerequisites?") .default(false) .interact()?; let prerequisites = if add_prerequisites { let prereq_input: String = Input::new() .with_prompt("Prerequisites (comma-separated job IDs)") .interact_text()?; Some(prereq_input.split(',').map(|s| s.trim().to_string()).collect()) } else { None }; let result = client.run_job(script, script_type, prerequisites).await?; println!("{} {}", "Job result:".green().bold(), result.cyan()); } "get_job_status" => { let job_id: String = Input::new() .with_prompt("Job ID") .interact_text()?; let result = client.get_job_status(job_id).await?; println!("{} {:?}", "Job status:".green().bold(), result); } "get_job_output" => { let job_id: String = Input::new() .with_prompt("Job ID") .interact_text()?; let result = client.get_job_output(job_id).await?; println!("{} {}", "Job output:".green().bold(), result.cyan()); } "get_job_logs" => { let job_id: String = Input::new() .with_prompt("Job ID") .interact_text()?; let result = client.get_job_logs(job_id).await?; println!("{} {}", "Job logs:".green().bold(), result.logs.cyan()); } "list_jobs" => { let result = client.list_jobs().await?; println!("{}", "Jobs:".green().bold()); for job in result { println!(" {} - {} ({:?})", job.id().yellow(), job.script_type(), job.status() ); } } "stop_job" => { let job_id: String = Input::new() .with_prompt("Job ID to stop") .interact_text()?; client.stop_job(job_id.clone()).await?; println!("{} {}", "Stopped job:".green().bold(), job_id.yellow()); } "delete_job" => { let job_id: String = Input::new() .with_prompt("Job ID to delete") .interact_text()?; client.delete_job(job_id.clone()).await?; println!("{} {}", "Deleted job:".green().bold(), job_id.yellow()); } "clear_all_jobs" => { let confirm = Confirm::new() .with_prompt("Are you sure you want to clear ALL jobs?") .default(false) .interact()?; if confirm { client.clear_all_jobs().await?; println!("{}", "Cleared all jobs".green().bold()); } else { println!("{}", "Operation cancelled".yellow()); } } _ => { println!("{} {}", "Unknown method:".red().bold(), method_name); } } Ok(()) }