xbp 10.32.0

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
use super::project::{
    discover_worker_apps, label_for_script, resolve_project_script_names,
    script_belongs_to_project, DiscoveredWorkerApp,
};
use super::{build_cloudflare_client, load_local_env};
use crate::cli::commands::WorkersListCmd;
use crate::provider_support::{CloudflareClient, CloudflareWorkerBuild, CloudflareWorkerScript};
use colored::Colorize;
use serde_json::json;
use std::collections::HashMap;
use std::env;
use std::path::Path;

pub async fn run_worker_list(
    root_override: Option<&Path>,
    token_override: Option<&str>,
    account_id_override: Option<&str>,
    cmd: WorkersListCmd,
) -> Result<(), String> {
    let start = root_override
        .map(Path::to_path_buf)
        .or_else(|| env::current_dir().ok())
        .ok_or_else(|| "Failed to resolve current directory.".to_string())?;

    let apps = discover_worker_apps(&start);
    let project_scripts = resolve_project_script_names(&apps);
    let local_env = apps
        .first()
        .map(|app| load_local_env(&app.root))
        .transpose()?
        .unwrap_or_default();
    let client = build_cloudflare_client(token_override, account_id_override, &local_env)?;

    let mut scripts = client.list_worker_scripts().await?;
    scripts.sort_by(|left, right| left.id.cmp(&right.id));

    let in_project = !project_scripts.is_empty();
    if in_project && !cmd.all {
        scripts.retain(|script| script_belongs_to_project(&script.id, &project_scripts));
    }

    let latest_builds = fetch_latest_build_statuses(&client, &scripts).await;

    if cmd.json {
        return print_json(&json!({
            "workers": scripts,
            "latest_builds": latest_builds,
            "project_apps": apps.iter().map(|app| json!({
                "label": app.label,
                "root": app.root,
                "base_script_name": app.base_script_name,
            })).collect::<Vec<_>>(),
            "scope": if cmd.all || !in_project { "account" } else { "project" },
        }));
    }

    if scripts.is_empty() {
        if in_project && !cmd.all {
            println!(
                "{}",
                "No deployed Workers matched this XBP project. Use --all to list every Worker in the account.".dimmed()
            );
        } else {
            println!(
                "{}",
                "No Workers found in this Cloudflare account.".dimmed()
            );
        }
        return Ok(());
    }

    print_workers_table(&scripts, &apps, &latest_builds, in_project && !cmd.all);
    if in_project && !cmd.all {
        println!(
            "\n{} {}",
            "Showing Workers for this XBP project.".dimmed(),
            "Pass --all to include every account Worker.".dimmed()
        );
    }
    Ok(())
}

async fn fetch_latest_build_statuses(
    client: &CloudflareClient,
    scripts: &[CloudflareWorkerScript],
) -> HashMap<String, CloudflareWorkerBuild> {
    let mut latest = HashMap::new();
    for script in scripts {
        let Some(tag) = script.tag.as_deref().filter(|value| !value.is_empty()) else {
            continue;
        };
        let Ok(builds) = client.list_worker_builds(tag).await else {
            continue;
        };
        if let Some(build) = builds.into_iter().next() {
            latest.insert(script.id.clone(), build);
        }
    }
    latest
}

