supermaven_api.rs

  1use anyhow::{Context as _, Result};
  2use futures::AsyncReadExt;
  3use futures::io::BufReader;
  4use http_client::{AsyncBody, HttpClient, Request as HttpRequest};
  5use paths::supermaven_dir;
  6use serde::Deserialize;
  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(Deserialize)]
 14pub struct SupermavenApiError {
 15    pub message: String,
 16}
 17
 18#[derive(Debug, Deserialize)]
 19#[serde(rename_all = "camelCase")]
 20pub struct SupermavenDownloadResponse {
 21    pub download_url: String,
 22    pub version: u64,
 23    pub sha256_hash: String,
 24}
 25
 26pub async fn latest_release(
 27    client: Arc<dyn HttpClient>,
 28    platform: &str,
 29    arch: &str,
 30) -> Result<SupermavenDownloadResponse> {
 31    let uri = format!(
 32        "https://supermaven.com/api/download-path?platform={}&arch={}",
 33        platform, arch
 34    );
 35
 36    // Download is not authenticated
 37    let request = HttpRequest::get(&uri);
 38
 39    let mut response = client
 40        .send(request.body(AsyncBody::default())?)
 41        .await
 42        .with_context(|| "Unable to acquire Supermaven Agent".to_string())?;
 43
 44    let mut body = Vec::new();
 45    response.body_mut().read_to_end(&mut body).await?;
 46
 47    if response.status().is_client_error() || response.status().is_server_error() {
 48        let body_str = std::str::from_utf8(&body)?;
 49        let error: SupermavenApiError = serde_json::from_str(body_str)?;
 50        anyhow::bail!("Supermaven API error: {}", error.message);
 51    }
 52
 53    serde_json::from_slice::<SupermavenDownloadResponse>(&body)
 54        .with_context(|| "Unable to parse Supermaven Agent response".to_string())
 55}
 56
 57pub fn version_path(version: u64) -> PathBuf {
 58    supermaven_dir().join(format!(
 59        "sm-agent-{}{}",
 60        version,
 61        std::env::consts::EXE_SUFFIX
 62    ))
 63}
 64
 65pub async fn has_version(version_path: &Path) -> bool {
 66    fs::metadata(version_path).await.is_ok_and(|m| m.is_file())
 67}
 68
 69pub async fn get_supermaven_agent_path(client: Arc<dyn HttpClient>) -> Result<PathBuf> {
 70    fs::create_dir_all(supermaven_dir())
 71        .await
 72        .with_context(|| {
 73            format!(
 74                "Could not create Supermaven Agent Directory at {:?}",
 75                supermaven_dir()
 76            )
 77        })?;
 78
 79    let platform = match std::env::consts::OS {
 80        "macos" => "darwin",
 81        "windows" => "windows",
 82        "linux" => "linux",
 83        unsupported => anyhow::bail!("unsupported platform {unsupported}"),
 84    };
 85
 86    let arch = match std::env::consts::ARCH {
 87        "x86_64" => "amd64",
 88        "aarch64" => "arm64",
 89        unsupported => anyhow::bail!("unsupported architecture {unsupported}"),
 90    };
 91
 92    let download_info = latest_release(client.clone(), platform, arch).await?;
 93
 94    let binary_path = version_path(download_info.version);
 95
 96    if has_version(&binary_path).await {
 97        // Due to an issue with the Supermaven binary not being made executable on
 98        // earlier Zed versions and Supermaven releases not occurring that frequently,
 99        // we ensure here that the found binary is actually executable.
100        make_file_executable(&binary_path).await?;
101
102        return Ok(binary_path);
103    }
104
105    let request = HttpRequest::get(&download_info.download_url);
106
107    let mut response = client
108        .send(request.body(AsyncBody::default())?)
109        .await
110        .with_context(|| "Unable to download Supermaven Agent".to_string())?;
111
112    let mut file = File::create(&binary_path)
113        .await
114        .with_context(|| format!("Unable to create file at {:?}", binary_path))?;
115
116    futures::io::copy(BufReader::new(response.body_mut()), &mut file)
117        .await
118        .with_context(|| format!("Unable to write binary to file at {:?}", binary_path))?;
119
120    make_file_executable(&binary_path).await?;
121
122    remove_matching(supermaven_dir(), |file| file != binary_path).await;
123
124    Ok(binary_path)
125}