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