fn print_workers_table(
    scripts: &[CloudflareWorkerScript],
    apps: &[DiscoveredWorkerApp],
    latest_builds: &HashMap<String, CloudflareWorkerBuild>,
    project_scope: bool,
) {
    let headers = ["SCRIPT", "APP", "BUILD", "PLACEMENT", "MODIFIED", "ROUTES"];
    let rows: Vec<[String; 6]> = scripts
        .iter()
        .map(|script| {
            let app = label_for_script(&script.id, apps).unwrap_or_else(|| {
                if project_scope {
                    "".to_string()
                } else {
                    "".to_string()
                }
            });
            let build_status = latest_builds
                .get(&script.id)
                .and_then(|build| build.status.clone())
                .unwrap_or_else(|| "".to_string());
            let placement = script
                .placement_status
                .clone()
                .unwrap_or_else(|| "".to_string());
            let modified = format_timestamp(script.modified_on.as_deref());
            let routes = script
                .routes
                .as_ref()
                .map(|routes| routes.len().to_string())
                .unwrap_or_else(|| "".to_string());
            [
                script.id.clone(),
                app,
                build_status,
                placement,
                modified,
                routes,
            ]
        })
        .collect();

    print_table(&headers, &rows, |row, column| match column {
        2 => color_build_status(&row[2], row[2].len()),
        3 => color_placement_status(&row[3], row[3].len()),
        0 => pad(&row[0], row[0].len()).cyan(),
        1 => pad(&row[1], row[1].len()).green(),
        _ => pad(&row[column], row[column].len()).normal(),
    });
}

fn format_timestamp(value: Option<&str>) -> String {
    let Some(raw) = value.map(str::trim).filter(|value| !value.is_empty()) else {
        return "".to_string();
    };
    if raw.len() >= 16 {
        raw[..16].replace('T', " ")
    } else {
        raw.to_string()
    }
}

fn print_table<'a, F>(headers: &[&str; 6], rows: &[[String; 6]], color_cell: F)
where
    F: Fn(&[String; 6], usize) -> colored::ColoredString,
{
    let mut widths = [0usize; 6];
    for (index, header) in headers.iter().enumerate() {
        widths[index] = header.len();
    }
    for row in rows {
        for (index, value) in row.iter().enumerate() {
            widths[index] = widths[index].max(value.len());
        }
    }

    let top = make_line('', '', '', &widths);
    let mid = make_line('', '', '', &widths);
    let bottom = make_line('', '', '', &widths);

    println!("{}", top);
    println!(
        "{}{}{}{}{}{}",
        pad(headers[0], widths[0]).bold().white(),
        pad(headers[1], widths[1]).bold().white(),
        pad(headers[2], widths[2]).bold().white(),
        pad(headers[3], widths[3]).bold().white(),
        pad(headers[4], widths[4]).bold().white(),
        pad(headers[5], widths[5]).bold().white()
    );
    println!("{}", mid);
    for row in rows {
        println!(
            "{}{}{}{}{}{}",
            color_cell(row, 0),
            color_cell(row, 1),
            color_cell(row, 2),
            color_cell(row, 3),
            color_cell(row, 4),
            color_cell(row, 5)
        );
    }
    println!("{}", bottom);
}

fn make_line(left: char, mid: char, right: char, widths: &[usize; 6]) -> String {
    let mut parts = Vec::with_capacity(13);
    for (index, width) in widths.iter().enumerate() {
        let fill = "".repeat(*width + 2);
        if index == 0 {
            parts.push(format!("{left}{fill}"));
        } else {
            parts.push(format!("{mid}{fill}"));
        }
    }
    parts.push(right.to_string());
    parts.join("")
}

fn pad(value: impl AsRef<str>, width: usize) -> String {
    format!("{:width$}", value.as_ref(), width = width)
}

fn color_build_status(status: &str, width: usize) -> colored::ColoredString {
    let base = pad(status, width);
    match status.to_ascii_lowercase().as_str() {
        "success" | "succeeded" | "deployed" | "active" => base.green(),
        "failed" | "failure" | "error" | "cancelled" | "canceled" => base.red(),
        "running" | "building" | "queued" | "pending" | "in_progress" => base.yellow(),
        _ => base.normal(),
    }
}

fn color_placement_status(status: &str, width: usize) -> colored::ColoredString {
    let base = pad(status, width);
    match status.to_ascii_lowercase().as_str() {
        "active" | "success" => base.green(),
        "failed" | "error" => base.red(),
        _ => base.normal(),
    }
}

fn print_json(value: &impl serde::Serialize) -> Result<(), String> {
    println!(
        "{}",
        serde_json::to_string_pretty(value)
            .map_err(|error| format!("Failed to encode JSON output: {}", error))?
    );
    Ok(())
}