use anyhow::Result;
use console::style;
use std::path::Path;
use crate::config::schema::YmConfig;
pub fn run_script(
cfg: &YmConfig,
name: &str,
project_dir: &Path,
) -> Result<()> {
run_script_with_args(cfg, name, project_dir, &[])
}
pub fn run_script_with_args(
cfg: &YmConfig,
name: &str,
project_dir: &Path,
extra_args: &[String],
) -> Result<()> {
let is_hook = name.starts_with("pre") || name.starts_with("post");
if is_hook && std::env::var("YM_LIFECYCLE").is_ok() {
return Ok(());
}
let scripts = match &cfg.scripts {
Some(s) => s,
None => return Ok(()),
};
let env = &cfg.env;
let script_value = match scripts.get(name) {
Some(v) => v,
None => return Ok(()),
};
let cmd = script_value.command();
let timeout_secs = script_value.timeout_secs();
println!(
" {} running script: {}",
style("➜").green(),
style(name).dim()
);
let shell = if cfg!(windows) { "cmd" } else { "sh" };
let flag = if cfg!(windows) { "/C" } else { "-c" };
let cmd = substitute_vars(cmd, cfg);
let full_cmd = if extra_args.is_empty() {
cmd.clone()
} else {
let escaped: Vec<String> = extra_args.iter().map(|a| {
if a.contains(' ') {
format!("\"{}\"", a)
} else {
a.clone()
}
}).collect();
format!("{} {}", cmd, escaped.join(" "))
};
let mut command = std::process::Command::new(shell);
command.arg(flag).arg(&full_cmd).current_dir(project_dir);
if is_hook {
command.env("YM_LIFECYCLE", "1");
}
command.env("YM_NAME", &cfg.name);
command.env("YM_VERSION", cfg.version.as_deref().unwrap_or("0.0.0"));
if let Some(env_map) = env {
let home = crate::home_dir_string();
for (k, v) in env_map {
let expanded = if v.starts_with("~/") {
format!("{}{}", home, &v[1..])
} else {
v.clone()
};
command.env(k, expanded);
}
}
let status = if let Some(secs) = timeout_secs {
let mut child = command.spawn()?;
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(secs);
loop {
match child.try_wait()? {
Some(s) => break s,
None => {
if std::time::Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
anyhow::bail!("Script '{}' timed out after {}s", name, secs);
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
}
} else {
command.status()?
};
if !status.success() {
if name.starts_with("post") {
eprintln!(
" {} Post-hook '{}' failed with exit code {:?}",
style("!").yellow(),
name,
status.code()
);
} else {
anyhow::bail!("Script '{}' failed with exit code {:?}", name, status.code());
}
}
Ok(())
}
fn substitute_vars(cmd: &str, cfg: &YmConfig) -> String {
cmd.replace("${project.name}", &cfg.name)
.replace("${project.version}", cfg.version.as_deref().unwrap_or("0.0.0"))
.replace("${project.groupId}", &cfg.group_id)
}