#[cfg(not(unix))]
use anyhow::Context;
use anyhow::Result;
use std::env;
use std::path::{Path, PathBuf};
use super::args::Cli;
use super::banner;
use super::run;
use super::shim;
const KNOWN_TARGETS: &[&str] = &["node", "npm", "npx", "pnpm", "yarn", "bun"];
const SHIM_DEPTH_LIMIT: u32 = 8;
const SHIM_DEPTH_ENV: &str = "BURN_SHIM_DEPTH";
pub enum Detected {
KnownTarget(String),
PathTarget(String),
Runnable,
Unknown(String),
}
pub fn detect(file: &Path) -> Detected {
if file.components().count() != 1 {
return Detected::Runnable;
}
if file.exists() {
return Detected::Runnable;
}
let name = file.to_string_lossy().into_owned();
if KNOWN_TARGETS.contains(&name.as_str()) {
return Detected::KnownTarget(name);
}
if is_on_path(&name) {
return Detected::PathTarget(name);
}
Detected::Unknown(name)
}
pub fn dispatch(cli: &mut Cli, target: &str) -> Result<()> {
banner::maybe_show(cli);
match target {
"node" => dispatch_node(cli),
_ => dispatch_via_shim(cli, target),
}
}
fn dispatch_node(cli: &mut Cli) -> Result<()> {
if let Some(code) = cli.eval_code.take() {
return run::run_source(cli, &code, &cli.rest_args);
}
let args = std::mem::take(&mut cli.rest_args);
if args.is_empty() {
anyhow::bail!(
"burn node: missing script path\n\
usage: burn node <file.js> [args…]\n\
usage: burn node -e '<code>' [args…]"
);
}
let file = PathBuf::from(&args[0]);
let user_args = &args[1..];
run::run_file(cli, &file, user_args)
}
fn dispatch_via_shim(cli: &mut Cli, target: &str) -> Result<()> {
check_shim_depth()?;
let shim_dir = shim::ensure_shim_dir()?;
let real = find_real_binary(target, &shim_dir)
.ok_or_else(|| anyhow::anyhow!("burn: '{target}' not found on PATH"))?;
let args = std::mem::take(&mut cli.rest_args);
exec_with_shim(&real, &args, &shim_dir)
}
fn check_shim_depth() -> Result<()> {
let depth = current_shim_depth();
if depth >= SHIM_DEPTH_LIMIT {
anyhow::bail!(
"burn: shim recursion limit reached ({SHIM_DEPTH_ENV}={depth}, limit={SHIM_DEPTH_LIMIT}).\n\
a process in this tree kept spawning `burn` via the PATH shim — check for a fork loop."
);
}
Ok(())
}
fn current_shim_depth() -> u32 {
env::var(SHIM_DEPTH_ENV)
.ok()
.and_then(|v| v.parse::<u32>().ok())
.unwrap_or(0)
}
fn is_on_path(name: &str) -> bool {
let shim_dir_pattern = format!("burn-shim-{}", std::process::id());
let Some(path_var) = env::var_os("PATH") else {
return false;
};
for dir in env::split_paths(&path_var) {
if dir
.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n == shim_dir_pattern)
{
continue;
}
if binary_exists_in(&dir, name) {
return true;
}
}
false
}
fn find_real_binary(name: &str, exclude: &Path) -> Option<PathBuf> {
let path_var = env::var_os("PATH")?;
for dir in env::split_paths(&path_var) {
if dir == exclude {
continue;
}
if let Some(p) = locate_in(&dir, name) {
return Some(p);
}
}
None
}
fn locate_in(dir: &Path, name: &str) -> Option<PathBuf> {
let candidates = binary_candidates(name);
for cand in candidates {
let p = dir.join(&cand);
if p.is_file() {
return Some(p);
}
}
None
}
fn binary_exists_in(dir: &Path, name: &str) -> bool {
binary_candidates(name)
.into_iter()
.any(|c| dir.join(c).is_file())
}
#[cfg(windows)]
fn binary_candidates(name: &str) -> Vec<String> {
let pathext = env::var("PATHEXT").unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".into());
let mut out = vec![name.to_string()];
for ext in pathext.split(';') {
let ext = ext.trim();
if ext.is_empty() {
continue;
}
out.push(format!("{name}{ext}"));
}
out
}
#[cfg(not(windows))]
fn binary_candidates(name: &str) -> Vec<String> {
vec![name.to_string()]
}
#[cfg(unix)]
fn exec_with_shim(real: &Path, args: &[String], shim_dir: &Path) -> Result<()> {
use std::os::unix::process::CommandExt;
let new_path = build_prepended_path(shim_dir);
let next_depth = current_shim_depth() + 1;
let err = std::process::Command::new(real)
.args(args)
.env("PATH", new_path)
.env(SHIM_DEPTH_ENV, next_depth.to_string())
.exec();
Err(anyhow::Error::new(err).context(format!("exec {real:?} failed")))
}
#[cfg(not(unix))]
fn exec_with_shim(real: &Path, args: &[String], shim_dir: &Path) -> Result<()> {
let new_path = build_prepended_path(shim_dir);
let next_depth = current_shim_depth() + 1;
let status = std::process::Command::new(real)
.args(args)
.env("PATH", new_path)
.env(SHIM_DEPTH_ENV, next_depth.to_string())
.status()
.with_context(|| format!("spawning {real:?}"))?;
std::process::exit(status.code().unwrap_or(1));
}
fn build_prepended_path(shim_dir: &Path) -> std::ffi::OsString {
let mut new_path = std::ffi::OsString::from(shim_dir);
if let Some(existing) = env::var_os("PATH")
&& !existing.is_empty()
{
#[cfg(windows)]
new_path.push(";");
#[cfg(not(windows))]
new_path.push(":");
new_path.push(&existing);
}
new_path
}