spacedriveapp/spacedrive · error · anyhow::Error

Failed to build action: {}

Error message

Failed to build action: {}

What it means

Before executing a copy, the CLI rebuilds the action via `FileCopyAction::from_input(input)`, which delegates to the core builder's `build()` (core/src/ops/files/copy/action.rs:279) and fails on invalid builder state. The CLI wraps that failure string in 'Failed to build action: {}'. The text after the colon is the underlying builder error and names the real problem, e.g. missing or empty sources/destination.

Source

Thrown at apps/cli/src/domains/file/mod.rs:120

				}
			);
		}
	}
	Ok(())
}

/// Run file copy with confirmation handling
async fn run_copy_with_confirmation(
	ctx: &Context,
	mut input: sd_core::ops::files::copy::input::FileCopyInput,
) -> Result<JobId> {
	use crate::util::confirm::prompt_for_choice;
	use sd_core::infra::action::LibraryAction;
	use sd_core::ops::files::copy::action::FileCopyAction;

	// Build the action from input for validation purposes
	let action = FileCopyAction::from_input(input.clone())
		.map_err(|e| anyhow::anyhow!("Failed to build action: {}", e))?;

	// Use the action's validation method to check for conflicts
	// For CLI validation, we'll use a simplified approach since we don't have full library context
	// In a production system, you'd want to pass the actual library context

	// Simple conflict detection - check if destination exists and overwrite is not enabled
	if !input.overwrite {
		let has_conflict = check_for_simple_conflicts(&action).await?;
		if has_conflict {
			use sd_core::infra::action::ConfirmationRequest;

			let request = ConfirmationRequest {
				message: "Destination file(s) already exist. What would you like to do?"
					.to_string(),
				choices: vec![
					"Overwrite the existing file(s)".to_string(),
					"Rename the new file(s) (e.g., file.txt -> file (1).txt)".to_string(),
					"Abort this copy operation".to_string(),

View on GitHub (pinned to 6dfeccf211)

Solutions

  1. Read the suffix after 'Failed to build action: ' — it is the exact builder message (which field was missing).
  2. Verify the arguments actually arrived: quote globs (`sd-cli file copy '/data/*.txt'` style handling) and echo the expanded command before running.
  3. Confirm the source path exists and the destination is provided on the command line.
  4. If the inner message mentions a field mismatch, your sd-cli build may be older than core — rebuild the workspace (`cargo build`) so CLI and core agree.

Example fix

# before
cd /data && sd-cli file copy *.txt /backup  # glob matched nothing -> empty sources -> Failed to build action: ...

# after
ls /data/*.txt >/dev/null && sd-cli file copy /data/*.txt /backup
Defensive patterns

Strategy: validation

Validate before calling

fn copy_input_valid(input: &FileCopyInput) -> bool {
    !input.sources.is_empty() && input.destination.as_os_str().len() > 0
}
if !copy_input_valid(&input) { anyhow::bail!("copy needs at least one source and a destination"); }

Try / catch

if let Err(e) = run_copy_with_confirmation(ctx, input).await {
    let msg = e.to_string();
    if msg.contains("Failed to build action") {
        // the suffix after the colon is the builder's own message; surface only that
        eprintln!("copy rejected: {}", msg.split_once(": ").map(|(_, r)| r).unwrap_or(&msg));
    }
    return Err(e);
}

Prevention

When it happens

Trigger: Running `sd-cli file copy` where argument parsing produced an empty source list or an unset destination (e.g. flags consumed by a shell glob that matched nothing), or a FileCopyInput assembled programmatically with missing required fields.

Common situations: Scripts passing an unquoted glob that expands to zero files; copying where the source argument was eaten by an earlier flag; version drift between the CLI's input shape and the core builder's required fields.

Related errors


AI-assisted analysis of spacedriveapp/spacedrive@6dfeccf211 (2026-08-16). Data as JSON: /api/errors/e6f91f83a8c5e706. Report an issue: GitHub.