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