use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result, bail};
use crate::ui;
pub(crate) fn run_command(
base_dir: &Path,
command: &str,
label: &str,
env: &[(&str, &str)],
) -> Result<()> {
let spinner = ui::spinner(command);
let mut cmd = Command::new("sh");
cmd.args(["-c", command]);
cmd.current_dir(base_dir);
for (k, v) in env {
cmd.env(k, v);
}
let output = cmd.output().with_context(|| format!("failed to run {label}"))?;
if !output.status.success() {
spinner.finish_with("failed");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let mut msg = format!("{label} exited with status {}", output.status);
if !stderr.is_empty() {
msg.push('\n');
msg.push_str(&stderr);
}
if !stdout.is_empty() {
msg.push('\n');
msg.push_str(&stdout);
}
bail!("{msg}");
}
spinner.finish();
Ok(())
}
pub(crate) fn run_builtin_bundler(
base_dir: &Path,
entry: &str,
out_dir: &str,
env: &[(&str, &str)],
) -> Result<()> {
let runtime = if which_exists("bun") { "bun" } else { "node" };
let script = find_cli_script(base_dir, "build-frontend.mjs")?;
let spinner = ui::spinner(&format!("{runtime} build-frontend.mjs {entry} {out_dir}"));
let mut cmd = Command::new(runtime);
cmd.args([script.to_str().expect("script path is valid UTF-8"), entry, out_dir]);
cmd.current_dir(base_dir);
for (k, v) in env {
cmd.env(k, v);
}
let output = cmd.output().context("failed to run built-in bundler")?;
if !output.status.success() {
spinner.finish_with("failed");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let mut msg = format!("built-in bundler exited with status {}", output.status);
if !stderr.is_empty() {
msg.push('\n');
msg.push_str(&stderr);
}
if !stdout.is_empty() {
msg.push('\n');
msg.push_str(&stdout);
}
bail!("{msg}");
}
spinner.finish();
Ok(())
}
pub(crate) fn find_cli_script(base_dir: &Path, name: &str) -> Result<PathBuf> {
let suffix = format!("@canmi/seam-cli/scripts/{name}");
resolve_node_module(base_dir, &suffix)
.ok_or_else(|| anyhow::anyhow!("{name} not found -- install @canmi/seam-cli"))
}
pub(crate) fn resolve_node_module(start: &Path, suffix: &str) -> Option<PathBuf> {
let mut dir = start.to_path_buf();
loop {
let candidate = dir.join("node_modules").join(suffix);
if candidate.exists() {
return Some(candidate);
}
if !dir.pop() {
break;
}
}
if let Ok(entries) = std::fs::read_dir(start) {
for entry in entries.flatten() {
if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
let candidate = entry.path().join("node_modules").join(suffix);
if candidate.exists() {
return Some(candidate);
}
}
}
}
None
}
pub(crate) fn which_exists(cmd: &str) -> bool {
Command::new("which")
.arg(cmd)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}