use super::ProcessService;
use crate::paths;
use anyhow::{Context, Result};
use mecha10_cli_core::utils::strip_node_scope;
use std::collections::HashMap;
impl ProcessService {
pub fn spawn_node_runner(
&mut self,
node_identifier: &str,
project_name: &str,
project_env: Option<HashMap<String, String>>,
) -> Result<u32> {
let runner_path = Self::resolve_node_runner_path();
let env = project_env.unwrap_or_default();
let args = node_runner_args(project_name, node_identifier);
self.spawn_with_env(node_identifier, &runner_path, &args, env)
}
pub fn spawn_node_direct(
&mut self,
node_identifier: &str,
_project_name: &str,
project_env: Option<HashMap<String, String>>,
) -> Result<u32> {
let node_name = strip_node_scope(node_identifier);
let is_local_node = self.is_local_project_node(node_name);
let mut env = HashMap::new();
env.insert("NODE_NAME".to_string(), node_identifier.to_string());
if let Ok(rust_log) = std::env::var("RUST_LOG") {
env.insert("RUST_LOG".to_string(), rust_log);
}
if let Some(project_vars) = project_env {
tracing::debug!(
"🔀 spawn_node_direct for '{}': merging {} project vars",
node_name,
project_vars.len()
);
for (key, value) in &project_vars {
tracing::debug!(
" Adding: {} = {}",
key,
if key == "ROBOT_API_KEY" { "<redacted>" } else { value }
);
}
env.extend(project_vars);
} else {
tracing::warn!("⚠️ spawn_node_direct for '{}': No project_env provided!", node_name);
}
tracing::debug!(
"🏁 spawn_node_direct for '{}': final env has {} vars, is_local: {}",
node_name,
env.len(),
is_local_node
);
if is_local_node {
self.spawn_local_node_with_identifier(node_identifier, node_name, env)
} else {
self.spawn_framework_node(node_identifier, node_name, env)
}
}
fn spawn_framework_node(
&mut self,
node_identifier: &str,
node_name: &str,
env: HashMap<String, String>,
) -> Result<u32> {
use super::super::binary_manager::BinaryManager;
let version = self.get_node_version_from_config(node_identifier);
let binary_manager = BinaryManager::new()?;
let binary_path = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current()
.block_on(binary_manager.resolve_with_version(node_name, version.as_deref()))
})?;
tracing::info!(
"🚀 Spawning framework node '{}' from {}",
node_name,
binary_path.display()
);
self.spawn_with_env(node_identifier, &binary_path.to_string_lossy(), &[], env)
}
fn spawn_local_node_with_identifier(
&mut self,
node_identifier: &str,
node_name: &str,
env: HashMap<String, String>,
) -> Result<u32> {
use std::process::Command;
tracing::info!("🔨 Building local node: {}", node_name);
let build_output = Command::new("cargo")
.args(["build", "-p", node_name])
.output()
.context("Failed to run cargo build")?;
if !build_output.status.success() {
let stderr = String::from_utf8_lossy(&build_output.stderr);
anyhow::bail!("Failed to build node '{}': {}", node_name, stderr);
}
tracing::info!("✅ Built local node: {}", node_name);
let binary_path = paths::target_path("debug", node_name);
if !std::path::Path::new(&binary_path).exists() {
anyhow::bail!(
"Binary not found at '{}'. Build may have failed or the package name differs from node name.",
binary_path
);
}
self.spawn_with_env(node_identifier, &binary_path, &[], env)
}
}
fn node_runner_args<'a>(project_name: &'a str, node_identifier: &'a str) -> [&'a str; 3] {
["--project", project_name, strip_node_scope(node_identifier)]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn node_runner_args_passes_project_name_and_strips_bundled_node_scope() {
let args = node_runner_args("rover-robot", "@mecha10/simulator");
assert_eq!(args, ["--project", "rover-robot", "simulator"]);
}
#[test]
fn node_runner_args_passes_project_name_and_strips_local_node_scope() {
let args = node_runner_args("rover-robot", "@local/my-node");
assert_eq!(args, ["--project", "rover-robot", "my-node"]);
}
#[test]
fn node_runner_args_leaves_unscoped_node_name_untouched() {
let args = node_runner_args("rover-robot", "my-node");
assert_eq!(args, ["--project", "rover-robot", "my-node"]);
}
}