xbp 10.33.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::render::{
    color_enabled, color_status_text, format_timestamp, pad, print_unicode_table,
    status_matches_filter, summarize_status_counts, truncate_middle,
};
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;
    let status_filter = resolve_status_filter(&cmd);
    if let Some(filter) = status_filter.as_deref() {
        scripts.retain(|script| {
            let status = latest_builds
                .get(&script.id)
                .and_then(|build| build.status.clone())
                .unwrap_or_else(|| "".to_string());
            status_matches_filter(&status, filter)
        });
    }

    sort_scripts(&mut scripts, &latest_builds, &cmd.sort);
    if let Some(limit) = cmd.limit {
        scripts.truncate(limit);
    }

    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" },
            "status_filter": status_filter,
            "limit": cmd.limit,
            "sort": cmd.sort,
        }));
    }

    if scripts.is_empty() {
        if status_filter.is_some() {
            println!(
                "{}",
                "No Workers matched the requested status filter.".dimmed()
            );
        } else 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(());
    }

    let color = color_enabled(cmd.no_color);
    print_workers_table(&scripts, &apps, &latest_builds, in_project && !cmd.all, color);

    let statuses: Vec<String> = scripts
        .iter()
        .map(|script| {
            latest_builds
                .get(&script.id)
                .and_then(|build| build.status.clone())
                .unwrap_or_else(|| "".to_string())
        })
        .collect();
    println!(
        "\n{}",
        summarize_status_counts(&statuses).dimmed()
    );

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

fn resolve_status_filter(cmd: &WorkersListCmd) -> Option<String> {
    if cmd.failed {
        return Some("failed".to_string());
    }
    cmd.status
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(str::to_ascii_lowercase)
}

fn sort_scripts(
    scripts: &mut [CloudflareWorkerScript],
    latest_builds: &HashMap<String, CloudflareWorkerBuild>,
    sort: &str,
) {
    match sort.trim().to_ascii_lowercase().as_str() {
        "modified" => scripts.sort_by(|left, right| {
            right
                .modified_on
                .as_deref()
                .unwrap_or_default()
                .cmp(left.modified_on.as_deref().unwrap_or_default())
        }),
        "status" => scripts.sort_by(|left, right| {
            let left_status = latest_builds
                .get(&left.id)
                .and_then(|build| build.status.clone())
                .unwrap_or_else(|| "".to_string());
            let right_status = latest_builds
                .get(&right.id)
                .and_then(|build| build.status.clone())
                .unwrap_or_else(|| "".to_string());
            left_status
                .to_ascii_lowercase()
                .cmp(&right_status.to_ascii_lowercase())
                .then_with(|| left.id.cmp(&right.id))
        }),
        _ => scripts.sort_by(|left, right| left.id.cmp(&right.id)),
    }
}

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,
    color: bool,
) {
    let headers = [
        "SCRIPT",
        "APP",
        "BUILD",
        "BRANCH",
        "PLACEMENT",
        "MODIFIED",
        "ROUTES",
    ];
    let rows: Vec<Vec<String>> = 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 = latest_builds.get(&script.id);
            let build_status = build
                .and_then(|entry| entry.status.clone())
                .unwrap_or_else(|| "".to_string());
            let branch = build
                .and_then(|entry| entry.branch.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());
            vec![
                truncate_middle(&script.id, 36),
                app,
                build_status,
                truncate_middle(&branch, 20),
                placement,
                modified,
                routes,
            ]
        })
        .collect();

    print_unicode_table(&headers, &rows, color, |row, column, color| match column {
        0 => {
            let padded = pad(&row[0], row[0].len());
            if color {
                padded.cyan().to_string()
            } else {
                padded
            }
        }
        1 => {
            let padded = pad(&row[1], row[1].len());
            if color {
                padded.green().to_string()
            } else {
                padded
            }
        }
        2 => color_status_text(&row[2], row[2].len(), color),
        4 => color_status_text(&row[4], row[4].len(), color),
        _ => pad(&row[column], row[column].len()),
    });
}

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(())
}