use std::io::{self, Write};
use std::path::Path;
use std::process::{Command, ExitStatus};
use anyhow::{Context, Result};
use crate::package::{Runner, Script};
pub const EXIT_CODE_INTERRUPTED: i32 = 130;
#[derive(Debug)]
pub struct ExecutionResult {
pub status: ExitStatus,
pub command: String,
}
impl ExecutionResult {
pub fn success(&self) -> bool {
self.status.success()
}
pub fn code(&self) -> Option<i32> {
self.status.code()
}
}
pub fn run_script(
runner: Runner,
script: &Script,
args: Option<&str>,
dry_run: bool,
) -> Result<i32> {
let args_vec: Vec<String> = args
.map(|a| shell_words::split(a).unwrap_or_else(|_| vec![a.to_string()]))
.unwrap_or_default();
let project_dir = std::env::current_dir().context("Failed to get current directory")?;
execute_script(runner, script.name(), &args_vec, &project_dir, dry_run)
.map(|result| result.code().unwrap_or(EXIT_CODE_INTERRUPTED))
}
pub fn run_script_in_dir(
runner: Runner,
script: &Script,
args: Option<&str>,
project_dir: &Path,
dry_run: bool,
) -> Result<i32> {
let args_vec: Vec<String> = args
.map(|a| shell_words::split(a).unwrap_or_else(|_| vec![a.to_string()]))
.unwrap_or_default();
execute_script(runner, script.name(), &args_vec, project_dir, dry_run)
.map(|result| result.code().unwrap_or(EXIT_CODE_INTERRUPTED))
}
pub fn run_scripts(
runner: Runner,
scripts: &[(&Script, Option<String>)],
dry_run: bool,
) -> Result<Vec<i32>> {
let total = scripts.len();
let mut results = Vec::with_capacity(total);
let project_dir = std::env::current_dir().context("Failed to get current directory")?;
for (i, (script, args)) in scripts.iter().enumerate() {
println!(
"\n\x1b[1;36mRunning {}/{}: {}...\x1b[0m",
i + 1,
total,
script.name()
);
io::stdout().flush().ok();
let args_vec: Vec<String> = args
.as_ref()
.map(|a| shell_words::split(a).unwrap_or_else(|_| vec![a.to_string()]))
.unwrap_or_default();
let result = execute_script(runner, script.name(), &args_vec, &project_dir, dry_run)?;
let exit_code = result.code().unwrap_or(EXIT_CODE_INTERRUPTED);
results.push(exit_code);
if exit_code != 0 {
println!(
"\n\x1b[1;31mScript '{}' failed with exit code {}\x1b[0m",
script.name(),
exit_code
);
break;
}
}
Ok(results)
}
pub fn run_scripts_in_dir(
runner: Runner,
scripts: &[(&Script, Option<String>)],
project_dir: &Path,
dry_run: bool,
) -> Result<Vec<i32>> {
let total = scripts.len();
let mut results = Vec::with_capacity(total);
for (i, (script, args)) in scripts.iter().enumerate() {
println!(
"\n\x1b[1;36mRunning {}/{}: {}...\x1b[0m",
i + 1,
total,
script.name()
);
io::stdout().flush().ok();
let args_vec: Vec<String> = args
.as_ref()
.map(|a| shell_words::split(a).unwrap_or_else(|_| vec![a.to_string()]))
.unwrap_or_default();
let result = execute_script(runner, script.name(), &args_vec, project_dir, dry_run)?;
let exit_code = result.code().unwrap_or(EXIT_CODE_INTERRUPTED);
results.push(exit_code);
if exit_code != 0 {
println!(
"\n\x1b[1;31mScript '{}' failed with exit code {}\x1b[0m",
script.name(),
exit_code
);
break;
}
}
Ok(results)
}
pub fn execute_script(
runner: Runner,
script: &str,
args: &[String],
project_dir: &Path,
dry_run: bool,
) -> Result<ExecutionResult> {
let cmd_parts = runner.run_command_with_args(script, args);
let command_str = cmd_parts.join(" ");
if dry_run {
println!("Would run: {command_str}");
return Ok(ExecutionResult {
status: std::process::ExitStatus::default(),
command: command_str,
});
}
let mut command = Command::new(&cmd_parts[0]);
command.args(&cmd_parts[1..]);
command.current_dir(project_dir);
command.stdin(std::process::Stdio::inherit());
command.stdout(std::process::Stdio::inherit());
command.stderr(std::process::Stdio::inherit());
let status = command
.status()
.with_context(|| format!("Failed to execute: {command_str}"))?;
Ok(ExecutionResult {
status,
command: command_str,
})
}
pub fn format_dry_run_command(runner: Runner, script: &str, args: Option<&str>) -> String {
let args_vec: Vec<String> = args
.map(|a| shell_words::split(a).unwrap_or_else(|_| vec![a.to_string()]))
.unwrap_or_default();
let cmd = runner.run_command_with_args(script, &args_vec);
format!("Would run: {}", cmd.join(" "))
}
pub fn execute_workspace_script(
runner: Runner,
workspace: &str,
script: &str,
args: &[String],
project_dir: &Path,
dry_run: bool,
) -> Result<ExecutionResult> {
let cmd_parts = runner.workspace_command_with_args(workspace, script, args);
let command_str = cmd_parts.join(" ");
if dry_run {
println!("Would run: {command_str}");
return Ok(ExecutionResult {
status: std::process::ExitStatus::default(),
command: command_str,
});
}
let mut command = Command::new(&cmd_parts[0]);
command.args(&cmd_parts[1..]);
command.current_dir(project_dir);
command.stdin(std::process::Stdio::inherit());
command.stdout(std::process::Stdio::inherit());
command.stderr(std::process::Stdio::inherit());
let status = command
.status()
.with_context(|| format!("Failed to execute: {command_str}"))?;
Ok(ExecutionResult {
status,
command: command_str,
})
}
pub fn run_workspace_script(
runner: Runner,
workspace: &str,
script: &Script,
args: Option<&str>,
project_dir: &Path,
dry_run: bool,
) -> Result<i32> {
let args_vec: Vec<String> = args
.map(|a| shell_words::split(a).unwrap_or_else(|_| vec![a.to_string()]))
.unwrap_or_default();
execute_workspace_script(
runner,
workspace,
script.name(),
&args_vec,
project_dir,
dry_run,
)
.map(|result| result.code().unwrap_or(EXIT_CODE_INTERRUPTED))
}
pub fn format_workspace_dry_run_command(
runner: Runner,
workspace: &str,
script: &str,
args: Option<&str>,
) -> String {
let args_vec: Vec<String> = args
.map(|a| shell_words::split(a).unwrap_or_else(|_| vec![a.to_string()]))
.unwrap_or_default();
let cmd = runner.workspace_command_with_args(workspace, script, &args_vec);
format!("Would run: {}", cmd.join(" "))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dry_run() {
let result = execute_script(Runner::Npm, "test", &[], Path::new("."), true).unwrap();
assert_eq!(result.command, "npm run test");
}
#[test]
fn test_dry_run_with_args() {
let args = vec!["--watch".to_string(), "--coverage".to_string()];
let result = execute_script(Runner::Npm, "test", &args, Path::new("."), true).unwrap();
assert_eq!(result.command, "npm run test -- --watch --coverage");
}
#[test]
fn test_format_dry_run_command() {
assert_eq!(
format_dry_run_command(Runner::Npm, "dev", None),
"Would run: npm run dev"
);
assert_eq!(
format_dry_run_command(Runner::Npm, "dev", Some("--host")),
"Would run: npm run dev -- --host"
);
assert_eq!(
format_dry_run_command(Runner::Yarn, "dev", Some("--host")),
"Would run: yarn dev --host"
);
assert_eq!(
format_dry_run_command(Runner::Pnpm, "dev", Some("--host")),
"Would run: pnpm dev -- --host"
);
assert_eq!(
format_dry_run_command(Runner::Bun, "dev", Some("--host")),
"Would run: bun run dev --host"
);
}
#[test]
fn test_run_script_dry_run() {
let script = Script::new("dev", "vite");
let result = run_script(Runner::Npm, &script, None, true).unwrap();
assert_eq!(result, 0);
}
#[test]
fn test_run_script_with_args_dry_run() {
let script = Script::new("dev", "vite");
let result = run_script(Runner::Npm, &script, Some("--host"), true).unwrap();
assert_eq!(result, 0);
}
#[test]
fn test_run_scripts_dry_run() {
let script1 = Script::new("build", "vite build");
let script2 = Script::new("test", "vitest");
let scripts: Vec<(&Script, Option<String>)> =
vec![(&script1, None), (&script2, Some("--coverage".to_string()))];
let results = run_scripts(Runner::Npm, &scripts, true).unwrap();
assert_eq!(results, vec![0, 0]);
}
}