use serde::Deserialize;
use std::{collections::HashMap, env, path::Path, process::Command};
use tracing::info;
#[derive(Debug, Deserialize, Clone, Default)]
struct CommandOptions {
cwd: Option<String>,
env: Option<HashMap<String, String>>,
}
#[derive(Debug, Deserialize, Clone)]
struct PlatformConfig {
command: Option<String>,
args: Option<Vec<String>>,
options: Option<CommandOptions>,
}
#[derive(Debug, Deserialize)]
pub struct Task {
pub label: String,
#[serde(rename = "type")] task_type: String,
command: Option<String>,
#[serde(rename = "isBackground")]
is_background: Option<bool>,
options: Option<CommandOptions>,
args: Option<Vec<String>>,
#[allow(dead_code)]
windows: Option<PlatformConfig>,
#[allow(dead_code)]
linux: Option<PlatformConfig>,
#[allow(dead_code)]
osx: Option<PlatformConfig>,
}
#[derive(Debug, Deserialize)]
pub struct TasksFile {
pub version: String,
pub tasks: Vec<Task>,
}
pub fn list_tasks(tasks: &[Task], json_output: bool) {
if tasks.is_empty() {
if json_output {
println!("[]");
} else {
println!("No tasks found in tasks.json");
}
return;
}
if json_output {
let labels: Vec<&str> = tasks.iter().map(|t| t.label.as_str()).collect();
println!("{}", serde_json::to_string_pretty(&labels).unwrap());
} else {
for task in tasks {
println!("{}", task.label);
}
}
}
pub fn execute_task(task: &Task) -> Result<(), Box<dyn std::error::Error>> {
info!("Executing task: '{}'", task.label);
let is_background = task.is_background.unwrap_or(false);
let workspace_root = env::current_dir()?;
let (mut command_str, args, options) = get_platform_config(task)?;
command_str = substitute_variables(&command_str, &workspace_root);
let args = args.map(|mut args_vec| {
for arg in &mut args_vec {
*arg = substitute_variables(arg, &workspace_root);
}
args_vec
});
let mut command = if task.task_type == "shell" {
#[cfg(target_os = "windows")]
let cmd = {
let mut c = Command::new("cmd");
c.args(["/C", &command_str]);
c
};
#[cfg(not(target_os = "windows"))]
let cmd = {
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
let mut c = Command::new(shell);
c.args(["-c", &command_str]);
c
};
info!(" Shell command: {}", command_str);
cmd
} else {
let mut cmd = Command::new(&command_str);
if let Some(args) = &args {
cmd.args(args);
}
info!(
" Command: {} {:?}",
command_str,
args.as_deref().unwrap_or(&[])
);
cmd
};
if let Some(options) = &options {
info!("Found options: {:?}", options);
if let Some(cwd) = &options.cwd {
info!("Found cwd: {:?}", cwd);
let expanded_cwd = substitute_variables(cwd, &workspace_root);
info!(" Setting working directory: {}", expanded_cwd);
command.current_dir(&expanded_cwd);
}
if let Some(env_vars) = &options.env {
info!("Found {} environment variables", env_vars.len());
for (key, value) in env_vars {
let expanded_value = substitute_variables(value, &workspace_root);
info!(" Setting env: {}={}", key, expanded_value);
command.env(key, &expanded_value);
}
}
} else {
info!("No options found for this task");
}
if is_background {
info!("Starting background task '{}'...", task.label);
command.spawn().map_err(|e| {
format!(
"Failed to spawn background command '{}': {}",
command_str, e
)
})?;
info!("Background task '{}' started.", task.label);
} else {
let output = command
.output()
.map_err(|e| format!("Failed to execute command '{}': {}", command_str, e))?;
if !output.stdout.is_empty() {
print!("{}", String::from_utf8_lossy(&output.stdout));
}
if !output.stderr.is_empty() {
eprint!("{}", String::from_utf8_lossy(&output.stderr));
}
if output.status.success() {
info!("Task '{}' completed successfully.", task.label);
} else {
return Err(format!(
"Task '{}' failed with exit code: {:?}",
task.label,
output.status.code()
)
.into());
}
}
Ok(())
}
type PlatformConfigResult =
Result<(String, Option<Vec<String>>, Option<CommandOptions>), Box<dyn std::error::Error>>;
fn get_platform_config(task: &Task) -> PlatformConfigResult {
#[cfg(target_os = "windows")]
let platform_config = &task.windows;
#[cfg(target_os = "linux")]
let platform_config = &task.linux;
#[cfg(target_os = "macos")]
let platform_config = &task.osx;
if let Some(config) = platform_config {
let command = config
.command
.as_ref()
.or(task.command.as_ref())
.ok_or("No command specified for task")?
.clone();
let args = config.args.as_ref().or(task.args.as_ref()).cloned();
let mut merged_options = task.options.clone().unwrap_or_default();
if let Some(platform_options) = &config.options {
if platform_options.cwd.is_some() {
merged_options.cwd = platform_options.cwd.clone();
}
if let Some(platform_env) = &platform_options.env {
let mut combined_env = merged_options.env.unwrap_or_default();
combined_env.extend(platform_env.clone());
merged_options.env = Some(combined_env);
}
}
Ok((command, args, Some(merged_options)))
} else {
let command = task
.command
.as_ref()
.ok_or("No command specified for task")?
.clone();
Ok((command, task.args.clone(), task.options.clone()))
}
}
fn substitute_variables(input: &str, workspace_root: &Path) -> String {
let mut result = input.to_string();
result = result.replace(
"${workspaceFolder}",
workspace_root.to_string_lossy().as_ref(),
);
let env_regex = regex::Regex::new(r"\$\{env:([^}]+)\}").unwrap();
result = env_regex
.replace_all(&result, |caps: ®ex::Captures| {
let var_name = &caps[1];
env::var(var_name).unwrap_or_else(|_| {
tracing::warn!("Environment variable '{}' not found", var_name);
format!(
"${{{}}}",
caps[0]
.strip_prefix("${")
.unwrap()
.strip_suffix("}")
.unwrap()
)
})
})
.to_string();
result
}