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}