nebulous/
config.rs

1use dirs;
2use dotenv::dotenv;
3use once_cell::sync::Lazy;
4use serde::{Deserialize, Serialize};
5use std::env;
6use std::fs;
7use std::path::PathBuf;
8
9#[derive(Serialize, Deserialize, Default, Debug)]
10pub struct GlobalConfig {
11    pub servers: Vec<ServerConfig>,
12    pub current_server: Option<String>,
13}
14
15#[derive(Serialize, Deserialize, Default, Clone, Debug)]
16pub struct ServerConfig {
17    /// Optional identifier for your server config.
18    pub name: Option<String>,
19    pub api_key: Option<String>,
20    pub server: Option<String>,
21    pub auth_server: Option<String>,
22}
23
24impl GlobalConfig {
25    /// Read the config from disk, or create a default one.
26    /// Then ensure that we either find or create a matching server in `self.servers`
27    /// based on environment variables, and set that as the `default_server`.
28    pub fn read() -> Result<Self, Box<dyn std::error::Error>> {
29        let config_path = get_config_file_path()?;
30        let path_exists = config_path.exists();
31
32        // Load or create default
33        let mut config = if path_exists {
34            let yaml = fs::read_to_string(&config_path)?;
35            serde_yaml::from_str::<GlobalConfig>(&yaml)?
36        } else {
37            GlobalConfig::default()
38        };
39
40        // Collect environment variables (NO fallback defaults here)
41        let env_api_key = env::var("NEBU_API_KEY")
42            .or_else(|_| env::var("NEBULOUS_API_KEY"))
43            .or_else(|_| env::var("AGENTSEA_API_KEY"))
44            .ok();
45        let env_server = env::var("NEBU_SERVER")
46            .or_else(|_| env::var("NEBULOUS_SERVER"))
47            .or_else(|_| env::var("AGENTSEA_SERVER"))
48            .ok();
49        let env_auth_server = env::var("NEBU_AUTH_SERVER")
50            .or_else(|_| env::var("NEBULOUS_AUTH_SERVER"))
51            .or_else(|_| env::var("AGENTSEA_AUTH_SERVER"))
52            .ok();
53
54        // Only proceed if all three environment variables are present.
55        if let (Some(env_api_key), Some(env_server), Some(env_auth_server)) =
56            (env_api_key, env_server, env_auth_server)
57        {
58            // Find a matching server (all three fields match).
59            let found_server = config.servers.iter_mut().find(|srv| {
60                srv.api_key.as_deref() == Some(&env_api_key)
61                    && srv.server.as_deref() == Some(&env_server)
62                    && srv.auth_server.as_deref() == Some(&env_auth_server)
63            });
64
65            // If found, use that. If not, create a new entry.
66            let server_name = "env-based-server".to_string();
67            let chosen_name = if let Some(srv) = found_server {
68                // Make sure it has a name, so we can set default_server to it
69                if srv.name.is_none() {
70                    srv.name = Some(server_name.clone());
71                }
72                srv.name.clone().unwrap()
73            } else {
74                // Need to create a new server entry
75                let new_server = ServerConfig {
76                    name: Some(server_name.clone()),
77                    api_key: Some(env_api_key),
78                    server: Some(env_server),
79                    auth_server: Some(env_auth_server),
80                };
81                config.servers.push(new_server);
82                server_name
83            };
84
85            // Set that server as the "current" or default
86            config.current_server = Some(chosen_name);
87        }
88
89        // Only write if the file didn't already exist
90        if !path_exists {
91            config.write()?;
92        }
93
94        Ok(config)
95    }
96
97    /// Write the current GlobalConfig to disk (YAML).
98    pub fn write(&self) -> Result<(), Box<dyn std::error::Error>> {
99        let config_path = get_config_file_path()?;
100
101        // Create parent directories if they don't exist
102        if let Some(parent) = config_path.parent() {
103            fs::create_dir_all(parent)?;
104        }
105
106        let yaml = serde_yaml::to_string(self)?;
107        fs::write(config_path, yaml)?;
108
109        Ok(())
110    }
111
112    /// Get the server config for the current `default_server`.
113    /// Returns `None` if `default_server` is unset or if no server
114    /// with that name is found.
115    pub fn get_current_server_config(&self) -> Option<&ServerConfig> {
116        self.current_server.as_deref().and_then(|name| {
117            self.servers
118                .iter()
119                .find(|srv| srv.name.as_deref() == Some(name))
120        })
121    }
122
123    pub fn get_auth_server(&self) -> Option<&str> {
124        self.get_current_server_config()
125            .and_then(|cfg| cfg.auth_server.as_deref())
126            .or_else(|| Some(CONFIG.auth_server.as_str()))
127    }
128}
129
130fn get_config_file_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
131    let home_dir = dirs::home_dir().ok_or("Could not determine home directory")?;
132    let config_dir = home_dir.join(".agentsea");
133    let config_path = config_dir.join("nebu.yaml");
134    Ok(config_path)
135}
136
137#[derive(Debug, Clone)]
138pub struct Config {
139    pub message_queue_type: String,
140    pub kafka_bootstrap_servers: String,
141    pub kafka_timeout_ms: String,
142    pub redis_host: String,
143    pub redis_port: String,
144    pub redis_password: Option<String>,
145    pub redis_url: Option<String>,
146    pub redis_publish_url: Option<String>,
147    pub database_url: String,
148    pub tailscale_api_key: Option<String>,
149    pub tailscale_tailnet: Option<String>,
150    pub bucket_name: String,
151    pub bucket_region: String,
152    pub root_owner: String,
153    pub auth_server: String,
154    pub publish_url: Option<String>,
155}
156
157impl Config {
158    pub fn new() -> Self {
159        dotenv().ok();
160
161        Self {
162            message_queue_type: env::var("MESSAGE_QUEUE_TYPE")
163                .unwrap_or_else(|_| "redis".to_string()),
164            kafka_bootstrap_servers: env::var("KAFKA_BOOTSTRAP_SERVERS")
165                .unwrap_or_else(|_| "localhost:9092".to_string()),
166            kafka_timeout_ms: env::var("KAFKA_TIMEOUT_MS").unwrap_or_else(|_| "5000".to_string()),
167            redis_host: env::var("REDIS_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()),
168            redis_port: env::var("REDIS_PORT").unwrap_or_else(|_| "6379".to_string()),
169            redis_password: env::var("REDIS_PASSWORD").ok(),
170            redis_url: env::var("REDIS_URL").ok(),
171            redis_publish_url: env::var("REDIS_PUBLISH_URL").ok(),
172            database_url: env::var("DATABASE_URL")
173                .unwrap_or_else(|_| "sqlite://.data/data.db".to_string()),
174            tailscale_api_key: env::var("TAILSCALE_API_KEY").ok(),
175            tailscale_tailnet: env::var("TAILSCALE_TAILNET").ok(),
176            bucket_name: env::var("NEBU_BUCKET_NAME")
177                .or_else(|_| env::var("NEBULOUS_BUCKET_NAME"))
178                .unwrap_or_else(|_| panic!("NEBU_BUCKET_NAME or NEBULOUS_BUCKET_NAME environment variable must be set")),
179            bucket_region: env::var("NEBU_BUCKET_REGION")
180                .or_else(|_| env::var("NEBULOUS_BUCKET_REGION"))
181                .unwrap_or_else(|_| panic!("NEBU_BUCKET_REGION or NEBULOUS_BUCKET_REGION environment variable must be set")),
182            root_owner: env::var("NEBU_ROOT_OWNER")
183                .or_else(|_| env::var("NEBULOUS_ROOT_OWNER"))
184                .unwrap_or_else(|_| panic!("NEBU_ROOT_OWNER or NEBULOUS_ROOT_OWNER environment variable must be set")),
185            auth_server: env::var("NEBU_AUTH_SERVER")
186                .or_else(|_| env::var("NEBULOUS_AUTH_SERVER"))
187                .or_else(|_| env::var("AGENTSEA_AUTH_SERVER"))
188                .or_else(|_| env::var("AGENTSEA_AUTH_URL"))
189                .unwrap_or_else(|_| "https://auth.hub.agentlabs.xyz".to_string()),
190            publish_url: env::var("NEBU_PUBLISH_URL")
191                .or_else(|_| env::var("NEBULOUS_PUBLISH_URL"))
192                .ok(),
193        }
194    }
195}
196// Global static CONFIG instance
197pub static CONFIG: Lazy<Config> = Lazy::new(Config::new);