1use anyhow::{Context, Result};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fs;
9use std::path::{Path, PathBuf};
10use std::sync::OnceLock;
11
12static COMMON_CUSTOM_CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
14
15pub fn set_config_dir(path: PathBuf) {
17 COMMON_CUSTOM_CONFIG_DIR.set(path).ok();
18}
19
20#[derive(Debug, Serialize, Deserialize, Clone)]
37pub struct SlashCommandConfig {
38 pub name: String,
40 pub description: String,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub input_hint: Option<String>,
46}
47
48#[derive(Debug, Serialize, Deserialize, Clone, Default)]
53pub struct PushRelayConfig {
54 #[serde(default, skip_serializing_if = "String::is_empty")]
56 pub url: String,
57 #[serde(default, skip_serializing_if = "String::is_empty")]
59 pub token_url: String,
60 #[serde(default, skip_serializing_if = "String::is_empty")]
62 pub client_id: String,
63 #[serde(default, skip_serializing_if = "String::is_empty")]
65 pub client_secret: String,
66}
67
68#[derive(Debug, Serialize, Deserialize, Clone)]
72pub struct CommonConfig {
73 #[serde(default, skip_serializing_if = "String::is_empty")]
75 pub agent_id: String,
76
77 #[serde(default, skip_serializing_if = "String::is_empty")]
79 pub auth_token: String,
80
81 #[serde(default)]
84 pub transports: HashMap<String, TransportConfig>,
85
86 #[serde(default, skip_serializing_if = "Vec::is_empty")]
90 pub slash_commands: Vec<SlashCommandConfig>,
91
92 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub push_relay: Option<PushRelayConfig>,
96}
97
98#[derive(Debug, Serialize, Deserialize, Clone, Default)]
100pub struct TransportConfig {
101 #[serde(default)]
103 pub enabled: bool,
104
105 pub port: Option<u16>,
107
108 pub tls: Option<bool>,
110
111 pub hostname: Option<String>,
113 pub tunnel_id: Option<String>,
114 pub tunnel_secret: Option<String>,
115 pub account_id: Option<String>,
116 pub client_id: Option<String>,
117 pub client_secret: Option<String>,
118 pub domain: Option<String>,
119 pub subdomain: Option<String>,
120}
121
122impl Default for CommonConfig {
123 fn default() -> Self {
124 let mut transports = HashMap::new();
125 transports.insert(
126 "local".to_string(),
127 TransportConfig {
128 enabled: true,
129 port: Some(8765),
130 tls: Some(true),
131 ..Default::default()
132 },
133 );
134 Self {
135 agent_id: String::new(),
136 auth_token: String::new(),
137 transports,
138 slash_commands: Vec::new(),
139 push_relay: None,
140 }
141 }
142}
143
144impl CommonConfig {
145 pub fn config_path() -> PathBuf {
147 Self::config_dir().join("common.toml")
148 }
149
150 pub fn config_dir() -> PathBuf {
152 let dir = if let Some(custom) = COMMON_CUSTOM_CONFIG_DIR.get() {
153 custom.clone()
154 } else {
155 std::env::current_dir()
156 .unwrap_or_else(|_| PathBuf::from("."))
157 .join(".aptove-bridge")
158 };
159 fs::create_dir_all(&dir).ok();
160 dir
161 }
162
163 pub fn load() -> Result<Self> {
165 Self::load_from_dir(&Self::config_dir())
166 }
167
168 pub fn load_from_dir(dir: &Path) -> Result<Self> {
170 let path = dir.join("common.toml");
171 if !path.exists() {
172 return Ok(Self::default());
173 }
174 let text = fs::read_to_string(&path)
175 .with_context(|| format!("Failed to read {:?}", path))?;
176 let config: Self = toml::from_str(&text)
177 .with_context(|| format!("Failed to parse {:?}", path))?;
178 Ok(config)
179 }
180
181 pub fn save(&self) -> Result<()> {
183 self.save_to_dir(&Self::config_dir())
184 }
185
186 pub fn save_to_dir(&self, dir: &Path) -> Result<()> {
188 fs::create_dir_all(dir)?;
189 let path = dir.join("common.toml");
190 let text = toml::to_string_pretty(self).context("Failed to serialize CommonConfig")?;
191 fs::write(&path, &text).with_context(|| format!("Failed to write {:?}", path))?;
192 #[cfg(unix)]
193 {
194 use std::os::unix::fs::PermissionsExt;
195 let mut perms = fs::metadata(&path)?.permissions();
196 perms.set_mode(0o600);
197 fs::set_permissions(&path, perms)?;
198 }
199 Ok(())
200 }
201
202 pub fn ensure_agent_id(&mut self) {
204 if self.agent_id.is_empty() {
205 self.agent_id = uuid::Uuid::new_v4().to_string();
206 }
207 }
208
209 pub fn generate_auth_token() -> String {
211 use base64::{engine::general_purpose, Engine as _};
212 let bytes: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
213 general_purpose::URL_SAFE_NO_PAD.encode(bytes)
214 }
215
216 pub fn ensure_auth_token(&mut self) {
218 if self.auth_token.is_empty() {
219 self.auth_token = Self::generate_auth_token();
220 }
221 }
222
223 pub fn enabled_transports(&self) -> Vec<(&str, &TransportConfig)> {
225 let mut result: Vec<_> = self
226 .transports
227 .iter()
228 .filter(|(_, t)| t.enabled)
229 .map(|(k, v)| (k.as_str(), v))
230 .collect();
231 result.sort_by_key(|(k, _)| *k);
232 result
233 }
234
235 pub fn to_connection_json(&self, hostname: &str, transport_name: &str, cwd: &str) -> Result<String> {
240 use serde_json::{Map, Value};
241 let transport = self.transports.get(transport_name);
242 let mut map = Map::new();
243 if !self.agent_id.is_empty() {
244 map.insert("agentId".to_string(), Value::String(self.agent_id.clone()));
245 }
246 map.insert("url".to_string(), Value::String(hostname.to_string()));
247 map.insert("protocol".to_string(), Value::String("acp".to_string()));
248 map.insert("version".to_string(), Value::String("1.0".to_string()));
249 map.insert("cwd".to_string(), Value::String(cwd.to_string()));
250 if !self.auth_token.is_empty() {
251 map.insert(
252 "authToken".to_string(),
253 Value::String(self.auth_token.clone()),
254 );
255 }
256 if let Some(t) = transport {
257 if let Some(ref id) = t.client_id {
258 if !id.is_empty() {
259 map.insert("clientId".to_string(), Value::String(id.clone()));
260 }
261 }
262 if let Some(ref secret) = t.client_secret {
263 if !secret.is_empty() {
264 map.insert("clientSecret".to_string(), Value::String(secret.clone()));
265 }
266 }
267 }
268 serde_json::to_string(&Value::Object(map)).context("Failed to serialize connection info")
269 }
270}