acp_agent/runtime/
prepare.rs1use std::ffi::OsString;
2use std::path::PathBuf;
3
4use anyhow::{Context, Result, bail};
5
6use crate::commands::install::{
7 download_archive, extract_archive, make_executable, resolve_cmd_path,
8};
9use crate::registry::{BinaryTarget, Environment, Platform, RegistryAgent};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct CommandSpec {
19 pub program: OsString,
21 pub args: Vec<OsString>,
23 pub env: Vec<(OsString, OsString)>,
25 pub current_dir: Option<PathBuf>,
27}
28
29#[derive(Debug)]
30pub struct PreparedCommand {
36 pub spec: CommandSpec,
38 pub temp_dir: Option<tempfile::TempDir>,
40}
41
42pub async fn prepare_agent_command(
49 agent: &RegistryAgent,
50 user_args: &[String],
51) -> Result<PreparedCommand> {
52 if let Some(binary) = &agent.distribution.binary {
53 let platform = Platform::current()?;
54 if let Some(target) = binary.for_platform(platform) {
55 let temp_dir = tokio::task::spawn_blocking(tempfile::tempdir)
56 .await
57 .context("failed to create temporary directory task")?
58 .context("failed to create temporary directory")?;
59 let archive_path = download_archive(target, temp_dir.path()).await?;
60 let extracted_dir = temp_dir.path().join("extracted");
61 tokio::fs::create_dir_all(&extracted_dir)
62 .await
63 .with_context(|| format!("failed to create {}", extracted_dir.display()))?;
64 extract_archive(archive_path, extracted_dir.clone()).await?;
65
66 let executable_path = resolve_cmd_path(&extracted_dir, &target.cmd);
67 let metadata = tokio::fs::metadata(&executable_path).await;
68 if metadata
69 .as_ref()
70 .map(|metadata| !metadata.is_file())
71 .unwrap_or(true)
72 {
73 bail!(
74 "downloaded {}, but could not find \"{}\" at {}",
75 target.archive,
76 target.cmd,
77 executable_path.display()
78 );
79 }
80
81 make_executable(&executable_path).await.with_context(|| {
82 format!("failed to mark {} executable", executable_path.display())
83 })?;
84
85 return Ok(PreparedCommand {
86 spec: binary_command_spec(executable_path, extracted_dir, target, user_args),
87 temp_dir: Some(temp_dir),
88 });
89 }
90 }
91
92 if let Some(npx) = &agent.distribution.npx {
93 return Ok(PreparedCommand {
94 spec: package_command_spec(
95 "npx",
96 &npx.package,
97 npx.args.as_ref(),
98 npx.env.as_ref(),
99 user_args,
100 ),
101 temp_dir: None,
102 });
103 }
104
105 if let Some(uvx) = &agent.distribution.uvx {
106 return Ok(PreparedCommand {
107 spec: package_command_spec(
108 "uvx",
109 &uvx.package,
110 uvx.args.as_ref(),
111 uvx.env.as_ref(),
112 user_args,
113 ),
114 temp_dir: None,
115 });
116 }
117
118 bail!(
119 "agent \"{}\" does not have a runnable distribution",
120 agent.id
121 )
122}
123
124fn package_command_spec(
125 program: &str,
126 package: &str,
127 args: Option<&Vec<String>>,
128 env: Option<&Environment>,
129 user_args: &[String],
130) -> CommandSpec {
131 let mut command_args = vec![OsString::from(package)];
132 if let Some(args) = args {
133 command_args.extend(args.iter().cloned().map(OsString::from));
134 }
135 command_args.extend(user_args.iter().cloned().map(OsString::from));
136
137 CommandSpec {
138 program: OsString::from(program),
139 args: command_args,
140 env: clone_env_pairs(env),
141 current_dir: None,
142 }
143}
144
145fn binary_command_spec(
146 executable_path: PathBuf,
147 extracted_dir: PathBuf,
148 target: &BinaryTarget,
149 user_args: &[String],
150) -> CommandSpec {
151 let mut args: Vec<OsString> = target
152 .args
153 .as_ref()
154 .into_iter()
155 .flatten()
156 .cloned()
157 .map(OsString::from)
158 .collect();
159 args.extend(user_args.iter().cloned().map(OsString::from));
160
161 CommandSpec {
162 program: executable_path.into_os_string(),
163 args,
164 env: clone_env_pairs(target.env.as_ref()),
165 current_dir: Some(extracted_dir),
166 }
167}
168
169fn clone_env_pairs(env: Option<&Environment>) -> Vec<(OsString, OsString)> {
170 env.into_iter()
171 .flat_map(|pairs| pairs.iter())
172 .map(|(key, value)| (OsString::from(key), OsString::from(value)))
173 .collect()
174}