Skip to main content

rootcx_client/
lib.rs

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