mod arguments;
mod dag_export;
mod discovery;
pub mod list_builder;
mod rendering;
mod types;
#[allow(unused_imports)]
pub use types::{ExecutionMode, OutputConfig, TaskExecutionRequest, TaskSelection};
use arguments::{apply_args_to_task, resolve_task_args};
use discovery::{evaluate_manifest, find_tasks_with_labels, format_label_root, normalize_labels};
use list_builder::prepare_task_index;
use rendering::{format_task_detail, get_task_cli_help, render_task_tree};
use cuenv_core::Result;
use cuenv_core::environment::Environment;
use cuenv_core::lockfile::{LOCKFILE_NAME, LockedRuntime, Lockfile};
use cuenv_core::manifest::{Project, Runtime};
use cuenv_core::tasks::cache::TaskCacheConfig;
use cuenv_core::tasks::executor::{TASK_FAILURE_SNIPPET_LINES, summarize_task_failure};
use cuenv_core::tasks::{
BackendFactory, ExecutorConfig, Task, TaskExecutor, TaskGraph, TaskNode, Tasks,
};
use cuenv_core::tools::apply_resolved_tool_activation;
use std::collections::BTreeMap;
use std::sync::Arc;
use super::env_file::find_cue_module_root;
use super::relative_path_from_root;
use super::tools::{ensure_tools_downloaded, resolve_tool_activation_steps};
use crate::tui::rich::RichTui;
use crate::tui::state::TaskInfo;
use cuenv_core::runtime::resolve_runtime_environment;
#[cfg(feature = "dagger-backend")]
#[allow(clippy::unnecessary_wraps)] fn get_dagger_factory() -> Option<BackendFactory> {
Some(cuenv_dagger::create_dagger_backend)
}
#[cfg(not(feature = "dagger-backend"))]
fn get_dagger_factory() -> Option<BackendFactory> {
None
}
use std::fmt::Write;
use std::path::{Path, PathBuf};
use super::export::get_environment_with_hooks;
use tracing::instrument;
fn resolve_cache_root(project_root: &Path) -> PathBuf {
if let Some(env) = std::env::var_os("CUENV_CACHE_DIR")
&& !env.is_empty()
{
return PathBuf::from(env);
}
if let Some(d) = dirs::cache_dir() {
return d.join("cuenv");
}
project_root.join(".cuenv-cache")
}
fn build_task_cache(
project_root: &Path,
runtime_identity: RuntimeCacheIdentity,
) -> Option<TaskCacheConfig> {
let root = resolve_cache_root(project_root);
let cas = match cuenv_cas::LocalCas::open(&root) {
Ok(c) => Arc::new(c) as Arc<dyn cuenv_cas::Cas>,
Err(e) => {
tracing::warn!(error = %e, root = %root.display(), "task cache disabled: cannot open CAS");
return None;
}
};
let action_cache = match cuenv_cas::LocalActionCache::open(&root) {
Ok(ac) => Arc::new(ac) as Arc<dyn cuenv_cas::ActionCache>,
Err(e) => {
tracing::warn!(error = %e, root = %root.display(), "task cache disabled: cannot open action cache");
return None;
}
};
let vcs_hasher =
Arc::new(cuenv_vcs::WalkHasher::new(project_root)) as Arc<dyn cuenv_vcs::VcsHasher>;
Some(TaskCacheConfig {
cas,
action_cache,
vcs_hasher,
vcs_hasher_root: project_root.to_path_buf(),
cuenv_version: env!("CARGO_PKG_VERSION").to_string(),
runtime_identity_properties: runtime_identity.properties,
cache_disabled_reason: runtime_identity.cache_disabled_reason,
})
}
#[derive(Debug, Clone, Default)]
struct RuntimeCacheIdentity {
properties: BTreeMap<String, String>,
cache_disabled_reason: Option<String>,
}
fn resolve_runtime_cache_identity(
module_root: &Path,
project_root: &Path,
runtime: Option<&Runtime>,
) -> RuntimeCacheIdentity {
let mut identity = RuntimeCacheIdentity::default();
let Some(runtime) = runtime else {
return identity;
};
match runtime {
Runtime::Nix(nix_runtime) => {
identity
.properties
.insert("runtime.kind".to_string(), "nix".to_string());
let lockfile_path = module_root.join(LOCKFILE_NAME);
let lockfile = match Lockfile::load(&lockfile_path) {
Ok(Some(lockfile)) => lockfile,
Ok(None) => {
identity.cache_disabled_reason = Some(format!(
"runtime is nix but {} is missing",
lockfile_path.display()
));
return identity;
}
Err(e) => {
identity.cache_disabled_reason = Some(format!(
"runtime is nix but {} could not be read: {}",
lockfile_path.display(),
e
));
return identity;
}
};
let project_path = relative_path_from_root(module_root, project_root);
let project_key = project_path.to_string_lossy().into_owned();
let Some(locked_runtime) = lockfile.find_runtime(&project_key) else {
identity.cache_disabled_reason = Some(format!(
"runtime is nix but lockfile has no runtime entry for project '{}'",
project_key
));
return identity;
};
let LockedRuntime::Nix(locked_nix) = locked_runtime;
if locked_nix.flake != nix_runtime.flake || locked_nix.output != nix_runtime.output {
identity.cache_disabled_reason = Some(format!(
"runtime lock mismatch for project '{}': expected flake='{}' output='{}', got flake='{}' output='{}'",
project_key,
nix_runtime.flake,
nix_runtime.output.as_deref().unwrap_or(""),
locked_nix.flake,
locked_nix.output.as_deref().unwrap_or("")
));
return identity;
}
identity
.properties
.insert("runtime.nix.digest".to_string(), locked_nix.digest.clone());
identity
.properties
.insert("runtime.nix.flake".to_string(), locked_nix.flake.clone());
if let Some(output) = &locked_nix.output {
identity
.properties
.insert("runtime.nix.output".to_string(), output.clone());
}
identity.properties.insert(
"runtime.nix.lockfile".to_string(),
locked_nix.lockfile.clone(),
);
identity
}
Runtime::Devenv(_) => {
identity
.properties
.insert("runtime.kind".to_string(), "devenv".to_string());
identity
}
Runtime::Container(_) => {
identity
.properties
.insert("runtime.kind".to_string(), "container".to_string());
identity
}
Runtime::Dagger(_) => {
identity
.properties
.insert("runtime.kind".to_string(), "dagger".to_string());
identity
}
Runtime::Oci(_) => {
identity
.properties
.insert("runtime.kind".to_string(), "oci".to_string());
identity
}
Runtime::Tools(_) => {
identity
.properties
.insert("runtime.kind".to_string(), "tools".to_string());
identity
}
}
}
#[instrument(name = "task_execute", skip(request), fields(path = %request.path, package = %request.package))]
pub async fn execute(request: TaskExecutionRequest<'_>) -> Result<String> {
execute_task_impl(&request).await
}
struct TaskResolution {
display_name: String,
node: TaskNode,
tasks: Tasks,
graph_root_name: String,
output_ref_deps: Vec<(String, String)>,
}
fn current_instance_output_ref_deps(
executor: &crate::commands::CommandExecutor,
project_root: &Path,
) -> Result<Vec<(String, String)>> {
let module = executor.get_module(project_root)?;
let rel_path = relative_path_from_root(&module.root, project_root);
Ok(module
.get(&rel_path)
.map_or_else(Vec::new, |instance| instance.output_ref_deps.clone()))
}
#[allow(clippy::too_many_lines)]
async fn execute_task_impl(request: &TaskExecutionRequest<'_>) -> Result<String> {
let (task_name, labels, task_args, interactive) = match &request.selection {
TaskSelection::Named { name, args } => {
(Some(name.as_str()), &[][..], args.as_slice(), false)
}
TaskSelection::Labels(l) => (None, l.as_slice(), &[][..], false),
TaskSelection::List => (None, &[][..], &[][..], false),
TaskSelection::Interactive => (None, &[][..], &[][..], true),
};
let path = &request.path;
let package = &request.package;
let environment = request.environment.as_deref();
let format: &str = &request.output.format;
let capture_output = request.output.capture_output;
let materialize_outputs = request
.output
.materialize_outputs
.as_ref()
.and_then(|p| p.to_str());
let show_cache_path = request.output.show_cache_path;
let backend = request.backend.as_deref();
let tui = request.execution_mode == ExecutionMode::Tui;
let help = request.output.help;
let skip_dependencies = request.skip_dependencies;
let dry_run = request.dry_run;
let executor = request.executor;
if task_name.is_none() && help {
return Ok(get_task_cli_help());
}
tracing::info!(
"Executing task from path: {}, package: {}, task: {:?}",
path,
package,
task_name
);
let mut manifest: Project = evaluate_manifest(Path::new(path), package, executor)?;
tracing::debug!("CUE evaluation successful");
tracing::debug!(
"Successfully parsed CUE evaluation, found {} tasks",
manifest.tasks.len()
);
let project_root =
std::fs::canonicalize(path).unwrap_or_else(|_| Path::new(path).to_path_buf());
let cue_module_root = find_cue_module_root(&project_root);
let task_index = prepare_task_index(&mut manifest, &project_root)?;
let local_tasks = task_index.to_tasks();
if interactive && task_name.is_none() && labels.is_empty() {
use super::task_picker::{PickerResult, SelectableTask, run_picker};
let tasks = task_index.list();
let selectable: Vec<SelectableTask> = tasks
.iter()
.map(|t| {
let description = match &t.node {
TaskNode::Task(task) => task.description.clone(),
TaskNode::Group(g) => g.description.clone(),
TaskNode::Sequence(_) => None,
};
SelectableTask {
name: t.name.clone(),
description,
}
})
.collect();
match run_picker(selectable) {
Ok(PickerResult::Selected(selected_task)) => {
let mut request =
TaskExecutionRequest::named(path, package, &selected_task, executor)
.with_format(format);
if let Some(env) = environment {
request = request.with_environment(env);
}
if capture_output.should_capture() {
request = request.with_capture();
}
if let Some(mat_path) = materialize_outputs {
request = request.with_materialize_outputs(mat_path);
}
if show_cache_path {
request = request.with_show_cache_path();
}
if let Some(be) = backend {
request = request.with_backend(be);
}
if tui {
request = request.with_tui();
}
if help {
request = request.with_help();
}
if skip_dependencies {
request = request.with_skip_dependencies();
}
return Box::pin(execute(request)).await;
}
Ok(PickerResult::Cancelled) => {
return Ok(String::new());
}
Err(e) => {
return Err(cuenv_core::Error::configuration(format!(
"Interactive picker failed: {e}"
)));
}
}
}
if task_name.is_none() && labels.is_empty() {
use super::task_list::{
DashboardFormatter, EmojiFormatter, RichFormatter, TablesFormatter, TaskListFormatter,
TextFormatter, build_task_list,
};
use std::io::IsTerminal;
tracing::debug!("Listing available tasks");
let tasks = task_index.list();
tracing::debug!("Found {} tasks to list", tasks.len());
if format == "json" {
return serde_json::to_string(&tasks).map_err(|e| {
cuenv_core::Error::configuration(format!("Failed to serialize tasks: {e}"))
});
}
if tasks.is_empty() {
return Ok("No tasks defined in the configuration".to_string());
}
let project_root =
std::fs::canonicalize(path).unwrap_or_else(|_| Path::new(path).to_path_buf());
let cwd_relative = cue_module_root.as_ref().and_then(|root| {
project_root
.strip_prefix(root)
.ok()
.map(|p| p.to_string_lossy().to_string())
});
let task_data = build_task_list(&tasks, cwd_relative.as_deref(), &project_root);
let effective_format = if format.is_empty() {
manifest
.config
.as_ref()
.and_then(|c| c.task_list_format())
.map(|f| f.as_str())
} else {
Some(format)
};
let output = match effective_format {
Some("rich") => {
let formatter = RichFormatter::new();
formatter.format(&task_data)
}
Some("text") => {
let formatter = TextFormatter;
formatter.format(&task_data)
}
Some("tables") => {
let formatter = TablesFormatter::new();
formatter.format(&task_data)
}
Some("dashboard") => {
let formatter = DashboardFormatter::new();
formatter.format(&task_data)
}
Some("emoji") => {
let formatter = EmojiFormatter;
formatter.format(&task_data)
}
_ => {
if std::io::stdout().is_terminal() {
let formatter = RichFormatter::new();
formatter.format(&task_data)
} else {
let formatter = TextFormatter;
formatter.format(&task_data)
}
}
};
return Ok(output);
}
if !labels.is_empty() && task_name.is_some() {
return Err(cuenv_core::Error::configuration(
"Cannot specify both a task name and --label",
));
}
if !labels.is_empty() && !task_args.is_empty() {
return Err(cuenv_core::Error::configuration(
"Task arguments are not supported when selecting tasks by label",
));
}
let normalized_labels = normalize_labels(labels);
if !labels.is_empty() && normalized_labels.is_empty() {
return Err(cuenv_core::Error::configuration(
"Labels cannot be empty or whitespace-only",
));
}
let resolution = if normalized_labels.is_empty() {
let requested_task = task_name.ok_or_else(|| {
cuenv_core::Error::configuration("task name required when no labels provided")
})?;
tracing::debug!("Looking for specific task: {}", requested_task);
if help {
let tasks = task_index.list();
let prefix = format!("{requested_task}.");
let subtasks: Vec<&cuenv_core::tasks::IndexedTask> = tasks
.iter()
.filter(|t| t.name == requested_task || t.name.starts_with(&prefix))
.copied()
.collect();
if subtasks.is_empty() {
return Err(cuenv_core::Error::configuration(format!(
"Task '{requested_task}' not found",
)));
}
if subtasks.len() == 1 && subtasks[0].name == requested_task {
return Ok(format_task_detail(subtasks[0]));
}
return Ok(render_task_tree(subtasks, None));
}
let task_entry = task_index.resolve(requested_task)?;
let canonical_task_name = task_entry.name.clone();
tracing::debug!(
"Task index entries: {:?}",
task_index
.list()
.iter()
.map(|t| t.name.as_str())
.collect::<Vec<_>>()
);
tracing::debug!(
"Indexed tasks for execution: {:?}",
local_tasks.list_tasks()
);
tracing::debug!(
"Requested task '{}' present: {}",
requested_task,
local_tasks.get(requested_task).is_some()
);
let original_task_node = local_tasks.get(&canonical_task_name).ok_or_else(|| {
cuenv_core::Error::configuration(format!("Task '{canonical_task_name}' not found"))
})?;
let display_task_name = canonical_task_name;
tracing::debug!("Found task node: {:?}", original_task_node);
let mut tasks_in_scope = local_tasks.clone();
let output_ref_deps = current_instance_output_ref_deps(executor, &project_root)?;
let selected_task_node = if task_args.is_empty() {
tasks_in_scope
.get(&display_task_name)
.cloned()
.ok_or_else(|| {
cuenv_core::Error::configuration(format!(
"Task '{display_task_name}' not found in local tasks"
))
})?
} else {
let node = tasks_in_scope
.get(&display_task_name)
.cloned()
.ok_or_else(|| {
cuenv_core::Error::configuration(format!(
"Task '{display_task_name}' not found in local tasks"
))
})?;
if let TaskNode::Task(task) = node {
let resolved_args = resolve_task_args(task.params.as_ref(), task_args)?;
tracing::debug!("Resolved task args: {:?}", resolved_args);
let modified_task = apply_args_to_task(&task, &resolved_args);
let modified_node = TaskNode::Task(Box::new(modified_task.clone()));
tasks_in_scope
.tasks
.insert(display_task_name.clone(), modified_node.clone());
modified_node
} else {
return Err(cuenv_core::Error::configuration(
"Task arguments are not supported for task groups or lists".to_string(),
));
}
};
TaskResolution {
display_name: display_task_name.clone(),
node: selected_task_node,
tasks: tasks_in_scope,
graph_root_name: display_task_name,
output_ref_deps,
}
} else {
let mut tasks_in_scope = local_tasks.clone();
let matching_tasks = find_tasks_with_labels(&local_tasks, &normalized_labels);
if matching_tasks.is_empty() {
return Err(cuenv_core::Error::configuration(format!(
"No tasks with labels {normalized_labels:?} were found in this scope"
)));
}
let display_task_name = format_label_root(&normalized_labels);
let synthetic = Task {
script: Some("true".to_string()),
hermetic: false,
depends_on: matching_tasks
.into_iter()
.map(cuenv_core::tasks::TaskDependency::from_name)
.collect(),
project_root: Some(project_root.clone()),
description: Some(format!(
"Run all tasks matching labels: {}",
normalized_labels.join(", ")
)),
..Default::default()
};
tasks_in_scope.tasks.insert(
display_task_name.clone(),
TaskNode::Task(Box::new(synthetic)),
);
let resolved_node = tasks_in_scope
.get(&display_task_name)
.cloned()
.ok_or_else(|| {
cuenv_core::Error::execution("synthetic task missing after insertion")
})?;
TaskResolution {
display_name: display_task_name.clone(),
node: resolved_node,
tasks: tasks_in_scope,
graph_root_name: display_task_name,
output_ref_deps: current_instance_output_ref_deps(executor, &project_root)?,
}
};
tracing::debug!(
"Building task graph for task: {}",
resolution.graph_root_name
);
let mut task_graph = TaskGraph::new();
if skip_dependencies {
tracing::debug!("Skipping dependencies - adding only the target task");
if let Some(TaskNode::Task(task)) = resolution.tasks.get(&resolution.graph_root_name) {
task_graph.add_task(&resolution.graph_root_name, (**task).clone())?;
}
} else {
task_graph
.build_for_task(&resolution.graph_root_name, &resolution.tasks)
.map_err(|e| {
tracing::error!("Failed to build task graph: {}", e);
e
})?;
}
if !resolution.output_ref_deps.is_empty() {
task_graph.add_output_ref_deps(&resolution.output_ref_deps, &resolution.tasks)?;
}
tracing::debug!(
"Successfully built task graph with {} tasks",
task_graph.task_count()
);
if dry_run.is_dry_run() {
let dag_export = dag_export::DagExport::from_task_graph(&task_graph)?;
return serde_json::to_string_pretty(&dag_export).map_err(|e| {
cuenv_core::Error::configuration(format!("Failed to serialize DAG: {e}"))
});
}
let directory = project_root.clone();
let base_env_vars =
get_environment_with_hooks(&directory, &manifest, package, Some(executor)).await?;
let mut runtime_env = Environment::new();
let runtime_env_vars =
resolve_runtime_environment(&project_root, manifest.runtime.as_ref()).await?;
for (key, value) in runtime_env_vars {
runtime_env.set(key, value);
}
if let Some(env) = &manifest.env {
for (key, value) in &base_env_vars {
runtime_env.set(key.clone(), value.clone());
}
let env_vars = if let Some(env_name) = environment {
env.for_environment(env_name)
} else {
env.base.clone()
};
let (task_env_vars, secrets) =
cuenv_core::environment::Environment::resolve_for_task_with_secrets(
resolution.display_name.as_str(),
&env_vars,
)
.await?;
cuenv_events::register_secrets(secrets.into_iter());
for (key, value) in task_env_vars {
runtime_env.set(key, value);
}
} else {
for (key, value) in base_env_vars {
runtime_env.set(key, value);
}
}
if should_activate_lockfile_tools(&manifest) {
ensure_tools_downloaded(Some(&project_root))
.await
.map_err(|e| {
cuenv_core::Error::configuration(format!("Failed to download tools: {e}"))
})?;
if let Some(activation_steps) =
resolve_tool_activation_steps(Some(&project_root)).map_err(|e| {
cuenv_core::Error::configuration(format!("Failed to resolve tools activation: {e}"))
})?
{
tracing::debug!(
steps = activation_steps.len(),
"Applying configured tool activation operations for task execution"
);
for step in activation_steps {
let current = runtime_env.get(&step.var);
if let Some(new_value) = apply_resolved_tool_activation(current, &step) {
runtime_env.set(step.var.clone(), new_value);
}
}
}
}
let module_root = cue_module_root.as_deref().unwrap_or(project_root.as_path());
let runtime_identity = resolve_runtime_cache_identity(
module_root,
project_root.as_path(),
manifest.runtime.as_ref(),
);
if let Some(reason) = &runtime_identity.cache_disabled_reason {
tracing::warn!(reason, "task cache disabled for this invocation");
}
let task_cache = build_task_cache(&project_root, runtime_identity);
let config = ExecutorConfig {
capture_output,
max_parallel: 0,
environment: runtime_env.clone(),
working_dir: None,
cue_module_root: cue_module_root.clone(),
project_root: project_root.clone(),
materialize_outputs: materialize_outputs.map(|s| Path::new(s).to_path_buf()),
cache_dir: None,
show_cache_path,
backend_config: manifest.config.as_ref().and_then(|c| c.backend.clone()),
cli_backend: backend.map(ToString::to_string),
cache: task_cache.clone(),
};
let executor = TaskExecutor::with_dagger_factory(config, get_dagger_factory());
if tui && task_graph.task_count() > 0 {
let tui_config = ExecutorConfig {
capture_output: cuenv_core::OutputCapture::Capture, max_parallel: 0,
environment: runtime_env.clone(),
working_dir: None,
cue_module_root: cue_module_root.clone(),
project_root: project_root.clone(),
materialize_outputs: materialize_outputs.map(|s| Path::new(s).to_path_buf()),
cache_dir: None,
show_cache_path,
backend_config: manifest.config.as_ref().and_then(|c| c.backend.clone()),
cli_backend: backend.map(ToString::to_string),
cache: task_cache.clone(),
};
let tui_executor = TaskExecutor::with_dagger_factory(tui_config, get_dagger_factory());
return execute_with_rich_tui(&tui_executor, resolution.display_name.as_str(), &task_graph)
.await;
}
let results = execute_task_with_strategy(
&executor,
resolution.display_name.as_str(),
&resolution.node,
&task_graph,
&resolution.tasks,
)
.await?;
if let Some(failed) = results.iter().find(|r| !r.success) {
return Err(cuenv_core::Error::configuration(summarize_task_failure(
failed,
TASK_FAILURE_SNIPPET_LINES,
)));
}
let output = format_task_results(results, capture_output, resolution.display_name.as_str());
Ok(output)
}
fn should_activate_lockfile_tools(project: &Project) -> bool {
matches!(project.runtime, Some(Runtime::Tools(_)))
}
async fn execute_with_rich_tui(
executor: &TaskExecutor,
task_name: &str,
task_graph: &TaskGraph,
) -> Result<String> {
let event_rx = crate::tracing::subscribe_global_events().ok_or_else(|| {
cuenv_core::Error::configuration(
"Global event bus not initialized - TUI requires event-based tracing".to_string(),
)
})?;
let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
let mut tui = RichTui::new(event_rx, ready_tx)
.map_err(|e| cuenv_core::Error::configuration(format!("Failed to initialize TUI: {e}")))?;
let mut task_infos = Vec::new();
let sorted_tasks = task_graph
.topological_sort()
.map_err(|e| cuenv_core::Error::configuration(format!("Failed to sort task graph: {e}")))?;
let mut levels: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for node in &sorted_tasks {
let max_dep_level = node
.task
.depends_on
.iter()
.filter_map(|dep| levels.get(dep.task_name()).copied())
.max()
.unwrap_or(0);
let increment = usize::from(!node.task.depends_on.is_empty());
levels.insert(node.name.clone(), max_dep_level.saturating_add(increment));
}
for node in sorted_tasks {
let task_name = node.name.clone();
let dependencies: Vec<String> = node
.task
.depends_on
.iter()
.map(|d| d.task_name().to_string())
.collect();
let level = levels.get(&task_name).copied().unwrap_or(0);
task_infos.push(TaskInfo::new(task_name, dependencies, level));
}
tui.init_tasks(task_infos);
let tui_handle = tokio::task::spawn_blocking(move || tui.run());
if ready_rx.await.is_err() {
return Err(cuenv_core::Error::configuration(
"TUI failed to initialize - event loop did not start".to_string(),
));
}
let results = executor.execute_graph(task_graph).await?;
let all_succeeded = results.iter().all(|r| r.success);
cuenv_events::emit_command_completed!("task", all_succeeded, 0_u64);
match tui_handle.await {
Ok(Ok(())) => {
}
Ok(Err(e)) => {
tracing::warn!(error = %e, "TUI error (task execution may have succeeded)");
cuenv_events::emit_stderr!(format!("Warning: TUI encountered an error: {e}"));
cuenv_events::emit_stderr!(
"Task output may not have been fully displayed. Check logs for details."
);
}
Err(e) => {
tracing::error!(error = %e, "TUI task failed");
cuenv_events::emit_stderr!(format!("Warning: TUI terminated unexpectedly: {e}"));
}
}
if let Some(failed) = results.iter().find(|r| !r.success) {
return Err(cuenv_core::Error::configuration(summarize_task_failure(
failed,
TASK_FAILURE_SNIPPET_LINES,
)));
}
Ok(format!(
"Task '{task_name}' completed successfully in TUI mode"
))
}
async fn execute_task_with_strategy(
executor: &TaskExecutor,
task_name: &str,
task_node: &TaskNode,
task_graph: &TaskGraph,
all_tasks: &Tasks,
) -> Result<Vec<cuenv_core::tasks::TaskResult>> {
match task_node {
TaskNode::Group(_) | TaskNode::Sequence(_) => {
executor.execute_node(task_name, task_node, all_tasks).await
}
TaskNode::Task(_) => {
if task_graph.task_count() <= 1 {
executor.execute_node(task_name, task_node, all_tasks).await
} else {
executor.execute_graph(task_graph).await
}
}
}
}
fn format_task_results(
results: Vec<cuenv_core::tasks::TaskResult>,
capture_output: cuenv_core::OutputCapture,
task_name: &str,
) -> String {
let mut output = String::new();
for result in results {
if capture_output.should_capture() {
write!(output, "Task '{}' ", result.name).expect("write to string");
if result.success {
output.push_str("succeeded\n");
if !result.stdout.is_empty() {
output.push_str("Output:\n");
output.push_str(&result.stdout);
output.push('\n');
}
} else {
writeln!(output, "failed with exit code {:?}", result.exit_code)
.expect("write to string");
if !result.stderr.is_empty() {
output.push_str("Error:\n");
output.push_str(&result.stderr);
output.push('\n');
}
}
} else {
}
}
if capture_output.should_capture() && output.is_empty() {
output = format!("Task '{task_name}' completed");
} else if !capture_output.should_capture() {
if output.is_empty() {
output = format!("Task '{task_name}' completed");
} else {
let _ = writeln!(output, "Task '{task_name}' completed");
}
}
output
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::CommandExecutor;
use cuenv_core::tasks::TaskNode;
use tokio::sync::mpsc;
use std::fs;
use tempfile::TempDir;
fn create_test_executor() -> CommandExecutor {
let (sender, _receiver) = mpsc::unbounded_channel();
CommandExecutor::new(sender, "cuenv".to_string())
}
#[tokio::test]
async fn test_list_tasks_empty() {
let temp_dir = TempDir::new().expect("write to string");
let cue_content = r#"package test
env: {
FOO: "bar"
}"#;
fs::write(temp_dir.path().join("env.cue"), cue_content).expect("write to string");
let executor = create_test_executor();
let request =
TaskExecutionRequest::list(temp_dir.path().to_str().unwrap(), "test", &executor);
let result = execute(request).await;
if let Ok(output) = result {
assert!(output.contains("No tasks") || output.contains("Available tasks"));
} else {
}
}
#[test]
fn test_format_task_results_variants() {
let r_ok = cuenv_core::tasks::TaskResult {
name: "t".into(),
exit_code: Some(0),
stdout: "hello".into(),
stderr: String::new(),
success: true,
};
let r_fail = cuenv_core::tasks::TaskResult {
name: "t".into(),
exit_code: Some(1),
stdout: String::new(),
stderr: "boom".into(),
success: false,
};
let s = format_task_results(vec![r_ok.clone(), r_fail.clone()], true.into(), "t");
assert!(s.contains("succeeded"));
assert!(s.contains("Output:"));
assert!(s.contains("failed with exit code"));
assert!(s.contains("Error:"));
let s2 = format_task_results(vec![r_ok], false.into(), "t");
assert!(!s2.contains("hello")); assert!(s2.contains("Task 't' completed"));
let s3 = format_task_results(vec![], true.into(), "abc");
assert_eq!(s3, "Task 'abc' completed");
}
#[test]
fn test_render_task_tree() {
use cuenv_core::tasks::IndexedTask;
let make_task = |desc: Option<&str>| Task {
command: "echo".into(),
description: desc.map(ToString::to_string),
..Default::default()
};
let t_build = IndexedTask {
name: "build".into(),
original_name: "build".into(),
node: TaskNode::Task(Box::new(make_task(Some("Build the project")))),
is_group: false,
source_file: None, };
let t_fmt_check = IndexedTask {
name: "fmt.check".into(),
original_name: "fmt.check".into(),
node: TaskNode::Task(Box::new(make_task(Some("Check formatting")))),
is_group: false,
source_file: None,
};
let t_fmt_fix = IndexedTask {
name: "fmt.fix".into(),
original_name: "fmt.fix".into(),
node: TaskNode::Task(Box::new(make_task(Some("Fix formatting")))),
is_group: false,
source_file: None,
};
let tasks = vec![&t_fmt_fix, &t_build, &t_fmt_check];
let output = render_task_tree(tasks, None);
let lines: Vec<&str> = output.lines().collect();
assert_eq!(lines[0], "Tasks:");
assert!(lines[1].starts_with("├─ build"));
assert!(lines[1].contains("Build the project"));
assert!(lines[2].starts_with("└─ fmt"));
assert!(lines[3].starts_with(" ├─ check"));
assert!(lines[3].contains("Check formatting"));
assert!(lines[4].starts_with(" └─ fix"));
assert!(lines[4].contains("Fix formatting"));
}
}