roboticus-cli 0.11.3

CLI commands and migration engine for the Roboticus agent runtime
Documentation
use super::*;

pub async fn cmd_schedule_list(url: &str, json: bool) -> Result<(), Box<dyn std::error::Error>> {
    let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
    let (OK, ACTION, WARN, DETAIL, ERR) = icons();
    let c = RoboticusClient::new(url)?;
    let data = c.get("/api/cron/jobs").await.map_err(|e| {
        RoboticusClient::check_connectivity_hint(&*e);
        e
    })?;
    if json {
        println!("{}", serde_json::to_string_pretty(&data)?);
        return Ok(());
    }
    heading("Cron Jobs");
    let jobs = data["jobs"].as_array();
    match jobs {
        Some(arr) if !arr.is_empty() => {
            let widths = [22, 24, 12, 22, 10, 8];
            table_header(
                &["Name", "Intent", "Schedule", "Last Run", "Status", "Errors"],
                &widths,
            );
            for j in arr {
                let name = j["name"].as_str().unwrap_or("").to_string();
                let intent = j["description"]
                    .as_str()
                    .map(|d| d.trim())
                    .filter(|d| !d.is_empty())
                    .unwrap_or("no description");
                let kind = j["schedule_kind"].as_str().unwrap_or("?");
                let expr = j["schedule_expr"].as_str().unwrap_or("");
                let schedule = format!("{kind}: {expr}");
                let last_run = j["last_run_at"]
                    .as_str()
                    .map(|t| if t.len() > 19 { &t[..19] } else { t })
                    .unwrap_or("never")
                    .to_string();
                let status = j["last_status"].as_str().unwrap_or("pending");
                let errors = j["consecutive_errors"].as_i64().unwrap_or(0);
                table_row(
                    &[
                        format!("{ACCENT}{name}{RESET}"),
                        format!("{DIM}{}{RESET}", truncate_id(intent, 24)),
                        truncate_id(&schedule, 12),
                        format!("{DIM}{last_run}{RESET}"),
                        status_badge(status),
                        if errors > 0 {
                            format!("{RED}{errors}{RESET}")
                        } else {
                            format!("{DIM}0{RESET}")
                        },
                    ],
                    &widths,
                );
            }
            eprintln!();
            eprintln!("    {DIM}{} job(s){RESET}", arr.len());
        }
        _ => empty_state("No cron jobs configured"),
    }
    eprintln!();
    Ok(())
}

