use clap::{Parser, Subcommand};
use chrono::{DateTime, Duration, Utc};
use std::path::PathBuf;
use t_minus::{AgentId, Engine, EventKind};
#[derive(Parser)]
#[command(name = "t-minus", about = "T-minus event coordination for multi-agent systems")]
struct Cli {
#[arg(long, default_value = "tminus.db")]
db: PathBuf,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Schedule {
kind: String,
time: String,
#[arg(long, default_value = "1")]
quorum: usize,
#[arg(long, value_delimiter = ',')]
attendees: Vec<String>,
#[arg(long, default_value = "{}")]
payload: String,
},
Confirm {
event_id: String,
agent_id: String,
},
Defer {
event_id: String,
agent_id: String,
duration: String,
},
Status,
Tick,
Campaign {
#[command(subcommand)]
action: CampaignCommands,
},
}
#[derive(Subcommand)]
enum CampaignCommands {
Create {
name: String,
},
Add {
campaign_id: String,
kind: String,
time: String,
},
Link {
campaign_id: String,
from_event: String,
to_event: String,
},
Order {
campaign_id: String,
},
List,
}
fn parse_duration(s: &str) -> Result<Duration, String> {
let s = s.trim();
if s.ends_with('s') {
let secs: i64 = s.trim_end_matches('s').parse().map_err(|_| format!("invalid seconds: {s}"))?;
Ok(Duration::seconds(secs))
} else if s.ends_with('m') {
let mins: i64 = s.trim_end_matches('m').parse().map_err(|_| format!("invalid minutes: {s}"))?;
Ok(Duration::minutes(mins))
} else if s.ends_with('h') {
let hrs: i64 = s.trim_end_matches('h').parse().map_err(|_| format!("invalid hours: {s}"))?;
Ok(Duration::hours(hrs))
} else if s.ends_with('d') {
let days: i64 = s.trim_end_matches('d').parse().map_err(|_| format!("invalid days: {s}"))?;
Ok(Duration::days(days))
} else {
let secs: i64 = s.parse().map_err(|_| format!("invalid duration: {s} (use 60s, 5m, 1h)"))?;
Ok(Duration::seconds(secs))
}
}
fn parse_time(s: &str) -> Result<DateTime<Utc>, String> {
if s.starts_with('+') {
let dur = parse_duration(&s[1..])?;
Ok(Utc::now() + dur)
} else {
s.parse::<DateTime<Utc>>().map_err(|e| format!("invalid time '{s}': {e}"))
}
}
fn parse_uuid(s: &str) -> Result<uuid::Uuid, String> {
s.parse().map_err(|e| format!("invalid UUID '{s}': {e}"))
}
fn main() {
let cli = Cli::parse();
if let Err(e) = run(cli) {
eprintln!("error: {e}");
std::process::exit(1);
}
}
fn run(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
let mut engine = Engine::new(&cli.db)?;
match cli.command {
Commands::Schedule { kind, time, quorum, attendees, payload } => {
let kind = kind.parse::<EventKind>().map_err(|e: String| Box::new(std::io::Error::new(std::io::ErrorKind::InvalidInput, e)) as Box<dyn std::error::Error>)?;
let scheduled_at = parse_time(&time)?;
let t_minus = Duration::zero(); let attendees: Vec<AgentId> = attendees.into_iter().map(AgentId).collect();
let payload_val: serde_json::Value = serde_json::from_str(&payload)?;
let event = engine.schedule_event(kind, scheduled_at, t_minus, AgentId("cli".into()), attendees, quorum, payload_val)?;
println!("✅ Scheduled event {}", event.id);
println!(" Kind: {}, Fire at: {}", event.kind, event.fire_time().to_rfc3339());
println!(" Quorum: {}/{}", 0, event.quorum);
}
Commands::Confirm { event_id, agent_id } => {
let eid = parse_uuid(&event_id)?;
let agent = AgentId(agent_id);
let event = engine.confirm(eid, &agent)?;
println!("✅ {} confirmed for event {}", agent, event.id);
println!(" Quorum: {}/{}", event.confirmed_count(), event.quorum);
}
Commands::Defer { event_id, agent_id, duration } => {
let eid = parse_uuid(&event_id)?;
let agent = AgentId(agent_id);
let dur = parse_duration(&duration)?;
let event = engine.defer(eid, &agent, dur)?;
println!("⏳ {} deferred by {} for event {}", agent, duration, event.id);
}
Commands::Status => {
let events = engine.list_events()?;
let now = Utc::now();
if events.is_empty() {
println!("No pending events.");
}
for event in events {
let remaining = event.time_remaining(now);
let status = if remaining <= Duration::zero() {
"READY".to_string()
} else {
format!("T-{}m{}s", remaining.num_minutes(), (remaining - Duration::minutes(remaining.num_minutes())).num_seconds())
};
let quorum_bar = format!("{}/{}", event.confirmed_count(), event.quorum);
println!("{} {} [{}] quorum:{} attendees:{}",
status,
event.kind,
event.id,
quorum_bar,
event.attendees.len(),
);
for (agent, resp) in &event.attendees {
println!(" {} {}", agent, resp);
}
}
}
Commands::Tick => {
let now = Utc::now();
let result = engine.tick(now)?;
for id in &result.fired {
println!("🚀 Event {} fired! (quorum reached)", id);
}
for id in &result.missed {
println!("❌ Event {} missed (quorum not reached)", id);
}
if result.fired.is_empty() && result.missed.is_empty() {
println!("Nothing to process.");
}
}
Commands::Campaign { action } => match action {
CampaignCommands::Create { name } => {
let campaign = engine.create_campaign(name)?;
println!("✅ Campaign '{}' created: {}", campaign.name, campaign.id);
}
CampaignCommands::Add { campaign_id, kind, time } => {
let cid = parse_uuid(&campaign_id)?;
let event_kind = kind.parse::<EventKind>().map_err(|e: String| Box::new(std::io::Error::new(std::io::ErrorKind::InvalidInput, e)) as Box<dyn std::error::Error>)?;
let scheduled_at = parse_time(&time)?;
let event = engine.schedule_event(event_kind, scheduled_at, Duration::zero(), AgentId("cli".into()), vec![], 1, serde_json::Value::Null)?;
engine.campaign_add_event(cid, event.id)?;
println!("✅ Event {} added to campaign {}", event.id, cid);
}
CampaignCommands::Link { campaign_id, from_event, to_event } => {
let cid = parse_uuid(&campaign_id)?;
let from = parse_uuid(&from_event)?;
let to = parse_uuid(&to_event)?;
engine.campaign_link(cid, from, to)?;
println!("✅ Linked {} → {} in campaign {}", from_event, to_event, campaign_id);
}
CampaignCommands::Order { campaign_id } => {
let cid = parse_uuid(&campaign_id)?;
let order = engine.campaign_execution_order(cid)?;
println!("Execution order:");
for (i, id) in order.iter().enumerate() {
println!(" {}. {}", i + 1, id);
}
}
CampaignCommands::List => {
let campaigns = engine.list_campaigns()?;
if campaigns.is_empty() {
println!("No campaigns.");
}
for c in campaigns {
println!("{}: {} ({} events, {} deps)",
c.id, c.name, c.events.len(), c.dependencies.len());
}
}
},
}
Ok(())
}