1use std::collections::HashMap;
2use std::sync::Arc;
3
4use rootcx_types::{AppManifest, InstalledApp, OsStatus, SchemaVerification};
5use serde_json::Value as JsonValue;
6
7#[derive(Debug, thiserror::Error)]
8pub enum ClientError {
9 #[error("HTTP request failed: {0}")]
10 Http(#[from] reqwest::Error),
11
12 #[error("API error ({status}): {message}")]
13 Api { status: u16, message: String },
14}
15
16#[derive(Clone)]
17pub struct RuntimeClient {
18 base_url: String,
19 client: reqwest::Client,
20 token: Arc<std::sync::RwLock<Option<String>>>,
21}
22
23impl RuntimeClient {
24 pub fn new(base_url: &str) -> Self {
25 Self {
26 base_url: base_url.trim_end_matches('/').to_string(),
27 client: reqwest::Client::new(),
28 token: Arc::new(std::sync::RwLock::new(None)),
29 }
30 }
31
32 fn api(&self, path: &str) -> String {
33 format!("{}/api/v1{path}", self.base_url)
34 }
35
36 fn authed(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
37 if let Some(ref t) = *self.token.read().unwrap() { req.bearer_auth(t) } else { req }
38 }
39
40 pub fn set_token(&self, token: Option<String>) {
41 *self.token.write().unwrap() = token;
42 }
43
44 pub fn token(&self) -> Option<String> {
45 self.token.read().unwrap().clone()
46 }
47
48 pub async fn is_available(&self) -> bool {
49 self.client.get(format!("{}/health", self.base_url)).send().await.is_ok()
50 }
51
52 pub async fn status(&self) -> Result<OsStatus, ClientError> {
53 let resp = self.authed(self.client.get(self.api("/status"))).send().await?;
54 check_response(resp).await?.json().await.map_err(Into::into)
55 }
56
57 pub async fn install_app(&self, manifest: &AppManifest) -> Result<String, ClientError> {
58 let resp = self.authed(self.client.post(self.api("/apps"))).json(manifest).send().await?;
59 extract_message(resp).await
60 }
61
62 pub async fn list_apps(&self) -> Result<Vec<InstalledApp>, ClientError> {
63 let resp = self.authed(self.client.get(self.api("/apps"))).send().await?;
64 check_response(resp).await?.json().await.map_err(Into::into)
65 }
66
67 pub async fn uninstall_app(&self, app_id: &str) -> Result<(), ClientError> {
68 let resp = self.authed(self.client.delete(self.api(&format!("/apps/{app_id}")))).send().await?;
69 check_response(resp).await?;
70 Ok(())
71 }
72
73 pub async fn list_records(&self, app_id: &str, entity: &str) -> Result<Vec<JsonValue>, ClientError> {
74 let resp = self.authed(self.client.get(self.api(&format!("/apps/{app_id}/collections/{entity}")))).send().await?;
75 check_response(resp).await?.json().await.map_err(Into::into)
76 }
77
78 pub async fn create_record(&self, app_id: &str, entity: &str, data: &JsonValue) -> Result<JsonValue, ClientError> {
79 let resp = self
80 .authed(self.client.post(self.api(&format!("/apps/{app_id}/collections/{entity}"))))
81 .json(data)
82 .send()
83 .await?;
84 check_response(resp).await?.json().await.map_err(Into::into)
85 }
86
87 pub async fn bulk_create_records(&self, app_id: &str, entity: &str, data: &[JsonValue]) -> Result<Vec<JsonValue>, ClientError> {
88 let resp = self
89 .authed(self.client.post(self.api(&format!("/apps/{app_id}/collections/{entity}/bulk"))))
90 .json(&data)
91 .send()
92 .await?;
93 check_response(resp).await?.json().await.map_err(Into::into)
94 }
95
96 pub async fn get_record(&self, app_id: &str, entity: &str, id: &str) -> Result<JsonValue, ClientError> {
97 let resp = self
98 .authed(self.client.get(self.api(&format!("/apps/{app_id}/collections/{entity}/{id}"))))
99 .send()
100 .await?;
101 check_response(resp).await?.json().await.map_err(Into::into)
102 }
103
104 pub async fn update_record(
105 &self,
106 app_id: &str,
107 entity: &str,
108 id: &str,
109 data: &JsonValue,
110 ) -> Result<JsonValue, ClientError> {
111 let resp = self
112 .authed(self.client.patch(self.api(&format!("/apps/{app_id}/collections/{entity}/{id}"))))
113 .json(data)
114 .send()
115 .await?;
116 check_response(resp).await?.json().await.map_err(Into::into)
117 }
118
119 pub async fn delete_record(&self, app_id: &str, entity: &str, id: &str) -> Result<(), ClientError> {
120 let resp = self
121 .authed(self.client.delete(self.api(&format!("/apps/{app_id}/collections/{entity}/{id}"))))
122 .send()
123 .await?;
124 check_response(resp).await?;
125 Ok(())
126 }
127
128 pub async fn verify_schema(&self, manifest: &AppManifest) -> Result<SchemaVerification, ClientError> {
129 let resp = self.authed(self.client.post(self.api("/apps/schema/verify"))).json(manifest).send().await?;
130 check_response(resp).await?.json().await.map_err(Into::into)
131 }
132
133 pub async fn deploy_app(&self, app_id: &str, archive: Vec<u8>) -> Result<String, ClientError> {
134 self.upload_archive(&format!("/apps/{app_id}/deploy"), archive).await
135 }
136
137 pub async fn deploy_frontend(&self, app_id: &str, archive: Vec<u8>) -> Result<String, ClientError> {
138 self.upload_archive(&format!("/apps/{app_id}/frontend"), archive).await
139 }
140
141 async fn upload_archive(&self, path: &str, archive: Vec<u8>) -> Result<String, ClientError> {
142 let part = reqwest::multipart::Part::bytes(archive)
143 .mime_str("application/gzip")
144 .map_err(ClientError::Http)?;
145 let form = reqwest::multipart::Form::new().part("archive", part);
146 let resp = self.authed(self.client.post(self.api(path))).multipart(form).send().await?;
147 extract_message(resp).await
148 }
149
150 pub async fn start_worker(&self, app_id: &str) -> Result<String, ClientError> {
151 self.worker_action(app_id, "start").await
152 }
153
154 pub async fn stop_worker(&self, app_id: &str) -> Result<String, ClientError> {
155 self.worker_action(app_id, "stop").await
156 }
157
158 pub async fn worker_status(&self, app_id: &str) -> Result<String, ClientError> {
159 let resp = self.authed(self.client.get(self.api(&format!("/apps/{app_id}/worker/status")))).send().await?;
160 let body: JsonValue = check_response(resp).await?.json().await?;
161 Ok(body["status"].as_str().unwrap_or("unknown").to_string())
162 }
163
164 pub async fn list_integrations(&self) -> Result<Vec<JsonValue>, ClientError> {
165 let resp = self.authed(self.client.get(self.api("/integrations"))).send().await?;
166 check_response(resp).await?.json().await.map_err(Into::into)
167 }
168
169 pub async fn get_forge_config(&self) -> Result<JsonValue, ClientError> {
170 let resp = self.authed(self.client.get(self.api("/config/ai/forge"))).send().await?;
171 check_response(resp).await?.json().await.map_err(Into::into)
172 }
173
174 pub async fn get_platform_env(&self) -> Result<HashMap<String, String>, ClientError> {
175 let resp = self.authed(self.client.get(self.api("/platform/secrets/env"))).send().await?;
176 let body: HashMap<String, String> = check_response(resp).await?.json().await?;
177 Ok(body)
178 }
179
180 pub async fn list_platform_secrets(&self) -> Result<Vec<String>, ClientError> {
181 let resp = self.authed(self.client.get(self.api("/platform/secrets"))).send().await?;
182 check_response(resp).await?.json().await.map_err(Into::into)
183 }
184
185 pub async fn set_platform_secret(&self, key: &str, value: &str) -> Result<(), ClientError> {
186 let body = serde_json::json!({ "key": key, "value": value });
187 let resp = self.authed(self.client.post(self.api("/platform/secrets"))).json(&body).send().await?;
188 check_response(resp).await?;
189 Ok(())
190 }
191
192 pub async fn delete_platform_secret(&self, key: &str) -> Result<(), ClientError> {
193 let resp = self.authed(self.client.delete(self.api(&format!("/platform/secrets/{key}")))).send().await?;
194 check_response(resp).await?;
195 Ok(())
196 }
197
198 async fn worker_action(&self, app_id: &str, action: &str) -> Result<String, ClientError> {
199 let resp = self
200 .authed(self.client.post(self.api(&format!("/apps/{app_id}/worker/{action}"))))
201 .send()
202 .await?;
203 extract_message(resp).await
204 }
205}
206
207async fn extract_message(resp: reqwest::Response) -> Result<String, ClientError> {
208 let body: JsonValue = check_response(resp).await?.json().await?;
209 Ok(body["message"].as_str().unwrap_or("ok").to_string())
210}
211
212async fn check_response(resp: reqwest::Response) -> Result<reqwest::Response, ClientError> {
213 if resp.status().is_success() {
214 return Ok(resp);
215 }
216 let status = resp.status().as_u16();
217 let body: JsonValue = resp.json().await.unwrap_or_default();
218 let message = body["error"].as_str().unwrap_or("unknown error").to_string();
219 Err(ClientError::Api { status, message })
220}