diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index 1e39ceca58fa8b0da450d98db2d6cc8fb0921f12..ab620ec6dbfc023dfdab073e3d4b3aad9413597f 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -164,6 +164,15 @@ pub struct AgentServerManifestEntry { /// args = ["--serve"] /// sha256 = "abc123..." # optional /// ``` + /// + /// For Node.js-based agents, you can use "node" as the cmd to automatically + /// use Zed's managed Node.js runtime instead of relying on the user's PATH: + /// ```toml + /// [agent_servers.nodeagent.targets.darwin-aarch64] + /// archive = "https://example.com/nodeagent.zip" + /// cmd = "node" + /// args = ["index.js", "--port", "3000"] + /// ``` pub targets: HashMap, } diff --git a/crates/project/src/agent_server_store.rs b/crates/project/src/agent_server_store.rs index 8d6781d4eb98b9380d193e2b65c1145bf1bb65a6..c710d96efa275290565a3c31dacf21cc7f7fb17c 100644 --- a/crates/project/src/agent_server_store.rs +++ b/crates/project/src/agent_server_store.rs @@ -259,6 +259,7 @@ impl AgentServerStore { // Insert agent servers from extension manifests match &self.state { AgentServerStoreState::Local { + node_runtime, project_environment, fs, http_client, @@ -289,6 +290,7 @@ impl AgentServerStore { Box::new(LocalExtensionArchiveAgent { fs: fs.clone(), http_client: http_client.clone(), + node_runtime: node_runtime.clone(), project_environment: project_environment.clone(), extension_id: Arc::from(ext_id), agent_id: agent_name.clone(), @@ -1356,6 +1358,7 @@ fn asset_name(version: &str) -> Option { struct LocalExtensionArchiveAgent { fs: Arc, http_client: Arc, + node_runtime: NodeRuntime, project_environment: Entity, extension_id: Arc, agent_id: Arc, @@ -1379,6 +1382,7 @@ impl ExternalAgentServer for LocalExtensionArchiveAgent { ) -> Task)>> { let fs = self.fs.clone(); let http_client = self.http_client.clone(); + let node_runtime = self.node_runtime.clone(); let project_environment = self.project_environment.downgrade(); let extension_id = self.extension_id.clone(); let agent_id = self.agent_id.clone(); @@ -1526,23 +1530,29 @@ impl ExternalAgentServer for LocalExtensionArchiveAgent { // Validate and resolve cmd path let cmd = &target_config.cmd; - if cmd.contains("..") { - anyhow::bail!("command path cannot contain '..': {}", cmd); - } - let cmd_path = if cmd.starts_with("./") || cmd.starts_with(".\\") { - // Relative to extraction directory - version_dir.join(&cmd[2..]) + let cmd_path = if cmd == "node" { + // Use Zed's managed Node.js runtime + node_runtime.binary_path().await? } else { - // On PATH - anyhow::bail!("command must be relative (start with './'): {}", cmd); - }; + if cmd.contains("..") { + anyhow::bail!("command path cannot contain '..': {}", cmd); + } - anyhow::ensure!( - fs.is_file(&cmd_path).await, - "Missing command {} after extraction", - cmd_path.to_string_lossy() - ); + if cmd.starts_with("./") || cmd.starts_with(".\\") { + // Relative to extraction directory + let cmd_path = version_dir.join(&cmd[2..]); + anyhow::ensure!( + fs.is_file(&cmd_path).await, + "Missing command {} after extraction", + cmd_path.to_string_lossy() + ); + cmd_path + } else { + // On PATH + anyhow::bail!("command must be relative (start with './'): {}", cmd); + } + }; let command = AgentServerCommand { path: cmd_path, @@ -1828,6 +1838,7 @@ mod extension_agent_tests { let agent = LocalExtensionArchiveAgent { fs, http_client, + node_runtime: node_runtime::NodeRuntime::unavailable(), project_environment, extension_id: Arc::from("my-extension"), agent_id: Arc::from("my-agent"), @@ -1893,6 +1904,48 @@ mod extension_agent_tests { assert_eq!(target.cmd, "./release-agent"); } + #[gpui::test] + async fn test_node_command_uses_managed_runtime(cx: &mut TestAppContext) { + let fs = fs::FakeFs::new(cx.background_executor.clone()); + let http_client = http_client::FakeHttpClient::with_404_response(); + let node_runtime = NodeRuntime::unavailable(); + let worktree_store = cx.new(|_| WorktreeStore::local(false, fs.clone())); + let project_environment = cx.new(|cx| { + crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx) + }); + + let agent = LocalExtensionArchiveAgent { + fs: fs.clone(), + http_client, + node_runtime, + project_environment, + extension_id: Arc::from("node-extension"), + agent_id: Arc::from("node-agent"), + targets: { + let mut map = HashMap::default(); + map.insert( + "darwin-aarch64".to_string(), + extension::TargetConfig { + archive: "https://example.com/node-agent.zip".into(), + cmd: "node".into(), + args: vec!["index.js".into()], + sha256: None, + }, + ); + map + }, + env: HashMap::default(), + }; + + // Verify that when cmd is "node", it attempts to use the node runtime + assert_eq!(agent.extension_id.as_ref(), "node-extension"); + assert_eq!(agent.agent_id.as_ref(), "node-agent"); + + let target = agent.targets.get("darwin-aarch64").unwrap(); + assert_eq!(target.cmd, "node"); + assert_eq!(target.args, vec!["index.js"]); + } + #[test] fn test_tilde_expansion_in_settings() { let settings = settings::BuiltinAgentServerSettings {