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}