Detailed changes
@@ -133,10 +133,10 @@ impl AgentRegistryPage {
fn reload_registry_agents(&mut self, cx: &mut Context<Self>) {
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<Self>,
) -> 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<Self>,
) -> 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 = <dyn 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 = <dyn 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| {
@@ -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<String>,
+ pub env: HashMap<String, String>,
+}
+
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<dyn NodeRuntimeTrait>;
}
}
};
@@ -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<dyn NodeRuntimeTrait>;
}
Err(err) => Some(err),
}
@@ -228,6 +238,14 @@ impl NodeRuntime {
.await
}
+ pub async fn npm_command(&self, subcommand: &str, args: &[&str]) -> Result<NpmCommand> {
+ 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<Version> {
let http = self.0.lock().await.http.clone();
let output = self
@@ -352,6 +370,13 @@ trait NodeRuntimeTrait: Send + Sync {
args: &[&str],
) -> Result<Output>;
+ async fn npm_command(
+ &self,
+ proxy: Option<&Url>,
+ subcommand: &str,
+ args: &[&str],
+ ) -> Result<NpmCommand>;
+
async fn npm_package_installed_version(
&self,
local_package_directory: &Path,
@@ -528,40 +553,12 @@ impl NodeRuntimeTrait for ManagedNodeRuntime {
subcommand: &str,
args: &[&str],
) -> Result<Output> {
- 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<NpmCommand> {
+ 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<Output> {
- 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<NpmCommand> {
+ 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<NpmCommand> {
+ 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<String> {
+ 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<String> {
+ 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<String, String> {
+ 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"
@@ -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<SharedString>,
pub icon_path: Option<SharedString>,
+}
+
+#[derive(Clone, Debug)]
+pub struct RegistryBinaryAgent {
+ pub metadata: RegistryAgentMetadata,
pub targets: HashMap<String, RegistryTargetConfig>,
pub supports_current_platform: bool,
}
+#[derive(Clone, Debug)]
+pub struct RegistryNpxAgent {
+ pub metadata: RegistryAgentMetadata,
+ pub package: SharedString,
+ pub args: Vec<String>,
+ pub env: HashMap<String, String>,
+}
+
+#[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<HashMap<String, RegistryBinaryTarget>>,
+ #[serde(default)]
+ npx: Option<RegistryNpxDistribution>,
}
#[derive(Deserialize)]
@@ -458,3 +543,12 @@ struct RegistryBinaryTarget {
#[serde(default)]
env: HashMap<String, String>,
}
+
+#[derive(Deserialize)]
+struct RegistryNpxDistribution {
+ package: String,
+ #[serde(default)]
+ args: Vec<String>,
+ #[serde(default)]
+ env: HashMap<String, String>,
+}
@@ -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::<HashMap<_, _>>()
})
.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<dyn ExternalAgentServer>,
- 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<dyn ExternalAgentServer>,
+ 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<dyn ExternalAgentServer>,
+ 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<ProjectEnvironment>,
+ package: SharedString,
+ args: Vec<String>,
+ distribution_env: HashMap<String, String>,
+ settings_env: HashMap<String, String>,
+}
+
+impl ExternalAgentServer for LocalRegistryNpxAgent {
+ fn get_command(
+ &mut self,
+ root_dir: Option<&str>,
+ extra_env: HashMap<String, String>,
+ _status_tx: Option<watch::Sender<SharedString>>,
+ _new_version_available_tx: Option<watch::Sender<Option<String>>>,
+ cx: &mut AsyncApp,
+ ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
+ 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<Path> = 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::<Vec<_>>(),
+ )
+ .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<ProjectEnvironment>,
command: AgentServerCommand,