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