1use anyhow::{Context as _, Result, anyhow};
2use futures::AsyncReadExt;
3use futures::io::BufReader;
4use http_client::{AsyncBody, HttpClient, Request as HttpRequest};
5use paths::supermaven_dir;
6use serde::{Deserialize, Serialize};
7use smol::fs::{self, File};
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11use util::fs::{make_file_executable, remove_matching};
12
13#[derive(Serialize)]
14pub struct GetExternalUserRequest {
15 pub id: String,
16}
17
18#[derive(Serialize)]
19pub struct CreateExternalUserRequest {
20 pub id: String,
21 pub email: String,
22}
23
24#[derive(Serialize)]
25pub struct DeleteExternalUserRequest {
26 pub id: String,
27}
28
29#[derive(Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct CreateExternalUserResponse {
32 pub api_key: String,
33}
34
35#[derive(Deserialize)]
36pub struct SupermavenApiError {
37 pub message: String,
38}
39
40pub struct SupermavenBinary {}
41
42pub struct SupermavenAdminApi {
43 admin_api_key: String,
44 api_url: String,
45 http_client: Arc<dyn HttpClient>,
46}
47
48#[derive(Debug, Deserialize)]
49#[serde(rename_all = "camelCase")]
50pub struct SupermavenDownloadResponse {
51 pub download_url: String,
52 pub version: u64,
53 pub sha256_hash: String,
54}
55
56#[derive(Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct SupermavenUser {
59 #[expect(
60 unused,
61 reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove"
62 )]
63 id: String,
64 #[expect(
65 unused,
66 reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove"
67 )]
68 email: String,
69 api_key: String,
70}
71
72impl SupermavenAdminApi {
73 pub fn new(admin_api_key: String, http_client: Arc<dyn HttpClient>) -> Self {
74 Self {
75 admin_api_key,
76 api_url: "https://supermaven.com/api/".to_string(),
77 http_client,
78 }
79 }
80
81 pub async fn try_get_user(
82 &self,
83 request: GetExternalUserRequest,
84 ) -> Result<Option<SupermavenUser>> {
85 let uri = format!("{}external-user/{}", &self.api_url, &request.id);
86
87 let request = HttpRequest::get(&uri).header("Authorization", self.admin_api_key.clone());
88
89 let mut response = self
90 .http_client
91 .send(request.body(AsyncBody::default())?)
92 .await
93 .with_context(|| "Unable to get Supermaven API Key".to_string())?;
94
95 let mut body = Vec::new();
96 response.body_mut().read_to_end(&mut body).await?;
97
98 if response.status().is_client_error() {
99 let error: SupermavenApiError = serde_json::from_slice(&body)?;
100 if error.message == "User not found" {
101 return Ok(None);
102 } else {
103 anyhow::bail!("Supermaven API error: {}", error.message);
104 }
105 } else if response.status().is_server_error() {
106 let error: SupermavenApiError = serde_json::from_slice(&body)?;
107 return Err(anyhow!("Supermaven API server error").context(error.message));
108 }
109
110 let body_str = std::str::from_utf8(&body)?;
111
112 Ok(Some(
113 serde_json::from_str::<SupermavenUser>(body_str)
114 .with_context(|| "Unable to parse Supermaven user response".to_string())?,
115 ))
116 }
117
118 pub async fn try_create_user(
119 &self,
120 request: CreateExternalUserRequest,
121 ) -> Result<CreateExternalUserResponse> {
122 let uri = format!("{}external-user", &self.api_url);
123
124 let request = HttpRequest::post(&uri)
125 .header("Authorization", self.admin_api_key.clone())
126 .body(AsyncBody::from(serde_json::to_vec(&request)?))?;
127
128 let mut response = self
129 .http_client
130 .send(request)
131 .await
132 .with_context(|| "Unable to create Supermaven API Key".to_string())?;
133
134 let mut body = Vec::new();
135 response.body_mut().read_to_end(&mut body).await?;
136
137 let body_str = std::str::from_utf8(&body)?;
138
139 if !response.status().is_success() {
140 let error: SupermavenApiError = serde_json::from_slice(&body)?;
141 return Err(anyhow!("Supermaven API server error").context(error.message));
142 }
143
144 serde_json::from_str::<CreateExternalUserResponse>(body_str)
145 .with_context(|| "Unable to parse Supermaven API Key response".to_string())
146 }
147
148 pub async fn try_delete_user(&self, request: DeleteExternalUserRequest) -> Result<()> {
149 let uri = format!("{}external-user/{}", &self.api_url, &request.id);
150
151 let request = HttpRequest::delete(&uri).header("Authorization", self.admin_api_key.clone());
152
153 let mut response = self
154 .http_client
155 .send(request.body(AsyncBody::default())?)
156 .await
157 .with_context(|| "Unable to delete Supermaven User".to_string())?;
158
159 let mut body = Vec::new();
160 response.body_mut().read_to_end(&mut body).await?;
161
162 if response.status().is_client_error() {
163 let error: SupermavenApiError = serde_json::from_slice(&body)?;
164 if error.message == "User not found" {
165 return Ok(());
166 } else {
167 anyhow::bail!("Supermaven API error: {}", error.message);
168 }
169 } else if response.status().is_server_error() {
170 let error: SupermavenApiError = serde_json::from_slice(&body)?;
171 return Err(anyhow!("Supermaven API server error").context(error.message));
172 }
173
174 Ok(())
175 }
176
177 pub async fn try_get_or_create_user(
178 &self,
179 request: CreateExternalUserRequest,
180 ) -> Result<CreateExternalUserResponse> {
181 let get_user_request = GetExternalUserRequest {
182 id: request.id.clone(),
183 };
184
185 match self.try_get_user(get_user_request).await? {
186 None => self.try_create_user(request).await,
187 Some(SupermavenUser { api_key, .. }) => Ok(CreateExternalUserResponse { api_key }),
188 }
189 }
190}
191
192pub async fn latest_release(
193 client: Arc<dyn HttpClient>,
194 platform: &str,
195 arch: &str,
196) -> Result<SupermavenDownloadResponse> {
197 let uri = format!(
198 "https://supermaven.com/api/download-path?platform={}&arch={}",
199 platform, arch
200 );
201
202 // Download is not authenticated
203 let request = HttpRequest::get(&uri);
204
205 let mut response = client
206 .send(request.body(AsyncBody::default())?)
207 .await
208 .with_context(|| "Unable to acquire Supermaven Agent".to_string())?;
209
210 let mut body = Vec::new();
211 response.body_mut().read_to_end(&mut body).await?;
212
213 if response.status().is_client_error() || response.status().is_server_error() {
214 let body_str = std::str::from_utf8(&body)?;
215 let error: SupermavenApiError = serde_json::from_str(body_str)?;
216 anyhow::bail!("Supermaven API error: {}", error.message);
217 }
218
219 serde_json::from_slice::<SupermavenDownloadResponse>(&body)
220 .with_context(|| "Unable to parse Supermaven Agent response".to_string())
221}
222
223pub fn version_path(version: u64) -> PathBuf {
224 supermaven_dir().join(format!(
225 "sm-agent-{}{}",
226 version,
227 std::env::consts::EXE_SUFFIX
228 ))
229}
230
231pub async fn has_version(version_path: &Path) -> bool {
232 fs::metadata(version_path).await.is_ok_and(|m| m.is_file())
233}
234
235pub async fn get_supermaven_agent_path(client: Arc<dyn HttpClient>) -> Result<PathBuf> {
236 fs::create_dir_all(supermaven_dir())
237 .await
238 .with_context(|| {
239 format!(
240 "Could not create Supermaven Agent Directory at {:?}",
241 supermaven_dir()
242 )
243 })?;
244
245 let platform = match std::env::consts::OS {
246 "macos" => "darwin",
247 "windows" => "windows",
248 "linux" => "linux",
249 unsupported => anyhow::bail!("unsupported platform {unsupported}"),
250 };
251
252 let arch = match std::env::consts::ARCH {
253 "x86_64" => "amd64",
254 "aarch64" => "arm64",
255 unsupported => anyhow::bail!("unsupported architecture {unsupported}"),
256 };
257
258 let download_info = latest_release(client.clone(), platform, arch).await?;
259
260 let binary_path = version_path(download_info.version);
261
262 if has_version(&binary_path).await {
263 // Due to an issue with the Supermaven binary not being made executable on
264 // earlier Zed versions and Supermaven releases not occurring that frequently,
265 // we ensure here that the found binary is actually executable.
266 make_file_executable(&binary_path).await?;
267
268 return Ok(binary_path);
269 }
270
271 let request = HttpRequest::get(&download_info.download_url);
272
273 let mut response = client
274 .send(request.body(AsyncBody::default())?)
275 .await
276 .with_context(|| "Unable to download Supermaven Agent".to_string())?;
277
278 let mut file = File::create(&binary_path)
279 .await
280 .with_context(|| format!("Unable to create file at {:?}", binary_path))?;
281
282 futures::io::copy(BufReader::new(response.body_mut()), &mut file)
283 .await
284 .with_context(|| format!("Unable to write binary to file at {:?}", binary_path))?;
285
286 make_file_executable(&binary_path).await?;
287
288 remove_matching(supermaven_dir(), |file| file != binary_path).await;
289
290 Ok(binary_path)
291}