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;
 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}