use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
static COMMON_CUSTOM_CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
pub fn set_config_dir(path: PathBuf) {
COMMON_CUSTOM_CONFIG_DIR.set(path).ok();
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SlashCommandConfig {
pub name: String,
pub description: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_hint: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CommonConfig {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub agent_id: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub auth_token: String,
#[serde(default)]
pub transports: HashMap<String, TransportConfig>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub slash_commands: Vec<SlashCommandConfig>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct TransportConfig {
#[serde(default)]
pub enabled: bool,
pub port: Option<u16>,
pub tls: Option<bool>,
pub hostname: Option<String>,
pub tunnel_id: Option<String>,
pub tunnel_secret: Option<String>,
pub account_id: Option<String>,
pub client_id: Option<String>,
pub client_secret: Option<String>,
pub domain: Option<String>,
pub subdomain: Option<String>,
}
impl Default for CommonConfig {
fn default() -> Self {
let mut transports = HashMap::new();
transports.insert(
"local".to_string(),
TransportConfig {
enabled: true,
port: Some(8765),
tls: Some(true),
..Default::default()
},
);
Self {
agent_id: String::new(),
auth_token: String::new(),
transports,
slash_commands: Vec::new(),
}
}
}
impl CommonConfig {
pub fn config_path() -> PathBuf {
Self::config_dir().join("common.toml")
}
pub fn config_dir() -> PathBuf {
let dir = if let Some(custom) = COMMON_CUSTOM_CONFIG_DIR.get() {
custom.clone()
} else {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(".aptove-bridge")
};
fs::create_dir_all(&dir).ok();
dir
}
pub fn load() -> Result<Self> {
Self::load_from_dir(&Self::config_dir())
}
pub fn load_from_dir(dir: &Path) -> Result<Self> {
let path = dir.join("common.toml");
if !path.exists() {
return Ok(Self::default());
}
let text = fs::read_to_string(&path)
.with_context(|| format!("Failed to read {:?}", path))?;
let config: Self = toml::from_str(&text)
.with_context(|| format!("Failed to parse {:?}", path))?;
Ok(config)
}
pub fn save(&self) -> Result<()> {
self.save_to_dir(&Self::config_dir())
}
pub fn save_to_dir(&self, dir: &Path) -> Result<()> {
fs::create_dir_all(dir)?;
let path = dir.join("common.toml");
let text = toml::to_string_pretty(self).context("Failed to serialize CommonConfig")?;
fs::write(&path, &text).with_context(|| format!("Failed to write {:?}", path))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&path)?.permissions();
perms.set_mode(0o600);
fs::set_permissions(&path, perms)?;
}
Ok(())
}
pub fn ensure_agent_id(&mut self) {
if self.agent_id.is_empty() {
self.agent_id = uuid::Uuid::new_v4().to_string();
}
}
pub fn generate_auth_token() -> String {
use base64::{engine::general_purpose, Engine as _};
let bytes: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
general_purpose::URL_SAFE_NO_PAD.encode(bytes)
}
pub fn ensure_auth_token(&mut self) {
if self.auth_token.is_empty() {
self.auth_token = Self::generate_auth_token();
}
}
pub fn enabled_transports(&self) -> Vec<(&str, &TransportConfig)> {
let mut result: Vec<_> = self
.transports
.iter()
.filter(|(_, t)| t.enabled)
.map(|(k, v)| (k.as_str(), v))
.collect();
result.sort_by_key(|(k, _)| *k);
result
}
pub fn to_connection_json(&self, hostname: &str, transport_name: &str, cwd: &str) -> Result<String> {
use serde_json::{Map, Value};
let transport = self.transports.get(transport_name);
let mut map = Map::new();
if !self.agent_id.is_empty() {
map.insert("agentId".to_string(), Value::String(self.agent_id.clone()));
}
map.insert("url".to_string(), Value::String(hostname.to_string()));
map.insert("protocol".to_string(), Value::String("acp".to_string()));
map.insert("version".to_string(), Value::String("1.0".to_string()));
map.insert("cwd".to_string(), Value::String(cwd.to_string()));
if !self.auth_token.is_empty() {
map.insert(
"authToken".to_string(),
Value::String(self.auth_token.clone()),
);
}
if let Some(t) = transport {
if let Some(ref id) = t.client_id {
if !id.is_empty() {
map.insert("clientId".to_string(), Value::String(id.clone()));
}
}
if let Some(ref secret) = t.client_secret {
if !secret.is_empty() {
map.insert("clientSecret".to_string(), Value::String(secret.clone()));
}
}
}
serde_json::to_string(&Value::Object(map)).context("Failed to serialize connection info")
}
}