use clap::{Parser, Subcommand};
mod allowlist;
mod builtins;
mod commands;
mod environment;
mod parsers;
mod prompt;
mod runner;
mod runners {
pub mod runners_package_json;
pub mod runners_pyproject_toml;
}
mod task_discovery;
mod task_shadowing;
mod types;
#[derive(Parser)]
#[command(
name = "dela",
author = "Alex Yankov",
version,
about = "A task runner that delegates to other runners",
after_help = r#"Supported Task Runners:
• Make (Makefile)
• Node.js: npm, yarn, pnpm, bun (package.json)
• Python: uv, poetry, poethepoet (pyproject.toml)
• Task (Taskfile.yml)
• Maven (pom.xml)
• Gradle (build.gradle, build.gradle.kts)
• GitHub Actions (act)
• Docker Compose (docker-compose.yml, compose.yml)
• CMake (CMakeLists.txt)
• Travis CI (.travis.yml)
"#,
long_about = r#"Dela integrates with you shell to let you to execute locally defined
tasks such as in Makefile or package.json without specifying the task runner.
Setup:
After installing dela, you need to add it to your shell configuration by
running `$ dela init`. You need to do it only once.
You shell can now execute tasks just by their name: `$ <TASKNAME>`
Get all tasks that are available in the current directory via `$ dela list`.
Tasks with name collisions will be suffixed and be runnable by their new names.
"#,
help_template = "\
{before-help}{about}
{usage-heading}
{usage}
{all-args}{after-help}"
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Init,
List {
#[arg(short, long)]
verbose: bool,
},
Run {
task: String,
},
#[command(name = "configure-shell", hide = true)]
ConfigureShell,
#[command(name = "get-command", hide = true, trailing_var_arg = true)]
GetCommand {
args: Vec<String>,
},
#[command(name = "allow-command", hide = true)]
AllowCommand {
task: String,
#[arg(long)]
allow: Option<u8>,
},
}
fn main() {
let cli = Cli::parse();
let result = match cli.command {
Commands::Init => commands::init::execute(),
Commands::ConfigureShell => commands::configure_shell::execute(),
Commands::List { verbose } => commands::list::execute(verbose),
Commands::Run { task } => commands::run::execute(&task),
Commands::GetCommand { args } => {
if args.is_empty() {
Err("No task name provided".to_string())
} else {
commands::get_command::execute(&args.join(" "))
}
}
Commands::AllowCommand { task, allow } => commands::allow_command::execute(&task, allow),
};
if let Err(err) = result {
if err.starts_with("dela: command or task not found") {
eprintln!("{}", err);
} else {
eprintln!("Error: {}", err);
}
std::process::exit(1);
}
}
#[cfg(test)]
mod tests {
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_command_not_found_error() {
let mut stderr_file = NamedTempFile::new().unwrap();
let mut handle_error = |err: &str| {
if err.starts_with("dela: command or task not found") {
writeln!(stderr_file, "{}", err).unwrap();
} else {
writeln!(stderr_file, "Error: {}", err).unwrap();
}
};
handle_error("dela: command or task not found: missing_command");
handle_error("Failed to execute task");
stderr_file.as_file_mut().flush().unwrap();
let content = std::fs::read_to_string(stderr_file.path()).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 2, "Expected exactly two error lines");
assert_eq!(
lines[0], "dela: command or task not found: missing_command",
"Command not found error should not have 'Error:' prefix"
);
assert_eq!(
lines[1], "Error: Failed to execute task",
"Regular error should have 'Error:' prefix"
);
}
}