//! Node.js and npm runtime management for Warp.//!//! This module provides functionality to install and manage Node.js/npm,//! supporting multiple platforms (macOS, Linux, Windows) and architectures//! (x64, arm64). use anyhow::{Context, Result, bail};use serde::Deserialize; cfg_if::cfg_if! { if #[cfg(feature = "local_fs")] { use flate2::read::GzDecoder; use futures::io::AsyncWriteExt; use std::ffi::OsStr; use std::io::Read; use std::path::{Path, PathBuf}; use tar::Archive; use command::r#async::Command; use semver::Version; }} /// The pinned Node.js version to install.#[cfg(feature = "local_fs")]const NODE_VERSION: &str = "v22.12.0"; /// Minimum supported Node.js version for system-installed Node.#[cfg(feature = "local_fs")]const MIN_NODE_VERSION: Version = Version::new(20, 0, 0); // Platform-specific paths for Node.js binariescfg_if::cfg_if! { if #[cfg(all(feature = "local_fs", windows))] { const NODE_BINARY_PATH: &str = "node.exe"; const NPM_BINARY_PATH: &str = "node_modules/npm/bin/npm-cli.js"; } else if #[cfg(feature = "local_fs")] { const NODE_BINARY_PATH: &str = "bin/node"; const NPM_BINARY_PATH: &str = "bin/npm"; }} /// Information about an npm package from the npm registry.#[derive(Debug, Deserialize)]#[serde(rename_all = "kebab-case")]pub struct NpmInfo { #[serde(default)] dist_tags: NpmInfoDistTags,} #[derive(Debug, Deserialize, Default)]pub struct NpmInfoDistTags { latest: Option<String>,} impl NpmInfo { /// Returns the latest version of the package. pub fn latest_version(&self) -> Option<&str> { self.dist_tags.latest.as_deref() }} /// Archive type for Node.js distribution.#[cfg(feature = "local_fs")]#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum ArchiveType { /// A gzip-compressed tarball (for macOS and Linux) TarGz, /// A zip archive (for Windows) Zip,} /// Platform-specific Node.js distribution information.#[cfg(feature = "local_fs")]struct NodeDistribution { os: &'static str, arch: &'static str, archive_type: ArchiveType,} #[cfg(feature = "local_fs")]impl NodeDistribution { /// Determines the Node.js distribution for the current platform. fn current() -> Result<Self> { let os = match std::env::consts::OS { "macos" => "darwin", "linux" => "linux", "windows" => "win", other => bail!("Unsupported operating system: {}", other), }; let arch = match std::env::consts::ARCH { "x86_64" => "x64", "aarch64" => "arm64", other => bail!("Unsupported architecture: {}", other), }; let archive_type = match std::env::consts::OS { "windows" => ArchiveType::Zip, _ => ArchiveType::TarGz, }; Ok(Self { os, arch, archive_type, }) } /// Returns the folder name for the extracted Node.js distribution. fn folder_name(&self, version: &str) -> String { format!("node-{}-{}-{}", version, self.os, self.arch) } /// Returns the file extension for the archive. fn file_extension(&self) -> &'static str { match self.archive_type { ArchiveType::TarGz => "tar.gz", ArchiveType::Zip => "zip", } } /// Returns the download URL for the Node.js distribution. fn download_url(&self, version: &str) -> String { let file_name = format!( "node-{}-{}-{}.{}", version, self.os, self.arch, self.file_extension() ); format!("https://nodejs.org/dist/{}/{}", version, file_name) }} /// Returns the path to the Node.js installation directory.#[cfg(feature = "local_fs")]pub fn node_installation_dir() -> Result<PathBuf> { let dist = NodeDistribution::current()?; let folder_name = dist.folder_name(NODE_VERSION); Ok(warp_core::paths::data_dir().join("node").join(folder_name))} /// Returns the path to the installed node binary.#[cfg(feature = "local_fs")]pub fn node_binary_path() -> Result<PathBuf> { Ok(node_installation_dir()?.join(NODE_BINARY_PATH))} /// Returns the path to the installed npm binary/script.#[cfg(feature = "local_fs")]pub fn npm_binary_path() -> Result<PathBuf> { Ok(node_installation_dir()?.join(NPM_BINARY_PATH))} /// Installs Node.js and npm if not already installed.////// This function will:/// 1. Check if a valid Node.js installation already exists/// 2. Download the appropriate Node.js distribution for the current platform/// 3. Extract it to the Warp data directory////// # Returns/// Returns the path to the Node.js installation directory on success.////// # Errors/// Returns an error if:/// - The current platform/architecture is not supported/// - Network errors occur during download/// - Extraction fails#[cfg(feature = "local_fs")]pub async fn install_npm(client: &http_client::Client) -> Result<PathBuf> { log::info!("Node.js runtime install_npm called"); let dist = NodeDistribution::current()?; let version = NODE_VERSION; let folder_name = dist.folder_name(version); let node_containing_dir = warp_core::paths::data_dir().join("node"); let node_dir = node_containing_dir.join(&folder_name); let node_binary = node_dir.join(NODE_BINARY_PATH); // Check if we already have a valid installation if is_valid_installation(&node_binary).await { log::info!( "Using existing Node.js installation at {}", node_dir.display() ); return Ok(node_dir); } // Remove any existing (potentially corrupted) installation if async_fs::metadata(&node_containing_dir).await.is_ok() { log::info!("Removing existing Node.js directory for clean installation"); async_fs::remove_dir_all(&node_containing_dir) .await .context("Failed to remove existing Node.js directory")?; } // Create the containing directory async_fs::create_dir_all(&node_containing_dir) .await .context("Failed to create Node.js containing directory")?; // Download Node.js let url = dist.download_url(version); log::info!("Downloading Node.js from {}", url); let response = client .get(&url) .send() .await .context("Failed to download Node.js")?; if !response.status().is_success() { bail!( "Node.js download failed with status {}: {}", response.status(), response.text().await.unwrap_or_default() ); } let bytes = response .bytes() .await .context("Failed to read Node.js download response")?; log::info!("Download complete, extracting..."); // Extract the archive match dist.archive_type { ArchiveType::TarGz => { extract_tar_gz(&bytes, &node_containing_dir)?; } ArchiveType::Zip => { extract_zip(&bytes, &node_containing_dir, None::<fn(&str) -> bool>).await?; } } log::info!("Node.js extracted successfully to {}", node_dir.display()); Ok(node_dir)} /// Checks if an existing Node.js installation is valid.#[cfg(feature = "local_fs")]async fn is_valid_installation(node_binary: &Path) -> bool { // Check if the binary exists if async_fs::metadata(node_binary).await.is_err() { return false; } // Try to run node --version to verify it works let result = Command::new(node_binary).arg("--version").output().await; match result { Ok(output) => { if output.status.success() { log::debug!( "Node.js version check passed: {}", String::from_utf8_lossy(&output.stdout).trim() ); true } else { log::warn!( "Node.js version check failed: {}", String::from_utf8_lossy(&output.stderr) ); false } } Err(err) => { log::warn!("Failed to run Node.js binary: {}", err); false } }} /// Extracts a gzip-compressed tarball to the destination directory.////// This is a simple wrapper around tar::Archive that extracts all contents.#[cfg(feature = "local_fs")]pub fn extract_tar_gz(data: &[u8], dest_dir: &Path) -> Result<()> { let decoder = GzDecoder::new(data); let mut archive = Archive::new(decoder); archive .unpack(dest_dir) .context("Failed to extract tar.gz archive")?; Ok(())} /// Extracts a single gzip-compressed file to the destination path.////// This decompresses a `.gz` file (not a `.tar.gz` archive) directly to a file.#[cfg(feature = "local_fs")]pub async fn extract_gz(data: &[u8], dest_path: &Path) -> Result<()> { let mut decoder = GzDecoder::new(data); let mut decompressed = Vec::new(); decoder .read_to_end(&mut decompressed) .context("Failed to decompress gzip data")?; let mut file = async_fs::File::create(dest_path) .await .with_context(|| format!("Failed to create file: {:?}", dest_path))?; file.write_all(&decompressed) .await .with_context(|| format!("Failed to write to {:?}", dest_path))?; file.flush() .await .with_context(|| format!("Failed to flush {:?}", dest_path))?; Ok(())} /// Extracts files from a zip archive to the destination directory.////// # Arguments/// * `data` - The zip archive data/// * `dest_dir` - The destination directory to extract to/// * `file_filter` - Optional filter function that takes a file name and returns true if it should be extracted./// If None, all files are extracted.////// # Returns/// Returns Ok(()) on success, or an error if extraction fails or if a filter is provided but no files match.////// Note: This function performs synchronous zip reading (which is CPU-bound) followed by/// async file I/O. Similar to `extract_gz`, this approach is acceptable for reasonably-sized/// archives since the zip reading is relatively fast.#[cfg(feature = "local_fs")]pub async fn extract_zip<F>(data: &[u8], dest_dir: &Path, file_filter: Option<F>) -> Result<()>where F: Fn(&str) -> bool,{ // Extract all file data synchronously first let files_to_write: Vec<(PathBuf, Vec<u8>)> = { let cursor = std::io::Cursor::new(data); let mut archive = zip::ZipArchive::new(cursor).context("Failed to read zip archive")?; let mut result = Vec::new(); for i in 0..archive.len() { let mut file = archive.by_index(i).context("Failed to read zip entry")?; let file_name = file.name().to_string(); if let Some(ref filter) = file_filter && !filter(&file_name) { continue; } let outpath = match file.enclosed_name() { Some(path) => dest_dir.join(path), None => continue, }; if !file.is_dir() { let mut data = Vec::new(); file.read_to_end(&mut data) .context("Failed to read file from zip")?; result.push((outpath, data)); } } result }; // archive is dropped here // If a filter was provided but no files matched, return an error if file_filter.is_some() && files_to_write.is_empty() { bail!("No files matched the filter in the zip archive"); } // Now do async file I/O for (outpath, data) in files_to_write { if let Some(parent) = outpath.parent() { async_fs::create_dir_all(parent).await.ok(); } let mut outfile = async_fs::File::create(&outpath) .await .with_context(|| format!("Failed to create file: {:?}", outpath))?; outfile .write_all(&data) .await .with_context(|| format!("Failed to write file: {:?}", outpath))?; outfile .flush() .await .with_context(|| format!("Failed to flush file: {:?}", outpath))?; } Ok(())} /// Finds a working Node.js binary, preferring our custom installation over system node.////// This function checks:/// 1. First, our custom Node.js installation in the Warp data directory/// 2. Falls back to system Node.js if custom isn't available////// # Arguments/// * `path_env_var` - The PATH environment variable to use when checking for system node./// If None, system node will not be checked.////// # Returns/// Returns the path to a working node binary, or `None` if no working node is found./// For system node, returns `PathBuf::from("node")` to let PATH resolution handle it.#[cfg(feature = "local_fs")]pub async fn find_working_node_binary(path_env_var: Option<&str>) -> Option<PathBuf> { // First, try our custom node installation if let Ok(custom_node) = node_binary_path() && custom_node.is_file() { let mut cmd = Command::new(&custom_node); cmd.arg("--version"); if let Ok(output) = cmd.output().await && output.status.success() { log::info!( "Using custom node installation at {}", custom_node.display() ); return Some(custom_node); } } // Fall back to system node if available if let Some(path_env_var) = path_env_var && detect_system_node(path_env_var).await.is_ok() { // System node is available and meets version requirements. // Use "node" and let the PATH resolve it. log::info!("Using system node"); return Some(PathBuf::from("node")); } log::info!("No working node binary found"); None} /// Detects and validates the system-installed Node.js meets the minimum version requirement.////// This function runs `node --version` using the provided PATH environment variable/// and verifies that the installed version meets the minimum requirement.////// # Arguments/// * `path_env_var` - The PATH environment variable value to use when running node////// # Errors/// Returns an error if Node.js is not found or doesn't meet the minimum version.#[cfg(feature = "local_fs")]pub async fn detect_system_node(path_env_var: impl AsRef<OsStr>) -> Result<()> { let path_env_var = path_env_var.as_ref(); // On Windows, we must use `cmd.exe /c node` so that the provided PATH // (set via `.env("PATH", ...)`) is used for executable search. // `CreateProcessW` uses the parent process's PATH, not the child's // `lpEnvironment` PATH, so running `node` directly would find node.exe // via Warp's inherited env rather than the captured interactive PATH. #[cfg(windows)] let output = Command::new("cmd.exe") .args(["/c", "node", "--version"]) .env("PATH", path_env_var) .output() .await .context("Failed to run node --version. Is Node.js installed?")?; #[cfg(not(windows))] let output = Command::new("node") .env("PATH", path_env_var) .arg("--version") .output() .await .context("Failed to run node --version. Is Node.js installed?")?; if !output.status.success() { bail!( "node --version failed: {}", String::from_utf8_lossy(&output.stderr) ); } let version_str = String::from_utf8_lossy(&output.stdout); let version_str = version_str.trim().trim_start_matches('v'); let version = Version::parse(version_str) .with_context(|| format!("Failed to parse Node.js version: {}", version_str))?; if version < MIN_NODE_VERSION { bail!( "System Node.js version {} is too old. Minimum required: {}", version, MIN_NODE_VERSION ); } log::info!("Detected system Node.js {}", version); Ok(())} /// Fetches the latest version of an npm package.////// # ArgumentsLines 1–500 of 535. Load more
//! Node.js and npm runtime management for Warp. //! //! This module provides functionality to install and manage Node.js/npm, //! supporting multiple platforms (macOS, Linux, Windows) and architectures //! (x64, arm64). use anyhow::{Context, Result, bail}; use serde::Deserialize; cfg_if::cfg_if! { if #[cfg(feature = "local_fs")] { use flate2::read::GzDecoder; use futures::io::AsyncWriteExt; use std::ffi::OsStr; use std::io::Read; use std::path::{Path, PathBuf}; use tar::Archive; use command::r#async::Command; use semver::Version; } } /// The pinned Node.js version to install. #[cfg(feature = "local_fs")] const NODE_VERSION: &str = "v22.12.0"; /// Minimum supported Node.js version for system-installed Node. #[cfg(feature = "local_fs")] const MIN_NODE_VERSION: Version = Version::new(20, 0, 0); // Platform-specific paths for Node.js binaries cfg_if::cfg_if! { if #[cfg(all(feature = "local_fs", windows))] { const NODE_BINARY_PATH: &str = "node.exe"; const NPM_BINARY_PATH: &str = "node_modules/npm/bin/npm-cli.js"; } else if #[cfg(feature = "local_fs")] { const NODE_BINARY_PATH: &str = "bin/node"; const NPM_BINARY_PATH: &str = "bin/npm"; } } /// Information about an npm package from the npm registry. #[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct NpmInfo { #[serde(default)] dist_tags: NpmInfoDistTags, } #[derive(Debug, Deserialize, Default)] pub struct NpmInfoDistTags { latest: Option<String>, } impl NpmInfo { /// Returns the latest version of the package. pub fn latest_version(&self) -> Option<&str> { self.dist_tags.latest.as_deref() } } /// Archive type for Node.js distribution. #[cfg(feature = "local_fs")] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ArchiveType { /// A gzip-compressed tarball (for macOS and Linux) TarGz, /// A zip archive (for Windows) Zip, } /// Platform-specific Node.js distribution information. #[cfg(feature = "local_fs")] struct NodeDistribution { os: &'static str, arch: &'static str, archive_type: ArchiveType, } #[cfg(feature = "local_fs")] impl NodeDistribution { /// Determines the Node.js distribution for the current platform. fn current() -> Result<Self> { let os = match std::env::consts::OS { "macos" => "darwin", "linux" => "linux", "windows" => "win", other => bail!("Unsupported operating system: {}", other), }; let arch = match std::env::consts::ARCH { "x86_64" => "x64", "aarch64" => "arm64", other => bail!("Unsupported architecture: {}", other), }; let archive_type = match std::env::consts::OS { "windows" => ArchiveType::Zip, _ => ArchiveType::TarGz, }; Ok(Self { os, arch, archive_type, }) } /// Returns the folder name for the extracted Node.js distribution. fn folder_name(&self, version: &str) -> String { format!("node-{}-{}-{}", version, self.os, self.arch) } /// Returns the file extension for the archive. fn file_extension(&self) -> &'static str { match self.archive_type { ArchiveType::TarGz => "tar.gz", ArchiveType::Zip => "zip", } } /// Returns the download URL for the Node.js distribution. fn download_url(&self, version: &str) -> String { let file_name = format!( "node-{}-{}-{}.{}", version, self.os, self.arch, self.file_extension() ); format!("https://nodejs.org/dist/{}/{}", version, file_name) } } /// Returns the path to the Node.js installation directory. #[cfg(feature = "local_fs")] pub fn node_installation_dir() -> Result<PathBuf> { let dist = NodeDistribution::current()?; let folder_name = dist.folder_name(NODE_VERSION); Ok(warp_core::paths::data_dir().join("node").join(folder_name)) } /// Returns the path to the installed node binary. #[cfg(feature = "local_fs")] pub fn node_binary_path() -> Result<PathBuf> { Ok(node_installation_dir()?.join(NODE_BINARY_PATH)) } /// Returns the path to the installed npm binary/script. #[cfg(feature = "local_fs")] pub fn npm_binary_path() -> Result<PathBuf> { Ok(node_installation_dir()?.join(NPM_BINARY_PATH)) } /// Installs Node.js and npm if not already installed. /// /// This function will: /// 1. Check if a valid Node.js installation already exists /// 2. Download the appropriate Node.js distribution for the current platform /// 3. Extract it to the Warp data directory /// /// # Returns /// Returns the path to the Node.js installation directory on success. /// /// # Errors /// Returns an error if: /// - The current platform/architecture is not supported /// - Network errors occur during download /// - Extraction fails #[cfg(feature = "local_fs")] pub async fn install_npm(client: &http_client::Client) -> Result<PathBuf> { log::info!("Node.js runtime install_npm called"); let dist = NodeDistribution::current()?; let version = NODE_VERSION; let folder_name = dist.folder_name(version); let node_containing_dir = warp_core::paths::data_dir().join("node"); let node_dir = node_containing_dir.join(&folder_name); let node_binary = node_dir.join(NODE_BINARY_PATH); // Check if we already have a valid installation if is_valid_installation(&node_binary).await { log::info!( "Using existing Node.js installation at {}", node_dir.display() ); return Ok(node_dir); } // Remove any existing (potentially corrupted) installation if async_fs::metadata(&node_containing_dir).await.is_ok() { log::info!("Removing existing Node.js directory for clean installation"); async_fs::remove_dir_all(&node_containing_dir) .await .context("Failed to remove existing Node.js directory")?; } // Create the containing directory async_fs::create_dir_all(&node_containing_dir) .await .context("Failed to create Node.js containing directory")?; // Download Node.js let url = dist.download_url(version); log::info!("Downloading Node.js from {}", url); let response = client .get(&url) .send() .await .context("Failed to download Node.js")?; if !response.status().is_success() { bail!( "Node.js download failed with status {}: {}", response.status(), response.text().await.unwrap_or_default() ); } let bytes = response .bytes() .await .context("Failed to read Node.js download response")?; log::info!("Download complete, extracting..."); // Extract the archive match dist.archive_type { ArchiveType::TarGz => { extract_tar_gz(&bytes, &node_containing_dir)?; } ArchiveType::Zip => { extract_zip(&bytes, &node_containing_dir, None::<fn(&str) -> bool>).await?; } } log::info!("Node.js extracted successfully to {}", node_dir.display()); Ok(node_dir) } /// Checks if an existing Node.js installation is valid. #[cfg(feature = "local_fs")] async fn is_valid_installation(node_binary: &Path) -> bool { // Check if the binary exists if async_fs::metadata(node_binary).await.is_err() { return false; } // Try to run node --version to verify it works let result = Command::new(node_binary).arg("--version").output().await; match result { Ok(output) => { if output.status.success() { log::debug!( "Node.js version check passed: {}", String::from_utf8_lossy(&output.stdout).trim() ); true } else { log::warn!( "Node.js version check failed: {}", String::from_utf8_lossy(&output.stderr) ); false } } Err(err) => { log::warn!("Failed to run Node.js binary: {}", err); false } } } /// Extracts a gzip-compressed tarball to the destination directory. /// /// This is a simple wrapper around tar::Archive that extracts all contents. #[cfg(feature = "local_fs")] pub fn extract_tar_gz(data: &[u8], dest_dir: &Path) -> Result<()> { let decoder = GzDecoder::new(data); let mut archive = Archive::new(decoder); archive .unpack(dest_dir) .context("Failed to extract tar.gz archive")?; Ok(()) } /// Extracts a single gzip-compressed file to the destination path. /// /// This decompresses a `.gz` file (not a `.tar.gz` archive) directly to a file. #[cfg(feature = "local_fs")] pub async fn extract_gz(data: &[u8], dest_path: &Path) -> Result<()> { let mut decoder = GzDecoder::new(data); let mut decompressed = Vec::new(); decoder .read_to_end(&mut decompressed) .context("Failed to decompress gzip data")?; let mut file = async_fs::File::create(dest_path) .await .with_context(|| format!("Failed to create file: {:?}", dest_path))?; file.write_all(&decompressed) .await .with_context(|| format!("Failed to write to {:?}", dest_path))?; file.flush() .await .with_context(|| format!("Failed to flush {:?}", dest_path))?; Ok(()) } /// Extracts files from a zip archive to the destination directory. /// /// # Arguments /// * `data` - The zip archive data /// * `dest_dir` - The destination directory to extract to /// * `file_filter` - Optional filter function that takes a file name and returns true if it should be extracted. /// If None, all files are extracted. /// /// # Returns /// Returns Ok(()) on success, or an error if extraction fails or if a filter is provided but no files match. /// /// Note: This function performs synchronous zip reading (which is CPU-bound) followed by /// async file I/O. Similar to `extract_gz`, this approach is acceptable for reasonably-sized /// archives since the zip reading is relatively fast. #[cfg(feature = "local_fs")] pub async fn extract_zip<F>(data: &[u8], dest_dir: &Path, file_filter: Option<F>) -> Result<()> where F: Fn(&str) -> bool, { // Extract all file data synchronously first let files_to_write: Vec<(PathBuf, Vec<u8>)> = { let cursor = std::io::Cursor::new(data); let mut archive = zip::ZipArchive::new(cursor).context("Failed to read zip archive")?; let mut result = Vec::new(); for i in 0..archive.len() { let mut file = archive.by_index(i).context("Failed to read zip entry")?; let file_name = file.name().to_string(); if let Some(ref filter) = file_filter && !filter(&file_name) { continue; } let outpath = match file.enclosed_name() { Some(path) => dest_dir.join(path), None => continue, }; if !file.is_dir() { let mut data = Vec::new(); file.read_to_end(&mut data) .context("Failed to read file from zip")?; result.push((outpath, data)); } } result }; // archive is dropped here // If a filter was provided but no files matched, return an error if file_filter.is_some() && files_to_write.is_empty() { bail!("No files matched the filter in the zip archive"); } // Now do async file I/O for (outpath, data) in files_to_write { if let Some(parent) = outpath.parent() { async_fs::create_dir_all(parent).await.ok(); } let mut outfile = async_fs::File::create(&outpath) .await .with_context(|| format!("Failed to create file: {:?}", outpath))?; outfile .write_all(&data) .await .with_context(|| format!("Failed to write file: {:?}", outpath))?; outfile .flush() .await .with_context(|| format!("Failed to flush file: {:?}", outpath))?; } Ok(()) } /// Finds a working Node.js binary, preferring our custom installation over system node. /// /// This function checks: /// 1. First, our custom Node.js installation in the Warp data directory /// 2. Falls back to system Node.js if custom isn't available /// /// # Arguments /// * `path_env_var` - The PATH environment variable to use when checking for system node. /// If None, system node will not be checked. /// /// # Returns /// Returns the path to a working node binary, or `None` if no working node is found. /// For system node, returns `PathBuf::from("node")` to let PATH resolution handle it. #[cfg(feature = "local_fs")] pub async fn find_working_node_binary(path_env_var: Option<&str>) -> Option<PathBuf> { // First, try our custom node installation if let Ok(custom_node) = node_binary_path() && custom_node.is_file() { let mut cmd = Command::new(&custom_node); cmd.arg("--version"); if let Ok(output) = cmd.output().await && output.status.success() { log::info!( "Using custom node installation at {}", custom_node.display() ); return Some(custom_node); } } // Fall back to system node if available if let Some(path_env_var) = path_env_var && detect_system_node(path_env_var).await.is_ok() { // System node is available and meets version requirements. // Use "node" and let the PATH resolve it. log::info!("Using system node"); return Some(PathBuf::from("node")); } log::info!("No working node binary found"); None } /// Detects and validates the system-installed Node.js meets the minimum version requirement. /// /// This function runs `node --version` using the provided PATH environment variable /// and verifies that the installed version meets the minimum requirement. /// /// # Arguments /// * `path_env_var` - The PATH environment variable value to use when running node /// /// # Errors /// Returns an error if Node.js is not found or doesn't meet the minimum version. #[cfg(feature = "local_fs")] pub async fn detect_system_node(path_env_var: impl AsRef<OsStr>) -> Result<()> { let path_env_var = path_env_var.as_ref(); // On Windows, we must use `cmd.exe /c node` so that the provided PATH // (set via `.env("PATH", ...)`) is used for executable search. // `CreateProcessW` uses the parent process's PATH, not the child's // `lpEnvironment` PATH, so running `node` directly would find node.exe // via Warp's inherited env rather than the captured interactive PATH. #[cfg(windows)] let output = Command::new("cmd.exe") .args(["/c", "node", "--version"]) .env("PATH", path_env_var) .output() .await .context("Failed to run node --version. Is Node.js installed?")?; #[cfg(not(windows))] let output = Command::new("node") .env("PATH", path_env_var) .arg("--version") .output() .await .context("Failed to run node --version. Is Node.js installed?")?; if !output.status.success() { bail!( "node --version failed: {}", String::from_utf8_lossy(&output.stderr) ); } let version_str = String::from_utf8_lossy(&output.stdout); let version_str = version_str.trim().trim_start_matches('v'); let version = Version::parse(version_str) .with_context(|| format!("Failed to parse Node.js version: {}", version_str))?; if version < MIN_NODE_VERSION { bail!( "System Node.js version {} is too old. Minimum required: {}", version, MIN_NODE_VERSION ); } log::info!("Detected system Node.js {}", version); Ok(()) } /// Fetches the latest version of an npm package. /// /// # Arguments