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