use std::time::Duration;
use lifeloop::router::{
BuiltinAdapterRegistry, CapabilityRequest, LifeloopReceiptEmitter, ReceiptContext,
SubprocessCallbackInvoker, SubprocessInvokerConfig, negotiate, route,
};
use lifeloop::{CallbackResponse, DispatchEnvelope};
use super::{CliError, parse_stdin_json, print_json};
pub fn run<I: Iterator<Item = String>>(mut args: I) -> Result<(), CliError> {
let action = args
.next()
.ok_or_else(|| CliError::Usage("event requires a subcommand: invoke".to_string()))?;
match action.as_str() {
"invoke" => run_invoke(args),
other => Err(CliError::Usage(format!(
"event: unknown subcommand `{other}` (expected: invoke)"
))),
}
}
#[derive(Debug)]
struct InvokeArgs {
client_cmd: Option<String>,
client_args: Vec<String>,
timeout_ms: u64,
client_id: String,
receipt_id: String,
at_epoch_s: u64,
in_process: bool,
}
impl InvokeArgs {
fn parse<I: Iterator<Item = String>>(mut args: I) -> Result<Self, CliError> {
let mut parsed = Self {
client_cmd: None,
client_args: Vec::new(),
timeout_ms: 5_000,
client_id: "lifeloop-cli".to_string(),
receipt_id: "rcpt-cli".to_string(),
at_epoch_s: 0,
in_process: false,
};
while let Some(arg) = args.next() {
match arg.as_str() {
"--client-cmd" => {
parsed.client_cmd = Some(require_value(&arg, args.next())?);
}
"--client-arg" => {
parsed.client_args.push(require_value(&arg, args.next())?);
}
"--timeout-ms" => {
let value = require_value(&arg, args.next())?;
parsed.timeout_ms = parse_u64_flag("--timeout-ms", &value)?;
}
"--client-id" => {
parsed.client_id = require_value(&arg, args.next())?;
}
"--receipt-id" => {
parsed.receipt_id = require_value(&arg, args.next())?;
}
"--at-epoch-s" => {
let value = require_value(&arg, args.next())?;
parsed.at_epoch_s = parse_u64_flag("--at-epoch-s", &value)?;
}
"--in-process" => {
parsed.in_process = true;
}
other => {
return Err(CliError::Usage(format!(
"event invoke: unknown flag `{other}`"
)));
}
}
}
if !parsed.in_process && parsed.client_cmd.is_none() {
return Err(CliError::Usage(
"event invoke: --client-cmd <path> is required (or pass --in-process)".into(),
));
}
Ok(parsed)
}
fn receipt_context(&self, request: &lifeloop::CallbackRequest) -> ReceiptContext {
ReceiptContext {
client_id: self.client_id.clone(),
receipt_id: self.receipt_id.clone(),
parent_receipt_id: None,
at_epoch_s: self.at_epoch_s,
harness_session_id: request.harness_session_id.clone(),
harness_run_id: request.harness_run_id.clone(),
harness_task_id: request.harness_task_id.clone(),
}
}
fn invoke_client(
&self,
plan: &lifeloop::router::RoutingPlan,
payloads: &[lifeloop::PayloadEnvelope],
) -> Result<CallbackResponse, CliError> {
if self.in_process {
let response = CallbackResponse::ok(lifeloop::ReceiptStatus::Delivered);
response.validate().map_err(|e| {
CliError::Validation(format!("in-process response failed validation: {e}"))
})?;
return Ok(response);
}
let mut config = SubprocessInvokerConfig::new(
self.client_cmd.as_ref().expect("checked by parse"),
Duration::from_millis(self.timeout_ms),
);
config = config.args(self.client_args.iter().cloned());
let invoker = SubprocessCallbackInvoker::new(config);
use lifeloop::router::CallbackInvoker;
invoker
.invoke(plan, payloads)
.map_err(|e| CliError::Validation(format!("subprocess callback failed: {e}")))
}
}
fn run_invoke<I: Iterator<Item = String>>(args: I) -> Result<(), CliError> {
let args = InvokeArgs::parse(args)?;
let envelope: DispatchEnvelope = parse_stdin_json("DispatchEnvelope")?;
envelope
.validate()
.map_err(|e| CliError::Validation(format!("DispatchEnvelope failed validation: {e}")))?;
let registry = BuiltinAdapterRegistry;
let plan = route(&envelope.request, ®istry)
.map_err(|e| CliError::Validation(format!("router rejected request: {e}")))?;
let cap_request = CapabilityRequest::new();
let negotiated = negotiate(&plan, &cap_request, envelope.payloads.as_slice());
let response = if negotiated.blocks_dispatch() {
CallbackResponse::ok(lifeloop::ReceiptStatus::Failed)
} else {
args.invoke_client(&plan, envelope.payloads.as_slice())?
};
let ctx = args.receipt_context(&envelope.request);
let emitter = LifeloopReceiptEmitter::in_memory();
let receipt = emitter
.synthesize_and_emit(&negotiated, &response, &ctx)
.map_err(|e| CliError::Validation(format!("receipt emission failed: {e}")))?;
print_json(&receipt)
}
fn parse_u64_flag(flag: &str, value: &str) -> Result<u64, CliError> {
value
.parse::<u64>()
.map_err(|e| CliError::Usage(format!("{flag} must be a non-negative integer: {e}")))
}
fn require_value(flag: &str, value: Option<String>) -> Result<String, CliError> {
value.ok_or_else(|| CliError::Usage(format!("flag `{flag}` requires a value")))
}