use super::project::{
discover_worker_apps, resolve_project_script_names, script_belongs_to_project,
worker_root_for_script, DiscoveredWorkerApp,
};
use super::wrangler::run_wrangler;
use super::{build_cloudflare_client, load_local_env, resolve_target_script_name};
use crate::cli::commands::WorkersLogsCmd;
use crate::provider_support::{CloudflareClient, CloudflareWorkerScript};
use colored::Colorize;
use dialoguer::{theme::ColorfulTheme, FuzzySelect};
use serde_json::Value;
use std::env;
use std::path::Path;
pub async fn run_worker_logs(
root_override: Option<&Path>,
token_override: Option<&str>,
account_id_override: Option<&str>,
cmd: WorkersLogsCmd,
) -> 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 scripts = client.list_worker_scripts().await?;
let scoped_scripts = filter_scripts(&scripts, &project_scripts);
let script_name = resolve_script_name(
&start,
&apps,
&cmd,
&scoped_scripts,
&scripts,
&project_scripts,
)?;
if cmd.build {
return show_build_logs(&client, &scripts, &script_name, &cmd).await;
}
let worker_root = worker_root_for_script(&script_name, &apps)
.or_else(|| apps.first().map(|app| app.root.as_path()))
.ok_or_else(|| {
format!(
"Could not resolve a Workers project root for `{}`. Pass `xbp workers --root <path> logs`.",
script_name
)
})?;
if cmd.follow {
tail_runtime_logs(worker_root, &script_name, &cmd)?;
return Ok(());
}
show_deployment_status(worker_root, &script_name)
}
fn filter_scripts<'a>(
scripts: &'a [CloudflareWorkerScript],
project_scripts: &[String],
) -> Vec<&'a CloudflareWorkerScript> {
if project_scripts.is_empty() {
return scripts.iter().collect();
}
scripts
.iter()
.filter(|script| script_belongs_to_project(&script.id, project_scripts))
.collect()
}
fn resolve_script_name(
start: &Path,
apps: &[DiscoveredWorkerApp],
cmd: &WorkersLogsCmd,
scoped_scripts: &[&CloudflareWorkerScript],
all_scripts: &[CloudflareWorkerScript],
project_scripts: &[String],
) -> Result<String, String> {
if let Some(script) = cmd
.target
.script
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
return Ok(script.to_string());
}
if let Some(script) = cmd
.script_name
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
return Ok(script.to_string());
}
if let Some(worker_root) = apps.first().map(|app| app.root.as_path()) {
let local_env = load_local_env(worker_root).unwrap_or_default();
let resolved = resolve_target_script_name(worker_root, &cmd.target, &local_env);
if all_scripts.iter().any(|script| script.id == resolved) {
return Ok(resolved);
}
} else if let Ok(worker_root) = super::project::resolve_workers_project_root(Some(start)) {
let local_env = load_local_env(&worker_root).unwrap_or_default();
let resolved = resolve_target_script_name(&worker_root, &cmd.target, &local_env);
if all_scripts.iter().any(|script| script.id == resolved) {
return Ok(resolved);
}
}
let candidates: Vec<&CloudflareWorkerScript> = if scoped_scripts.is_empty() {
all_scripts.iter().collect()
} else {
scoped_scripts.to_vec()
};
if candidates.is_empty() {
return Err("No Workers found in this Cloudflare account.".to_string());
}
if candidates.len() == 1 {
return Ok(candidates[0].id.clone());
}
let labels: Vec<String> = candidates
.iter()
.map(|script| format_worker_choice(script, project_scripts))
.collect();
let selection = FuzzySelect::with_theme(&ColorfulTheme::default())
.with_prompt("Select a Worker to inspect")
.items(&labels)
.default(0)
.interact()
.map_err(|error| format!("Failed to run Worker selection prompt: {}", error))?;
Ok(candidates[selection].id.clone())
}
fn format_worker_choice(script: &CloudflareWorkerScript, project_scripts: &[String]) -> String {
let scope =
if project_scripts.is_empty() || script_belongs_to_project(&script.id, project_scripts) {
"project"
} else {
"account"
};
format!("{} ({scope})", script.id)
}
async fn show_build_logs(
client: &CloudflareClient,
scripts: &[CloudflareWorkerScript],
script_name: &str,
cmd: &WorkersLogsCmd,
) -> Result<(), String> {
let script = scripts
.iter()
.find(|script| script.id == script_name)
.ok_or_else(|| format!("Worker script `{}` was not found.", script_name))?;
let tag = script
.tag
.as_deref()
.filter(|value| !value.is_empty())
.ok_or_else(|| {
format!(
"Worker `{}` has no build tag. Workers Builds may not be configured for it.",
script_name
)
})?;
let builds = client.list_worker_builds(tag).await?;
if builds.is_empty() {
println!(
"{}",
format!("No Workers Builds history found for `{}`.", script_name).dimmed()
);
return Ok(());
}
let build = if cmd.failed {
builds
.iter()
.find(|build| is_failed_build_status(build.status.as_deref()))
.or_else(|| builds.first())
} else {
builds.first()
}
.ok_or_else(|| format!("No build records found for `{}`.", script_name))?;
let status = build
.status
.clone()
.unwrap_or_else(|| "unknown".to_string());
println!(
"{} {} {} {}",
"Build".bold(),
build.build_uuid.cyan(),
status_color(&status),
build
.branch
.as_deref()
.map(|branch| format!("branch={branch}"))
.unwrap_or_default()
.dimmed()
);
let logs = client.get_worker_build_logs(&build.build_uuid).await?;
if cmd.json {
println!(
"{}",
serde_json::to_string_pretty(&logs)
.map_err(|error| format!("Failed to encode build logs: {}", error))?
);
return Ok(());
}
print_build_logs(&logs);
if is_failed_build_status(build.status.as_deref()) {
println!(
"\n{}",
"Latest matching build failed. Use `xbp workers logs --build --failed` after a new failure to jump to it.".yellow()
);
}
Ok(())
}
fn is_failed_build_status(status: Option<&str>) -> bool {
matches!(
status.map(str::to_ascii_lowercase).as_deref(),
Some("failed") | Some("failure") | Some("error") | Some("cancelled") | Some("canceled")
)
}
fn status_color(status: &str) -> colored::ColoredString {
match status.to_ascii_lowercase().as_str() {
"success" | "succeeded" | "deployed" | "active" => status.green(),
"failed" | "failure" | "error" | "cancelled" | "canceled" => status.red(),
"running" | "building" | "queued" | "pending" | "in_progress" => status.yellow(),
_ => status.normal(),
}
}
fn print_build_logs(value: &Value) {
match value {
Value::String(text) if !text.trim().is_empty() => println!("{text}"),
Value::Array(lines) => {
for line in lines {
match line {
Value::String(text) => println!("{text}"),
other => println!("{other}"),
}
}
}
Value::Object(map) => {
if let Some(Value::String(text)) = map.get("logs") {
println!("{text}");
return;
}
if let Some(Value::Array(lines)) = map.get("logs") {
for line in lines {
match line {
Value::String(text) => println!("{text}"),
Value::Object(entry) => {
let message = entry
.get("message")
.or_else(|| entry.get("text"))
.or_else(|| entry.get("line"))
.and_then(Value::as_str)
.unwrap_or("");
if !message.is_empty() {
println!("{message}");
}
}
other => println!("{other}"),
}
}
return;
}
println!(
"{}",
serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())
);
}
Value::Null => println!("{}", "No build logs were returned.".dimmed()),
other => println!("{other}"),
}
}
fn tail_runtime_logs(
worker_root: &Path,
script_name: &str,
cmd: &WorkersLogsCmd,
) -> Result<(), String> {
let mut args = vec!["tail".to_string(), script_name.to_string()];
if let Some(environment) = cmd
.target
.environment
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
args.push("--env".to_string());
args.push(environment.to_string());
}
args.push("--format".to_string());
args.push(if cmd.json {
"json".to_string()
} else {
"pretty".to_string()
});
println!(
"{} {} {}",
"Tailing runtime logs for".dimmed(),
script_name.cyan().bold(),
"(Ctrl+C to stop)".dimmed()
);
run_wrangler(worker_root, &args)
}
fn show_deployment_status(worker_root: &Path, script_name: &str) -> Result<(), String> {
println!(
"{} {}",
"Recent deployments for".dimmed(),
script_name.cyan().bold()
);
let args = vec![
"deployments".to_string(),
"list".to_string(),
"--name".to_string(),
script_name.to_string(),
];
run_wrangler(worker_root, &args)?;
println!(
"\n{}",
"Use `xbp workers logs -f` to stream runtime logs, or `xbp workers logs --build` for Workers Builds CI output.".dimmed()
);
Ok(())
}