Use our node runtime for ACP extensions (#41955)
Release Notes: - Now ACP extensions use Zed's managed Node.js runtime --------- Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
This commit is contained in:
co-authored by
Mikayla Maki
parent
9fc4abd8de
commit
60ee0dd19b
@@ -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<String, TargetConfig>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String> {
|
||||
struct LocalExtensionArchiveAgent {
|
||||
fs: Arc<dyn Fs>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
node_runtime: NodeRuntime,
|
||||
project_environment: Entity<ProjectEnvironment>,
|
||||
extension_id: Arc<str>,
|
||||
agent_id: Arc<str>,
|
||||
@@ -1379,6 +1382,7 @@ impl ExternalAgentServer for LocalExtensionArchiveAgent {
|
||||
) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user