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