use clap::{Args, Subcommand};
use std::collections::HashMap;
use std::path::PathBuf;
use base64::{engine::general_purpose::STANDARD, Engine};
use invariant_robotics::authority::crypto::sign_pca;
use invariant_robotics::intent;
#[derive(Args)]
pub struct IntentArgs {
#[command(subcommand)]
pub mode: IntentMode,
}
#[derive(Subcommand)]
pub enum IntentMode {
Template(TemplateArgs),
Direct(DirectArgs),
ListTemplates,
}
#[derive(Args)]
pub struct TemplateArgs {
#[arg(long)]
pub template: String,
#[arg(long = "param", value_name = "KEY=VALUE")]
pub params: Vec<String>,
#[arg(long, default_value = "operator")]
pub principal: String,
#[arg(long, value_name = "KEY_FILE")]
pub key: PathBuf,
#[arg(long)]
pub duration: Option<f64>,
}
#[derive(Args)]
pub struct DirectArgs {
#[arg(long = "op", value_name = "OPERATION")]
pub ops: Vec<String>,
#[arg(long, default_value = "operator")]
pub principal: String,
#[arg(long, value_name = "KEY_FILE")]
pub key: PathBuf,
#[arg(long)]
pub duration: Option<f64>,
}
pub fn run(args: &IntentArgs) -> i32 {
match &args.mode {
IntentMode::Template(targs) => run_template(targs),
IntentMode::Direct(dargs) => run_direct(dargs),
IntentMode::ListTemplates => run_list_templates(),
}
}
fn run_list_templates() -> i32 {
let templates = intent::builtin_templates();
println!("Built-in task templates:");
for t in &templates {
println!(
" {} — {} (params: [{}], duration: {}s)",
t.name,
t.description,
t.required_params.join(", "),
t.default_duration_s
);
}
println!("\n{} templates available.", templates.len());
0
}
fn run_template(args: &TemplateArgs) -> i32 {
let template = match intent::find_template(&args.template) {
Some(t) => t,
None => {
eprintln!("error: unknown template '{}'. Use 'intent list-templates' to see available templates.", args.template);
return 2;
}
};
let mut params = HashMap::new();
for kv in &args.params {
let parts: Vec<&str> = kv.splitn(2, '=').collect();
if parts.len() != 2 {
eprintln!("error: invalid parameter format '{kv}'. Expected KEY=VALUE.");
return 2;
}
params.insert(parts[0].to_string(), parts[1].to_string());
}
let resolved = match intent::resolve_template(
&template,
¶ms,
&args.principal,
"", args.duration,
) {
Ok(r) => r,
Err(e) => {
eprintln!("error: {e}");
return 2;
}
};
sign_and_output(resolved, &args.key)
}
fn run_direct(args: &DirectArgs) -> i32 {
let resolved = match intent::resolve_direct(
&args.ops,
&args.principal,
"", args.duration,
) {
Ok(r) => r,
Err(e) => {
eprintln!("error: {e}");
return 2;
}
};
sign_and_output(resolved, &args.key)
}
fn sign_and_output(mut resolved: intent::ResolvedIntent, key_path: &std::path::Path) -> i32 {
let kf = match crate::key_file::load_key_file(key_path) {
Ok(kf) => kf,
Err(e) => {
eprintln!("error: {e}");
return 2;
}
};
let (sk, _vk, kid) = match crate::key_file::load_signing_key(&kf) {
Ok(t) => t,
Err(e) => {
eprintln!("error: {e}");
return 2;
}
};
resolved.kid = kid;
let pca = match intent::intent_to_pca(&resolved) {
Ok(p) => p,
Err(e) => {
eprintln!("error: {e}");
return 2;
}
};
let signed = match sign_pca(&pca, &sk) {
Ok(s) => s,
Err(e) => {
eprintln!("error: signing failed: {e}");
return 2;
}
};
let chain = vec![signed];
let chain_json = match serde_json::to_vec(&chain) {
Ok(j) => j,
Err(e) => {
eprintln!("error: serialization failed: {e}");
return 2;
}
};
let chain_b64 = STANDARD.encode(&chain_json);
println!("{chain_b64}");
eprintln!("intent: principal={}", resolved.principal);
eprintln!("intent: operations={:?}", resolved.operations);
eprintln!(
"intent: expiry={}",
resolved
.expiry
.map(|e| e.to_rfc3339())
.unwrap_or_else(|| "none".into())
);
eprintln!("intent: source={:?}", resolved.source);
0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_key_returns_2() {
let args = IntentArgs {
mode: IntentMode::Direct(DirectArgs {
ops: vec!["actuate:j1".into()],
principal: "test".into(),
key: PathBuf::from("/nonexistent/key.json"),
duration: None,
}),
};
assert_eq!(run(&args), 2);
}
#[test]
fn unknown_template_returns_2() {
let args = IntentArgs {
mode: IntentMode::Template(TemplateArgs {
template: "nonexistent".into(),
params: vec![],
principal: "test".into(),
key: PathBuf::from("/nonexistent/key.json"),
duration: None,
}),
};
assert_eq!(run(&args), 2);
}
#[test]
fn list_templates_returns_0() {
let args = IntentArgs {
mode: IntentMode::ListTemplates,
};
assert_eq!(run(&args), 0);
}
}