supermaven_api.rs

  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}