mod deploy;
mod project;
mod worktree;
mod wrangler;
use crate::cli::commands::{
WorkersCmd, WorkersDeploySubCommand, WorkersSecretsBulkCmd, WorkersSecretsPutCmd,
WorkersSecretsSubCommand, WorkersSubCommand, WorkersTargetArgs, WorkersWorktreeSubCommand,
WorkersWranglerSubCommand,
};
use crate::config::{resolve_cloudflare_account_id, resolve_cloudflare_api_token};
use crate::provider_support::CloudflareClient;
use serde_json::{json, Map, Value};
use std::fs;
use std::io::{self, Read};
use std::path::Path;
pub async fn run_workers(cmd: WorkersCmd, _debug: bool) -> Result<(), String> {
let worker_root = project::resolve_workers_project_root(cmd.root.as_deref())?;
match cmd.command {
WorkersSubCommand::Secrets(secrets_cmd) => {
run_worker_secrets(
&worker_root,
cmd.token.as_deref(),
cmd.account_id.as_deref(),
secrets_cmd.target,
secrets_cmd.command,
)
.await
}
WorkersSubCommand::Settings(settings_cmd) => {
let local_env = load_local_env(&worker_root)?;
let script_name =
resolve_target_script_name(&worker_root, &settings_cmd.target, &local_env);
let client = build_cloudflare_client(
cmd.token.as_deref(),
cmd.account_id.as_deref(),
&local_env,
)?;
let settings = client.get_worker_settings(&script_name).await?;
match settings {
Some(settings) => print_json(&json!({
"script_name": script_name,
"settings": settings,
})),
None => Err(format!(
"Worker settings were not found for script `{}`.",
script_name
)),
}
}
WorkersSubCommand::Wrangler(wrangler_cmd) => match wrangler_cmd.command {
WorkersWranglerSubCommand::GenerateConfig(generate_cmd) => {
wrangler::run_generate_config(&worker_root, &generate_cmd.output)
}
WorkersWranglerSubCommand::ConfigPath(config_path_cmd) => {
match project::resolve_wrangler_config_path(
&worker_root,
&config_path_cmd.command_name,
&config_path_cmd.mode,
) {
Some(path) => {
println!("{}", path);
Ok(())
}
None => {
println!();
Ok(())
}
}
}
},
WorkersSubCommand::D1(d1_cmd) => match d1_cmd.command {
crate::cli::commands::WorkersD1SubCommand::Migrations(migrations_cmd) => {
match migrations_cmd.command {
crate::cli::commands::WorkersD1MigrationsSubCommand::Apply(apply_cmd) => {
wrangler::run_d1_migrations_apply(&worker_root, &apply_cmd)
}
}
}
},
WorkersSubCommand::Deploy(deploy_cmd) => match deploy_cmd.command {
WorkersDeploySubCommand::Predeploy(predeploy_cmd) => {
deploy::run_predeploy(
&worker_root,
cmd.token.as_deref(),
cmd.account_id.as_deref(),
predeploy_cmd.ci,
)
.await
}
WorkersDeploySubCommand::SyncEnvLocal(_) => {
deploy::run_sync_env_local(
&worker_root,
cmd.token.as_deref(),
cmd.account_id.as_deref(),
)
.await
}
WorkersDeploySubCommand::Ci(ci_cmd) => {
deploy::run_deploy_ci_script(&worker_root, ci_cmd.version_upload)
}
WorkersDeploySubCommand::Select(select_cmd) => deploy::run_select_deploy_script(
&worker_root,
select_cmd.ci,
select_cmd.branch.as_deref(),
),
},
WorkersSubCommand::Worktree(worktree_cmd) => match worktree_cmd.command {
WorkersWorktreeSubCommand::Paths(_) => worktree::print_worktree_paths(&worker_root),
WorkersWorktreeSubCommand::LinkDevVars(_) => {
worktree::link_dev_vars_from_primary_worktree(&worker_root)
}
},
WorkersSubCommand::Env(env_cmd) => {
let local_env = load_local_env(&worker_root)?;
let script_name = resolve_target_script_name(&worker_root, &env_cmd.target, &local_env);
let summary = wrangler::build_env_summary(
&worker_root,
&script_name,
&local_env,
env_cmd.show_values,
)?;
print_json(&summary)
}
}
}
async fn run_worker_secrets(
worker_root: &Path,
token_override: Option<&str>,
account_id_override: Option<&str>,
target: WorkersTargetArgs,
command: WorkersSecretsSubCommand,
) -> Result<(), String> {
let local_env = load_local_env(worker_root)?;
let script_name = resolve_target_script_name(worker_root, &target, &local_env);
let client = build_cloudflare_client(token_override, account_id_override, &local_env)?;
match command {
WorkersSecretsSubCommand::List(_) => {
let secrets = client.list_worker_secrets(&script_name).await?;
print_json(&json!({
"script_name": script_name,
"secrets": secrets,
}))
}
WorkersSecretsSubCommand::Get(get_cmd) => {
let secret = client
.get_worker_secret(&script_name, &get_cmd.name)
.await?;
print_json(&json!({
"script_name": script_name,
"secret": secret,
}))
}
WorkersSecretsSubCommand::Put(put_cmd) => {
let secret_value = resolve_secret_value(&put_cmd)?;
let result = client
.put_worker_secret(&script_name, &put_cmd.name, &secret_value)
.await?;
print_json(&json!({
"script_name": script_name,
"secret": result,
}))
}
WorkersSecretsSubCommand::Delete(delete_cmd) => {
client
.delete_worker_secret(&script_name, &delete_cmd.name)
.await?;
println!(
"Deleted Worker secret `{}` from script `{}`.",
delete_cmd.name, script_name
);
Ok(())
}
WorkersSecretsSubCommand::Bulk(bulk_cmd) => {
let patch = read_bulk_secret_patch(&bulk_cmd)?;
let result = client
.patch_worker_secrets_bulk(&script_name, &patch)
.await?;
print_json(&json!({
"script_name": script_name,
"result": result,
}))
}
}
}
fn resolve_target_script_name(
worker_root: &Path,
target: &WorkersTargetArgs,
local_env: &std::collections::HashMap<String, String>,
) -> String {
project::resolve_remote_script_name(
worker_root,
target.script.as_deref(),
target.worker.as_deref(),
target.environment.as_deref(),
local_env,
)
}
fn build_cloudflare_client(
token_override: Option<&str>,
account_id_override: Option<&str>,
local_env: &std::collections::HashMap<String, String>,
) -> Result<CloudflareClient, String> {
let token = token_override
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.or_else(|| local_env.get("CLOUDFLARE_API_TOKEN").cloned())
.or_else(resolve_cloudflare_api_token)
.ok_or_else(|| {
"No Cloudflare API token found. Use `--token`, `CLOUDFLARE_API_TOKEN`, or `xbp config cloudflare set-key`.".to_string()
})?;
let account_id = account_id_override
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.or_else(|| local_env.get("CLOUDFLARE_ACCOUNT_ID").cloned())
.or_else(resolve_cloudflare_account_id)
.ok_or_else(|| {
"No Cloudflare account ID found. Use `--account-id`, `CLOUDFLARE_ACCOUNT_ID`, or `xbp config cloudflare set-account-id`.".to_string()
})?;
CloudflareClient::new(token, account_id)
}
fn load_local_env(worker_root: &Path) -> Result<std::collections::HashMap<String, String>, String> {
let env_local_path = worker_root.join(".env.local");
if env_local_path.exists() {
project::parse_multiline_env_file(&env_local_path)
} else {
Ok(std::collections::HashMap::new())
}
}
fn resolve_secret_value(cmd: &WorkersSecretsPutCmd) -> Result<String, String> {
match (cmd.from_stdin, cmd.value.as_deref()) {
(true, Some(_)) => Err("Use either `--value` or `--from-stdin`, not both.".to_string()),
(true, None) => {
let mut buffer = String::new();
io::stdin()
.read_to_string(&mut buffer)
.map_err(|error| format!("Failed to read stdin: {}", error))?;
let value = buffer.trim_end_matches(['\r', '\n']).to_string();
if value.is_empty() {
return Err("No secret value was provided on stdin.".to_string());
}
Ok(value)
}
(false, Some(value)) if !value.trim().is_empty() => Ok(value.to_string()),
_ => Err("Provide `--value <secret>` or `--from-stdin`.".to_string()),
}
}
fn read_bulk_secret_patch(cmd: &WorkersSecretsBulkCmd) -> Result<Value, String> {
let content = fs::read_to_string(&cmd.file)
.map_err(|error| format!("Failed to read {}: {}", cmd.file.display(), error))?;
if cmd.format.eq_ignore_ascii_case("json") {
let parsed = serde_json::from_str::<Value>(&content)
.map_err(|error| format!("Failed to parse JSON {}: {}", cmd.file.display(), error))?;
return normalize_secret_patch_value(parsed);
}
if !cmd.format.eq_ignore_ascii_case("env") {
return Err(format!(
"Unsupported bulk secret format `{}`. Use `env` or `json`.",
cmd.format
));
}
let parsed = project::parse_multiline_env_content(&content);
let patch = parsed
.into_iter()
.map(|(key, value)| {
(
key,
json!({
"type": "secret_text",
"text": value,
}),
)
})
.collect::<Map<String, Value>>();
Ok(Value::Object(patch))
}
fn normalize_secret_patch_value(value: Value) -> Result<Value, String> {
let Value::Object(entries) = value else {
return Err("Bulk JSON secrets input must be an object.".to_string());
};
let mut normalized = Map::new();
for (key, value) in entries {
let normalized_value = match value {
Value::Null => Value::Null,
Value::String(text) => json!({
"type": "secret_text",
"text": text,
}),
Value::Object(mut object) => {
object
.entry("type".to_string())
.or_insert_with(|| Value::String("secret_text".to_string()));
Value::Object(object)
}
other => {
return Err(format!(
"Invalid bulk secret value for `{}`. Use a string, object, or null, not {}.",
key, other
));
}
};
normalized.insert(key, normalized_value);
}
Ok(Value::Object(normalized))
}
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(())
}