pub async fn cmd_schedule_recover(
    url: &str,
    names: &[String],
    all: bool,
    dry_run: bool,
    json: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
    let (OK, ACTION, WARN, DETAIL, ERR) = icons();

    let c = RoboticusClient::new(url)?;
    let data = c.get("/api/cron/jobs").await.map_err(|e| {
        RoboticusClient::check_connectivity_hint(&*e);
        e
    })?;
    if json {
        println!("{}", serde_json::to_string_pretty(&data)?);
        return Ok(());
    }
    let jobs = data["jobs"].as_array().cloned().unwrap_or_default();

    let paused: Vec<Value> = jobs
        .into_iter()
        .filter(|j| j["last_status"].as_str() == Some("paused_unknown_action"))
        .collect();

    if paused.is_empty() {
        heading("Schedule Recovery");
        empty_state("No paused cron jobs found.");
        eprintln!();
        return Ok(());
    }

    let selected: Vec<Value> = if all {
        paused.clone()
    } else if !names.is_empty() {
        paused
            .iter()
            .filter(|j| {
                let job_name = j["name"].as_str().unwrap_or_default();
                names.iter().any(|n| n == job_name)
            })
            .cloned()
            .collect()
    } else {
        heading("Schedule Recovery");
        eprintln!("    {WARN} Found {} paused job(s).{RESET}", paused.len());
        eprintln!(
            "    {DIM}Use {BOLD}roboticus schedule recover --all{RESET}{DIM} to re-enable all, or {BOLD}--name <job>{RESET}{DIM} to select specific jobs.{RESET}"
        );
        eprintln!();
        let widths = [36, 10, 22];
        table_header(&["Name", "Enabled", "Last Run"], &widths);
        for j in &paused {
            let name = j["name"].as_str().unwrap_or("").to_string();
            let enabled = j["enabled"].as_bool().unwrap_or(false);
            let last_run = j["last_run_at"]
                .as_str()
                .map(|t| if t.len() > 19 { &t[..19] } else { t })
                .unwrap_or("never")
                .to_string();
            table_row(
                &[
                    format!("{ACCENT}{name}{RESET}"),
                    if enabled {
                        format!("{GREEN}true{RESET}")
                    } else {
                        format!("{YELLOW}false{RESET}")
                    },
                    format!("{DIM}{last_run}{RESET}"),
                ],
                &widths,
            );
        }
        eprintln!();
        return Ok(());
    };

    if selected.is_empty() {
        heading("Schedule Recovery");
        eprintln!("    {ERR} No paused jobs matched the provided name filter.{RESET}");
        eprintln!();
        return Ok(());
    }

    heading("Schedule Recovery");
    eprintln!(
        "    {ACTION} {} job(s) selected for re-enable{}",
        selected.len(),
        if dry_run { " (dry-run)" } else { "" }
    );
    eprintln!();

    let widths = [36, 12, 10];
    table_header(&["Name", "Job ID", "Result"], &widths);

    for j in selected {
        let id = j["id"].as_str().unwrap_or_default();
        let name = j["name"].as_str().unwrap_or_default();
        let result = if dry_run {
            format!("{CYAN}would-enable{RESET}")
        } else {
            match c
                .put(
                    &format!("/api/cron/jobs/{id}"),
                    serde_json::json!({ "enabled": true }),
                )
                .await
            {
                Ok(_) => format!("{GREEN}enabled{RESET}"),
                Err(e) => format!("{RED}failed: {}{RESET}", truncate_id(&e.to_string(), 40)),
            }
        };
        table_row(
            &[
                format!("{ACCENT}{name}{RESET}"),
                format!("{MONO}{}{RESET}", truncate_id(id, 12)),
                result,
            ],
            &widths,
        );
    }
    eprintln!();
    Ok(())
}

pub async fn cmd_schedule_run(
    url: &str,
    name_or_id: &str,
    json: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let (_DIM, _BOLD, ACCENT, GREEN, _YELLOW, RED, _CYAN, RESET, _MONO) = colors();
    let (_OK, ACTION, _WARN, _DETAIL, _ERR) = icons();
    let c = RoboticusClient::new(url)?;
    let data = c.get("/api/cron/jobs").await?;
    let jobs = data["jobs"].as_array().cloned().unwrap_or_default();
    let Some(job) = jobs
        .into_iter()
        .find(|j| j["id"].as_str() == Some(name_or_id) || j["name"].as_str() == Some(name_or_id))
    else {
        return Err(format!("cron job not found: {name_or_id}").into());
    };
    let id = job["id"].as_str().unwrap_or_default();
    let name = job["name"].as_str().unwrap_or_default();
    heading("Schedule Run");
    eprintln!("    {ACTION} Running {ACCENT}{name}{RESET} now...");
    let body = c
        .post(&format!("/api/cron/jobs/{id}/run"), serde_json::json!({}))
        .await?;
    if json {
        println!("{}", serde_json::to_string_pretty(&body)?);
        return Ok(());
    }
    let status = body["status"].as_str().unwrap_or("unknown");
    let error = body["detail"].as_str().unwrap_or("");
    let output_text = body["output_text"].as_str().unwrap_or("").trim();
    if status == "success" {
        eprintln!("    {GREEN}Ran successfully{RESET}");
        if !output_text.is_empty() {
            eprintln!("    Output");
            eprintln!("      {}", output_text.replace('\n', "\n      "));
        }
    } else {
        eprintln!("    {RED}Run failed:{RESET} {}", error);
    }
    eprintln!();
    Ok(())
}