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