From 1103f3b9d47febe18a35f579f1f2c7e42b94854b Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Wed, 21 Jan 2026 15:52:44 +0100 Subject: [PATCH] acp: Allow running NPM agents from registry (#47291) Release Notes: - N/A --- crates/agent_ui/src/agent_registry_ui.rs | 41 ++-- crates/node_runtime/src/node_runtime.rs | 252 ++++++++++++++------- crates/project/src/agent_registry_store.rs | 156 ++++++++++--- crates/project/src/agent_server_store.rs | 154 ++++++++++--- 4 files changed, 447 insertions(+), 156 deletions(-) diff --git a/crates/agent_ui/src/agent_registry_ui.rs b/crates/agent_ui/src/agent_registry_ui.rs index 40f95d2f5114a0adcdf155d1aa6794261a9f65a7..15b2aed5e292feced7587fb2b562c74dcc7e18df 100644 --- a/crates/agent_ui/src/agent_registry_ui.rs +++ b/crates/agent_ui/src/agent_registry_ui.rs @@ -133,10 +133,10 @@ impl AgentRegistryPage { fn reload_registry_agents(&mut self, cx: &mut Context) { self.registry_agents = self.registry_store.read(cx).agents().to_vec(); self.registry_agents.sort_by(|left, right| { - left.name + left.name() .as_ref() - .cmp(right.name.as_ref()) - .then_with(|| left.id.as_ref().cmp(right.id.as_ref())) + .cmp(right.name().as_ref()) + .then_with(|| left.id().as_ref().cmp(right.id().as_ref())) }); self.filter_registry_agents(cx); } @@ -189,19 +189,19 @@ impl AgentRegistryPage { .filter(|(_, agent)| { // Filter out built-in agents since they already appear in the main // agent configuration UI and don't need to be installed from the registry. - if BUILT_IN_REGISTRY_IDS.contains(&agent.id.as_ref()) { + if BUILT_IN_REGISTRY_IDS.contains(&agent.id().as_ref()) { return false; } let matches_search = search.as_ref().is_none_or(|query| { let query = query.as_str(); - agent.id.as_ref().to_lowercase().contains(query) - || agent.name.as_ref().to_lowercase().contains(query) - || agent.description.as_ref().to_lowercase().contains(query) + agent.id().as_ref().to_lowercase().contains(query) + || agent.name().as_ref().to_lowercase().contains(query) + || agent.description().as_ref().to_lowercase().contains(query) }); let install_status = installed_statuses - .get(agent.id.as_ref()) + .get(agent.id().as_ref()) .copied() .unwrap_or(RegistryInstallStatus::NotInstalled); let matches_filter = match filter { @@ -371,10 +371,10 @@ impl AgentRegistryPage { agent: &RegistryAgent, cx: &mut Context, ) -> AgentRegistryCard { - let install_status = self.install_status(agent.id.as_ref()); - let supports_current_platform = agent.supports_current_platform; + let install_status = self.install_status(agent.id().as_ref()); + let supports_current_platform = agent.supports_current_platform(); - let icon = match agent.icon_path.as_ref() { + let icon = match agent.icon_path() { Some(icon_path) => Icon::from_external_svg(icon_path.clone()), None => Icon::new(IconName::Sparkle), } @@ -384,10 +384,10 @@ impl AgentRegistryPage { let install_button = self.install_button(agent, install_status, supports_current_platform, cx); - let repository_button = agent.repository.as_ref().map(|repository| { + let repository_button = agent.repository().map(|repository| { let repository = repository.clone(); IconButton::new( - SharedString::from(format!("agent-repo-{}", agent.id)), + SharedString::from(format!("agent-repo-{}", agent.id())), IconName::Link, ) .icon_size(IconSize::Small) @@ -409,10 +409,11 @@ impl AgentRegistryPage { .gap_2() .items_end() .child( - Headline::new(agent.name.clone()).size(HeadlineSize::Small), + Headline::new(agent.name().clone()) + .size(HeadlineSize::Small), ) .child( - Headline::new(format!("v{}", agent.version)) + Headline::new(format!("v{}", agent.version())) .size(HeadlineSize::XSmall), ), ), @@ -425,7 +426,7 @@ impl AgentRegistryPage { .gap_2() .justify_between() .child( - Label::new(agent.description.clone()) + Label::new(agent.description().clone()) .size(LabelSize::Small) .color(Color::Default) .truncate(), @@ -437,7 +438,7 @@ impl AgentRegistryPage { .gap_2() .justify_between() .child( - Label::new(format!("ID: {}", agent.id)) + Label::new(format!("ID: {}", agent.id())) .size(LabelSize::Small) .color(Color::Muted) .truncate(), @@ -459,7 +460,7 @@ impl AgentRegistryPage { supports_current_platform: bool, cx: &mut Context, ) -> Button { - let button_id = SharedString::from(format!("install-agent-{}", agent.id)); + let button_id = SharedString::from(format!("install-agent-{}", agent.id())); if !supports_current_platform { return Button::new(button_id, "Unavailable") @@ -470,7 +471,7 @@ impl AgentRegistryPage { match install_status { RegistryInstallStatus::NotInstalled => { let fs = ::global(cx); - let agent_id = agent.id.to_string(); + let agent_id = agent.id().to_string(); Button::new(button_id, "Install") .style(ButtonStyle::Tinted(ui::TintColor::Accent)) .icon(IconName::Download) @@ -496,7 +497,7 @@ impl AgentRegistryPage { } RegistryInstallStatus::InstalledRegistry => { let fs = ::global(cx); - let agent_id = agent.id.to_string(); + let agent_id = agent.id().to_string(); Button::new(button_id, "Remove") .style(ButtonStyle::OutlinedGhost) .on_click(move |_, _, cx| { diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index eb8a5b45797baf7329554cb0b8d4a4f67a1f6579..acd4ebc5221d0fdb8cbdffd745f9d8f0944c5349 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -8,6 +8,7 @@ use semver::Version; use serde::Deserialize; use smol::io::BufReader; use smol::{fs, lock::Mutex}; +use std::collections::HashMap; use std::fmt::Display; use std::{ env::{self, consts}, @@ -30,6 +31,15 @@ pub struct NodeBinaryOptions { pub use_paths: Option<(PathBuf, PathBuf)>, } +/// Use this when you need to launch npm as a long-lived process (for example, an agent server), +/// so the invocation and environment stay consistent with the Node runtime's proxy and CA setup. +#[derive(Clone, Debug)] +pub struct NpmCommand { + pub path: PathBuf, + pub args: Vec, + pub env: HashMap, +} + pub enum VersionStrategy<'a> { /// Install if current version doesn't match pinned version Pin(&'a Version), @@ -86,7 +96,7 @@ impl NodeRuntime { Err(err) => { return Box::new(UnavailableNodeRuntime { error_message: err.to_string().into(), - }); + }) as Box; } } }; @@ -128,7 +138,7 @@ impl NodeRuntime { log::info!("using Node.js found on PATH: {:?}", instance); state.instance = Some(instance.boxed_clone()); state.last_options = Some(options); - return Box::new(instance); + return Box::new(instance) as Box; } Err(err) => Some(err), } @@ -228,6 +238,14 @@ impl NodeRuntime { .await } + pub async fn npm_command(&self, subcommand: &str, args: &[&str]) -> Result { + let http = self.0.lock().await.http.clone(); + self.instance() + .await + .npm_command(http.proxy(), subcommand, args) + .await + } + pub async fn npm_package_latest_version(&self, name: &str) -> Result { let http = self.0.lock().await.http.clone(); let output = self @@ -352,6 +370,13 @@ trait NodeRuntimeTrait: Send + Sync { args: &[&str], ) -> Result; + async fn npm_command( + &self, + proxy: Option<&Url>, + subcommand: &str, + args: &[&str], + ) -> Result; + async fn npm_package_installed_version( &self, local_package_directory: &Path, @@ -528,40 +553,12 @@ impl NodeRuntimeTrait for ManagedNodeRuntime { subcommand: &str, args: &[&str], ) -> Result { - let attempt = || async move { - let node_binary = self.installation_path.join(Self::NODE_PATH); - let npm_file = self.installation_path.join(Self::NPM_PATH); - let env_path = path_with_node_binary_prepended(&node_binary).unwrap_or_default(); - - anyhow::ensure!( - smol::fs::metadata(&node_binary).await.is_ok(), - "missing node binary file" - ); - anyhow::ensure!( - smol::fs::metadata(&npm_file).await.is_ok(), - "missing npm file" - ); - - let node_ca_certs = env::var(NODE_CA_CERTS_ENV_VAR).unwrap_or_else(|_| String::new()); - - let mut command = util::command::new_smol_command(node_binary); - command.env("PATH", env_path); - command.env(NODE_CA_CERTS_ENV_VAR, node_ca_certs); - command.arg(npm_file).arg(subcommand); - command.arg(format!( - "--cache={}", - self.installation_path.join("cache").display() - )); - command.args([ - "--userconfig".into(), - self.installation_path.join("blank_user_npmrc"), - ]); - command.args([ - "--globalconfig".into(), - self.installation_path.join("blank_global_npmrc"), - ]); - command.args(args); - configure_npm_command(&mut command, directory, proxy); + let attempt = || async { + let npm_command = self.npm_command(proxy, subcommand, args).await?; + let mut command = util::command::new_smol_command(npm_command.path); + command.args(npm_command.args); + command.envs(npm_command.env); + configure_npm_command(&mut command, directory); command.output().await.map_err(|e| anyhow!("{e}")) }; @@ -586,6 +583,43 @@ impl NodeRuntimeTrait for ManagedNodeRuntime { output.map_err(|e| anyhow!("{e}")) } + + async fn npm_command( + &self, + proxy: Option<&Url>, + subcommand: &str, + args: &[&str], + ) -> Result { + let node_binary = self.installation_path.join(Self::NODE_PATH); + let npm_file = self.installation_path.join(Self::NPM_PATH); + + anyhow::ensure!( + smol::fs::metadata(&node_binary).await.is_ok(), + "missing node binary file" + ); + anyhow::ensure!( + smol::fs::metadata(&npm_file).await.is_ok(), + "missing npm file" + ); + + let command_args = build_npm_command_args( + Some(&npm_file), + &self.installation_path.join("cache"), + Some(&self.installation_path.join("blank_user_npmrc")), + Some(&self.installation_path.join("blank_global_npmrc")), + proxy, + subcommand, + args, + ); + let command_env = npm_command_env(Some(&node_binary)); + + Ok(NpmCommand { + path: node_binary, + args: command_args, + env: command_env, + }) + } + async fn npm_package_installed_version( &self, local_package_directory: &Path, @@ -688,19 +722,11 @@ impl NodeRuntimeTrait for SystemNodeRuntime { subcommand: &str, args: &[&str], ) -> anyhow::Result { - let node_ca_certs = env::var(NODE_CA_CERTS_ENV_VAR).unwrap_or_else(|_| String::new()); - let mut command = util::command::new_smol_command(self.npm.clone()); - let path = path_with_node_binary_prepended(&self.node).unwrap_or_default(); - command - .env("PATH", path) - .env(NODE_CA_CERTS_ENV_VAR, node_ca_certs) - .arg(subcommand) - .arg(format!( - "--cache={}", - self.scratch_dir.join("cache").display() - )) - .args(args); - configure_npm_command(&mut command, directory, proxy); + let npm_command = self.npm_command(proxy, subcommand, args).await?; + let mut command = util::command::new_smol_command(npm_command.path); + command.args(npm_command.args); + command.envs(npm_command.env); + configure_npm_command(&mut command, directory); let output = command.output().await?; anyhow::ensure!( output.status.success(), @@ -711,6 +737,30 @@ impl NodeRuntimeTrait for SystemNodeRuntime { Ok(output) } + async fn npm_command( + &self, + proxy: Option<&Url>, + subcommand: &str, + args: &[&str], + ) -> Result { + let command_args = build_npm_command_args( + None, + &self.scratch_dir.join("cache"), + None, + None, + proxy, + subcommand, + args, + ); + let command_env = npm_command_env(Some(&self.node)); + + Ok(NpmCommand { + path: self.npm.clone(), + args: command_args, + env: command_env, + }) + } + async fn npm_package_installed_version( &self, local_package_directory: &Path, @@ -773,6 +823,15 @@ impl NodeRuntimeTrait for UnavailableNodeRuntime { bail!("{}", self.error_message) } + async fn npm_command( + &self, + _proxy: Option<&Url>, + _subcommand: &str, + _args: &[&str], + ) -> Result { + bail!("{}", self.error_message) + } + async fn npm_package_installed_version( &self, _local_package_directory: &Path, @@ -782,59 +841,101 @@ impl NodeRuntimeTrait for UnavailableNodeRuntime { } } -fn configure_npm_command( - command: &mut smol::process::Command, - directory: Option<&Path>, - proxy: Option<&Url>, -) { +fn configure_npm_command(command: &mut smol::process::Command, directory: Option<&Path>) { if let Some(directory) = directory { command.current_dir(directory); command.args(["--prefix".into(), directory.to_path_buf()]); } +} + +fn proxy_argument(proxy: Option<&Url>) -> Option { + let mut proxy = proxy.cloned()?; + // Map proxy settings from `http://localhost:10809` to `http://127.0.0.1:10809` + // NodeRuntime without environment information can not parse `localhost` + // correctly. + // TODO: map to `[::1]` if we are using ipv6 + if matches!(proxy.host(), Some(Host::Domain(domain)) if domain.eq_ignore_ascii_case("localhost")) + { + // When localhost is a valid Host, so is `127.0.0.1` + let _ = proxy.set_ip_host(IpAddr::V4(Ipv4Addr::LOCALHOST)); + } - if let Some(mut proxy) = proxy.cloned() { - // Map proxy settings from `http://localhost:10809` to `http://127.0.0.1:10809` - // NodeRuntime without environment information can not parse `localhost` - // correctly. - // TODO: map to `[::1]` if we are using ipv6 - if matches!(proxy.host(), Some(Host::Domain(domain)) if domain.eq_ignore_ascii_case("localhost")) - { - // When localhost is a valid Host, so is `127.0.0.1` - let _ = proxy.set_ip_host(IpAddr::V4(Ipv4Addr::LOCALHOST)); - } + Some(proxy.as_str().to_string()) +} - command.args(["--proxy", proxy.as_str()]); +fn build_npm_command_args( + entrypoint: Option<&Path>, + cache_dir: &Path, + user_config: Option<&Path>, + global_config: Option<&Path>, + proxy: Option<&Url>, + subcommand: &str, + args: &[&str], +) -> Vec { + let mut command_args = Vec::new(); + if let Some(entrypoint) = entrypoint { + command_args.push(entrypoint.to_string_lossy().into_owned()); + } + command_args.push(subcommand.to_string()); + command_args.push(format!("--cache={}", cache_dir.display())); + if let Some(user_config) = user_config { + command_args.push("--userconfig".into()); + command_args.push(user_config.to_string_lossy().into_owned()); + } + if let Some(global_config) = global_config { + command_args.push("--globalconfig".into()); + command_args.push(global_config.to_string_lossy().into_owned()); + } + if let Some(proxy_arg) = proxy_argument(proxy) { + command_args.push("--proxy".into()); + command_args.push(proxy_arg); + } + command_args.extend(args.into_iter().map(|a| a.to_string())); + command_args +} + +fn npm_command_env(node_binary: Option<&Path>) -> HashMap { + let mut command_env = HashMap::new(); + if let Some(node_binary) = node_binary { + let env_path = path_with_node_binary_prepended(node_binary).unwrap_or_default(); + command_env.insert("PATH".into(), env_path.to_string_lossy().into_owned()); + } + + if let Ok(node_ca_certs) = env::var(NODE_CA_CERTS_ENV_VAR) { + if !node_ca_certs.is_empty() { + command_env.insert(NODE_CA_CERTS_ENV_VAR.to_string(), node_ca_certs); + } } #[cfg(windows)] { - // SYSTEMROOT is a critical environment variables for Windows. if let Some(val) = env::var("SYSTEMROOT") .context("Missing environment variable: SYSTEMROOT!") .log_err() { - command.env("SYSTEMROOT", val); + command_env.insert("SYSTEMROOT".into(), val); } - // Without ComSpec, the post-install will always fail. if let Some(val) = env::var("ComSpec") .context("Missing environment variable: ComSpec!") .log_err() { - command.env("ComSpec", val); + command_env.insert("ComSpec".into(), val); } } + + command_env } #[cfg(test)] mod tests { use http_client::Url; - use super::configure_npm_command; + use super::proxy_argument; // Map localhost to 127.0.0.1 // NodeRuntime without environment information can not parse `localhost` correctly. #[test] - fn test_configure_npm_command_map_localhost_proxy() { + fn test_proxy_argument_map_localhost_proxy() { const CASES: [(&str, &str); 4] = [ // Map localhost to 127.0.0.1 ("http://localhost:9090/", "http://127.0.0.1:9090/"), @@ -851,15 +952,8 @@ mod tests { ]; for (proxy, mapped_proxy) in CASES { - let mut dummy = smol::process::Command::new(""); let proxy = Url::parse(proxy).unwrap(); - configure_npm_command(&mut dummy, None, Some(&proxy)); - let proxy = dummy - .get_args() - .skip_while(|&arg| arg != "--proxy") - .skip(1) - .next(); - let proxy = proxy.expect("Proxy was not passed to Command correctly"); + let proxy = proxy_argument(Some(&proxy)).expect("Proxy was not passed correctly"); assert_eq!( proxy, mapped_proxy, "Incorrectly mapped localhost to 127.0.0.1" diff --git a/crates/project/src/agent_registry_store.rs b/crates/project/src/agent_registry_store.rs index 2a59c77fb550fe068bc0fb17271cfe954b2b07ad..838114c3edb96fed4fde3614dfd65be69496b639 100644 --- a/crates/project/src/agent_registry_store.rs +++ b/crates/project/src/agent_registry_store.rs @@ -15,17 +15,76 @@ const REGISTRY_URL: &str = const REGISTRY_REFRESH_INTERVAL: Duration = Duration::from_secs(60 * 60); #[derive(Clone, Debug)] -pub struct RegistryAgent { +pub struct RegistryAgentMetadata { pub id: SharedString, pub name: SharedString, pub description: SharedString, pub version: SharedString, pub repository: Option, pub icon_path: Option, +} + +#[derive(Clone, Debug)] +pub struct RegistryBinaryAgent { + pub metadata: RegistryAgentMetadata, pub targets: HashMap, pub supports_current_platform: bool, } +#[derive(Clone, Debug)] +pub struct RegistryNpxAgent { + pub metadata: RegistryAgentMetadata, + pub package: SharedString, + pub args: Vec, + pub env: HashMap, +} + +#[derive(Clone, Debug)] +pub enum RegistryAgent { + Binary(RegistryBinaryAgent), + Npx(RegistryNpxAgent), +} + +impl RegistryAgent { + pub fn metadata(&self) -> &RegistryAgentMetadata { + match self { + RegistryAgent::Binary(agent) => &agent.metadata, + RegistryAgent::Npx(agent) => &agent.metadata, + } + } + + pub fn id(&self) -> &SharedString { + &self.metadata().id + } + + pub fn name(&self) -> &SharedString { + &self.metadata().name + } + + pub fn description(&self) -> &SharedString { + &self.metadata().description + } + + pub fn version(&self) -> &SharedString { + &self.metadata().version + } + + pub fn repository(&self) -> Option<&SharedString> { + self.metadata().repository.as_ref() + } + + pub fn icon_path(&self) -> Option<&SharedString> { + self.metadata().icon_path.as_ref() + } + + pub fn supports_current_platform(&self) -> bool { + match self { + RegistryAgent::Binary(agent) => agent.supports_current_platform, + RegistryAgent::Npx(_) => true, + } + } +} + #[derive(Clone, Debug)] pub struct RegistryTargetConfig { pub archive: String, @@ -81,7 +140,7 @@ impl AgentRegistryStore { } pub fn agent(&self, id: &str) -> Option<&RegistryAgent> { - self.agents.iter().find(|agent| agent.id == id) + self.agents.iter().find(|agent| agent.id().as_ref() == id) } pub fn is_fetching(&self) -> bool { @@ -247,32 +306,6 @@ async fn build_registry_agents( let mut agents = Vec::new(); for entry in index.agents { - let Some(binary) = entry.distribution.binary.as_ref() else { - continue; - }; - - if binary.is_empty() { - continue; - } - - let mut targets = HashMap::default(); - for (platform, target) in binary.iter() { - targets.insert( - platform.clone(), - RegistryTargetConfig { - archive: target.archive.clone(), - cmd: target.cmd.clone(), - args: target.args.clone(), - sha256: None, - env: target.env.clone(), - }, - ); - } - - let supports_current_platform = current_platform - .as_ref() - .is_some_and(|platform| targets.contains_key(*platform)); - let icon_path = resolve_icon_path( &entry, &icons_dir, @@ -282,16 +315,66 @@ async fn build_registry_agents( ) .await?; - agents.push(RegistryAgent { + let metadata = RegistryAgentMetadata { id: entry.id.into(), name: entry.name.into(), description: entry.description.into(), version: entry.version.into(), repository: entry.repository.map(Into::into), icon_path, - targets, - supports_current_platform, + }; + + let binary_agent = entry.distribution.binary.as_ref().and_then(|binary| { + if binary.is_empty() { + return None; + } + + let mut targets = HashMap::default(); + for (platform, target) in binary.iter() { + targets.insert( + platform.clone(), + RegistryTargetConfig { + archive: target.archive.clone(), + cmd: target.cmd.clone(), + args: target.args.clone(), + sha256: None, + env: target.env.clone(), + }, + ); + } + + let supports_current_platform = current_platform + .as_ref() + .is_some_and(|platform| targets.contains_key(*platform)); + + Some(RegistryBinaryAgent { + metadata: metadata.clone(), + targets, + supports_current_platform, + }) }); + + let npx_agent = entry.distribution.npx.as_ref().map(|npx| RegistryNpxAgent { + metadata: metadata.clone(), + package: npx.package.clone().into(), + args: npx.args.clone(), + env: npx.env.clone(), + }); + + let agent = match (binary_agent, npx_agent) { + (Some(binary_agent), Some(npx_agent)) => { + if binary_agent.supports_current_platform { + RegistryAgent::Binary(binary_agent) + } else { + RegistryAgent::Npx(npx_agent) + } + } + (Some(binary_agent), None) => RegistryAgent::Binary(binary_agent), + (None, Some(npx_agent)) => RegistryAgent::Npx(npx_agent), + (None, None) => continue, + }; + + agents.push(agent); } Ok(agents) @@ -447,6 +530,8 @@ struct RegistryEntry { struct RegistryDistribution { #[serde(default)] binary: Option>, + #[serde(default)] + npx: Option, } #[derive(Deserialize)] @@ -458,3 +543,12 @@ struct RegistryBinaryTarget { #[serde(default)] env: HashMap, } + +#[derive(Deserialize)] +struct RegistryNpxDistribution { + package: String, + #[serde(default)] + args: Vec, + #[serde(default)] + env: HashMap, +} diff --git a/crates/project/src/agent_server_store.rs b/crates/project/src/agent_server_store.rs index 999a0b3e84e490207d0b00e7302e35716411a0db..44f0373acfb44dd48ce48f39b710e9ca906c273a 100644 --- a/crates/project/src/agent_server_store.rs +++ b/crates/project/src/agent_server_store.rs @@ -30,7 +30,7 @@ use task::{Shell, SpawnInTerminal}; use util::{ResultExt as _, debug_panic}; use crate::ProjectEnvironment; -use crate::agent_registry_store::{AgentRegistryStore, RegistryTargetConfig}; +use crate::agent_registry_store::{AgentRegistryStore, RegistryAgent, RegistryTargetConfig}; #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)] pub struct AgentServerCommand { @@ -718,7 +718,7 @@ impl AgentServerStore { .agents() .iter() .cloned() - .map(|agent| (agent.id.to_string(), agent)) + .map(|agent| (agent.id().to_string(), agent)) .collect::>() }) .unwrap_or_default(); @@ -747,32 +747,57 @@ impl AgentServerStore { } continue; }; - if !agent.supports_current_platform { - log::warn!( - "Registry agent '{}' has no compatible binary for this platform", - name - ); - continue; - } let agent_name = ExternalAgentServerName(name.clone().into()); - self.external_agents.insert( - agent_name.clone(), - ExternalAgentEntry::new( - Box::new(LocalRegistryArchiveAgent { - fs: fs.clone(), - http_client: http_client.clone(), - node_runtime: node_runtime.clone(), - project_environment: project_environment.clone(), - registry_id: Arc::from(name.as_str()), - targets: agent.targets.clone(), - env: env.clone(), - }) as Box, - ExternalAgentSource::Registry, - agent.icon_path.clone(), - Some(agent.name.clone()), - ), - ); + match agent { + RegistryAgent::Binary(agent) => { + if !agent.supports_current_platform { + log::warn!( + "Registry agent '{}' has no compatible binary for this platform", + name + ); + continue; + } + + self.external_agents.insert( + agent_name.clone(), + ExternalAgentEntry::new( + Box::new(LocalRegistryArchiveAgent { + fs: fs.clone(), + http_client: http_client.clone(), + node_runtime: node_runtime.clone(), + project_environment: project_environment.clone(), + registry_id: Arc::from(name.as_str()), + targets: agent.targets.clone(), + env: env.clone(), + }) + as Box, + ExternalAgentSource::Registry, + agent.metadata.icon_path.clone(), + Some(agent.metadata.name.clone()), + ), + ); + } + RegistryAgent::Npx(agent) => { + self.external_agents.insert( + agent_name.clone(), + ExternalAgentEntry::new( + Box::new(LocalRegistryNpxAgent { + node_runtime: node_runtime.clone(), + project_environment: project_environment.clone(), + package: agent.package.clone(), + args: agent.args.clone(), + distribution_env: agent.env.clone(), + settings_env: env.clone(), + }) + as Box, + ExternalAgentSource::Registry, + agent.metadata.icon_path.clone(), + Some(agent.metadata.name.clone()), + ), + ); + } + } } CustomAgentServerSettings::Extension { .. } => {} } @@ -2310,6 +2335,83 @@ impl ExternalAgentServer for LocalRegistryArchiveAgent { } } +struct LocalRegistryNpxAgent { + node_runtime: NodeRuntime, + project_environment: Entity, + package: SharedString, + args: Vec, + distribution_env: HashMap, + settings_env: HashMap, +} + +impl ExternalAgentServer for LocalRegistryNpxAgent { + fn get_command( + &mut self, + root_dir: Option<&str>, + extra_env: HashMap, + _status_tx: Option>, + _new_version_available_tx: Option>>, + cx: &mut AsyncApp, + ) -> Task)>> { + let node_runtime = self.node_runtime.clone(); + let project_environment = self.project_environment.downgrade(); + let package = self.package.clone(); + let args = self.args.clone(); + let distribution_env = self.distribution_env.clone(); + let settings_env = self.settings_env.clone(); + + let env_root_dir: Arc = root_dir + .map(|root_dir| Path::new(root_dir)) + .unwrap_or(paths::home_dir()) + .into(); + + cx.spawn(async move |cx| { + let mut env = project_environment + .update(cx, |project_environment, cx| { + project_environment.local_directory_environment( + &Shell::System, + env_root_dir.clone(), + cx, + ) + })? + .await + .unwrap_or_default(); + + let mut exec_args = Vec::new(); + exec_args.push("--yes".to_string()); + exec_args.push(package.to_string()); + if !args.is_empty() { + exec_args.push("--".to_string()); + exec_args.extend(args); + } + + let npm_command = node_runtime + .npm_command( + "exec", + &exec_args.iter().map(|a| a.as_str()).collect::>(), + ) + .await?; + + env.extend(npm_command.env); + env.extend(distribution_env); + env.extend(extra_env); + env.extend(settings_env); + + let command = AgentServerCommand { + path: npm_command.path, + args: npm_command.args, + env: Some(env), + }; + + Ok((command, env_root_dir.to_string_lossy().into_owned(), None)) + }) + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + struct LocalCustomAgent { project_environment: Entity, command: AgentServerCommand,