Production deployment with zinit config

This commit is contained in:
Timur Gordon
2025-10-20 22:24:48 +02:00
parent e7c377460e
commit e2971a335c
17 changed files with 10305 additions and 1201 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,15 +8,18 @@ license = "MIT OR Apache-2.0"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = []
[dependencies]
# Common dependencies for both native and WASM
hero-supervisor = { path = "../../" }
hero-job = { path = "../../../job" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
log = "0.4"
uuid = { version = "1.0", features = ["v4", "serde"] }
# Collections (common)
indexmap = "2.0"
# Native JSON-RPC client (not WASM compatible)
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
@@ -65,19 +68,9 @@ features = ["serde", "wasmbind"]
version = "1.0"
features = ["v4", "serde", "js"]
# Collections
# Collections (common)
indexmap = "2.0"
# Interactive CLI
crossterm = "0.27"
ratatui = "0.28"
# Command line parsing
clap = { version = "4.0", features = ["derive"] }
[[bin]]
name = "openrpc-cli"
path = "cmd/main.rs"
[dev-dependencies]
# Testing utilities

View File

@@ -1,872 +0,0 @@
//! 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".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" => params["runner"] = value,
"payload" => params["payload"] = value,
_ => {}
}
} else {
params[&param.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), Some(payload)) = (
params.get("secret").and_then(|v| v.as_str()),
params.get("job_id").and_then(|v| v.as_str()),
params.get("runner").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": runner,
"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, 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(&param.name, label_style),
Span::raw(if param.required { " (required)" } else { " (optional)" }),
Span::raw(format!(" [{}]", param.param_type)),
]),
Line::from(Span::styled(&param.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(())
}

View File

@@ -171,8 +171,13 @@ pub struct JobStatusResponse {
pub completed_at: Option<String>,
}
// Re-export Job types from shared crate
pub use hero_job::{Job, JobStatus, JobError, JobBuilder};
// Re-export Job types from runner_rust crate (native only)
#[cfg(not(target_arch = "wasm32"))]
pub use runner_rust::{Job, JobStatus, JobError, JobBuilder, Client, ClientBuilder};
// WASM-compatible Job types (simplified versions)
#[cfg(target_arch = "wasm32")]
pub use crate::wasm::{Job, JobStatus, JobError, JobBuilder};
/// Process status wrapper for OpenRPC serialization (matches server response)
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]

View File

@@ -7,10 +7,8 @@ use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response, Headers};
use serde::{Deserialize, Serialize};
// use std::collections::HashMap; // Unused
use thiserror::Error;
use uuid::Uuid;
// use js_sys::Promise; // Unused
/// WASM-compatible client for communicating with Hero Supervisor OpenRPC server
#[wasm_bindgen]
@@ -87,6 +85,102 @@ pub enum WasmJobType {
V,
}
/// Job status enumeration
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum JobStatus {
Pending,
Running,
Finished,
Error,
}
/// Job error type
#[derive(Debug, Clone, thiserror::Error)]
pub enum JobError {
#[error("Validation error: {0}")]
Validation(String),
#[error("Execution error: {0}")]
Execution(String),
#[error("Timeout error")]
Timeout,
}
/// Job builder for WASM
pub struct JobBuilder {
id: Option<String>,
caller_id: Option<String>,
context_id: Option<String>,
payload: Option<String>,
runner: Option<String>,
executor: Option<String>,
timeout_secs: Option<u64>,
env_vars: Option<String>,
}
impl JobBuilder {
pub fn new() -> Self {
Self {
id: None,
caller_id: None,
context_id: None,
payload: None,
runner: None,
executor: None,
timeout_secs: None,
env_vars: None,
}
}
pub fn caller_id(mut self, caller_id: &str) -> Self {
self.caller_id = Some(caller_id.to_string());
self
}
pub fn context_id(mut self, context_id: &str) -> Self {
self.context_id = Some(context_id.to_string());
self
}
pub fn payload(mut self, payload: &str) -> Self {
self.payload = Some(payload.to_string());
self
}
pub fn runner(mut self, runner: &str) -> Self {
self.runner = Some(runner.to_string());
self
}
pub fn executor(mut self, executor: &str) -> Self {
self.executor = Some(executor.to_string());
self
}
pub fn timeout(mut self, timeout_secs: u64) -> Self {
self.timeout_secs = Some(timeout_secs);
self
}
pub fn build(self) -> Result<Job, JobError> {
let now = chrono::Utc::now().to_rfc3339();
Ok(Job {
id: self.id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
caller_id: self.caller_id.ok_or_else(|| JobError::Validation("caller_id is required".to_string()))?,
context_id: self.context_id.ok_or_else(|| JobError::Validation("context_id is required".to_string()))?,
payload: self.payload.ok_or_else(|| JobError::Validation("payload is required".to_string()))?,
runner: self.runner.ok_or_else(|| JobError::Validation("runner is required".to_string()))?,
executor: self.executor.ok_or_else(|| JobError::Validation("executor is required".to_string()))?,
timeout_secs: self.timeout_secs.unwrap_or(30),
env_vars: self.env_vars.unwrap_or_else(|| "{}".to_string()),
created_at: now.clone(),
updated_at: now,
})
}
}
/// Job structure for creating and managing jobs (alias for WasmJob)
pub type Job = WasmJob;
/// Job structure for creating and managing jobs
#[derive(Debug, Clone, Serialize, Deserialize)]
#[wasm_bindgen]