use clap::Parser; use hero_dispatcher::{Dispatcher, DispatcherBuilder, ScriptType}; use log::{error, info}; use colored::Colorize; use std::io::{self, Write}; use std::time::Duration; #[derive(Parser, Debug)] #[command(author, version, about = "Rhai Client - Script execution client", long_about = None)] struct Args { /// Caller ID (your identity) #[arg(short = 'c', long = "caller-id", help = "Caller ID (your identity)")] caller_id: String, /// Context ID (execution context) #[arg(short = 'k', long = "context-id", help = "Context ID (execution context)")] context_id: String, /// Script type to execute (heroscript, rhai-sal, rhai-dsl) #[arg(short = 'T', long = "script-type", default_value = "heroscript", help = "Script type: heroscript, rhai-sal, or rhai-dsl")] script_type: String, /// HeroScript workers (comma-separated) #[arg(long = "hero-workers", default_value = "hero-worker-1", help = "HeroScript worker IDs (comma-separated)")] hero_workers: String, /// Rhai SAL workers (comma-separated) #[arg(long = "rhai-sal-workers", default_value = "rhai-sal-worker-1", help = "Rhai SAL worker IDs (comma-separated)")] rhai_sal_workers: String, /// Rhai DSL workers (comma-separated) #[arg(long = "rhai-dsl-workers", default_value = "rhai-dsl-worker-1", help = "Rhai DSL worker IDs (comma-separated)")] rhai_dsl_workers: String, /// Redis URL #[arg(short, long, default_value = "redis://localhost:6379", help = "Redis connection URL")] redis_url: String, /// Rhai script to execute #[arg(short, long, help = "Rhai script to execute")] script: Option, /// Path to Rhai script file #[arg(short, long, help = "Path to Rhai script file")] file: Option, /// Timeout for script execution (in seconds) #[arg(short, long, default_value = "30", help = "Timeout for script execution in seconds")] timeout: u64, /// Increase verbosity (can be used multiple times) #[arg(short, long, action = clap::ArgAction::Count, help = "Increase verbosity (-v for debug, -vv for trace)")] verbose: u8, /// Disable timestamps in log output #[arg(long, help = "Remove timestamps from log output")] no_timestamp: bool, } #[tokio::main] async fn main() -> Result<(), Box> { let args = Args::parse(); // Configure logging based on verbosity level let log_config = match args.verbose { 0 => "warn,hero_dispatcher=warn", 1 => "info,hero_dispatcher=info", 2 => "debug,hero_dispatcher=debug", _ => "trace,hero_dispatcher=trace", }; std::env::set_var("RUST_LOG", log_config); // Configure env_logger with or without timestamps if args.no_timestamp { env_logger::Builder::from_default_env() .format_timestamp(None) .init(); } else { env_logger::init(); } // Parse worker lists let hero_workers: Vec = args.hero_workers.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(); let rhai_sal_workers: Vec = args.rhai_sal_workers.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(); let rhai_dsl_workers: Vec = args.rhai_dsl_workers.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(); // Validate that at least one worker is provided for the selected script type match args.script_type.to_lowercase().as_str() { "heroscript" => { if hero_workers.is_empty() { error!("❌ No HeroScript workers provided. Use --hero-workers to specify at least one worker."); return Err("At least one HeroScript worker must be provided".into()); } } "rhai-sal" => { if rhai_sal_workers.is_empty() { error!("❌ No Rhai SAL workers provided. Use --rhai-sal-workers to specify at least one worker."); return Err("At least one Rhai SAL worker must be provided".into()); } } "rhai-dsl" => { if rhai_dsl_workers.is_empty() { error!("❌ No Rhai DSL workers provided. Use --rhai-dsl-workers to specify at least one worker."); return Err("At least one Rhai DSL worker must be provided".into()); } } _ => { error!("❌ Invalid script type: {}. Valid types: heroscript, rhai-sal, rhai-dsl", args.script_type); return Err(format!("Invalid script type: {}", args.script_type).into()); } } if args.verbose > 0 { info!("🔗 Starting Hero Dispatcher"); info!("📋 Configuration:"); info!(" Caller ID: {}", args.caller_id); info!(" Context ID: {}", args.context_id); info!(" Script Type: {}", args.script_type); info!(" HeroScript Workers: {:?}", hero_workers); info!(" Rhai SAL Workers: {:?}", rhai_sal_workers); info!(" Rhai DSL Workers: {:?}", rhai_dsl_workers); info!(" Redis URL: {}", args.redis_url); info!(" Timeout: {}s", args.timeout); info!(""); } // Create the dispatcher client let client = DispatcherBuilder::new() .caller_id(&args.caller_id) .context_id(&args.context_id) .heroscript_workers(hero_workers) .rhai_sal_workers(rhai_sal_workers) .rhai_dsl_workers(rhai_dsl_workers) .redis_url(&args.redis_url) .build()?; if args.verbose > 0 { info!("✅ Connected to Redis at {}", args.redis_url); } // Determine execution mode if let Some(script_content) = args.script { // Execute inline script if args.verbose > 0 { info!("📜 Executing inline script"); } execute_script(&client, script_content, &args.script_type, args.timeout).await?; } else if let Some(file_path) = args.file { // Execute script from file if args.verbose > 0 { info!("📁 Loading script from file: {}", file_path); } let script_content = std::fs::read_to_string(&file_path) .map_err(|e| format!("Failed to read script file '{}': {}", file_path, e))?; execute_script(&client, script_content, &args.script_type, args.timeout).await?; } else { // Interactive mode info!("🎮 Entering interactive mode"); info!("Type Rhai scripts and press Enter to execute. Type 'exit' or 'quit' to close."); run_interactive_mode(&client, &args.script_type, args.timeout, args.verbose).await?; } Ok(()) } async fn execute_script( client: &Dispatcher, script: String, script_type_str: &str, timeout_secs: u64, ) -> Result<(), Box> { info!("⚡ Executing script: {:.50}...", script); // Parse script type let script_type = match script_type_str.to_lowercase().as_str() { "heroscript" => ScriptType::HeroScript, "rhai-sal" => ScriptType::RhaiSAL, "rhai-dsl" => ScriptType::RhaiDSL, _ => { error!("❌ Invalid script type: {}. Valid types: heroscript, rhai-sal, rhai-dsl", script_type_str); return Err(format!("Invalid script type: {}", script_type_str).into()); } }; let timeout = Duration::from_secs(timeout_secs); match client .new_job() .script_type(script_type) .script(&script) .timeout(timeout) .await_response() .await { Ok(result) => { info!("✅ Script execution completed"); println!("{}", "Result:".green().bold()); println!("{}", result); } Err(e) => { error!("❌ Script execution failed: {}", e); return Err(Box::new(e)); } } Ok(()) } async fn run_interactive_mode( client: &Dispatcher, script_type_str: &str, timeout_secs: u64, verbose: u8, ) -> Result<(), Box> { // Parse script type let script_type = match script_type_str.to_lowercase().as_str() { "heroscript" => ScriptType::HeroScript, "rhai-sal" => ScriptType::RhaiSAL, "rhai-dsl" => ScriptType::RhaiDSL, _ => { error!("❌ Invalid script type: {}. Valid types: heroscript, rhai-sal, rhai-dsl", script_type_str); return Err(format!("Invalid script type: {}", script_type_str).into()); } }; let timeout = Duration::from_secs(timeout_secs); loop { print!("rhai> "); io::stdout().flush()?; let mut input = String::new(); io::stdin().read_line(&mut input)?; let input = input.trim(); if input.is_empty() { continue; } if input == "exit" || input == "quit" { info!("👋 Goodbye!"); break; } if verbose > 0 { info!("⚡ Executing: {}", input); } match client .new_job() .script_type(script_type.clone()) .script(input) .timeout(timeout) .await_response() .await { Ok(result) => { println!("{}", result.green()); } Err(e) => { println!("{}", format!("error: {}", e).red()); } } println!(); // Add blank line for readability } Ok(()) }