use std::{env, fs, net::SocketAddr, path::PathBuf, time::Duration}; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; use crate::{Error, Result}; const DATA_DIR_NAME: &str = ".cursor-byok-v3"; const DATABASE_FILE_NAME: &str = "cursor-byok.db"; const V0049_DATA_DIR_NAME: &str = ".cursor-local-assistant-v2"; const V0049_CONFIG_FILE_NAME: &str = "config.yaml"; const DEFAULT_PROVIDER_REQUEST_TIMEOUT: Duration = Duration::from_secs(3000); pub fn managed_data_dir() -> Result { let home_dir = dirs::home_dir() .ok_or_else(|| Error::Config("cannot resolve user home directory".into()))?; let data_dir = home_dir.join(DATA_DIR_NAME); fs::create_dir_all(&data_dir)?; #[cfg(unix)] fs::set_permissions(&data_dir, fs::Permissions::from_mode(0o700))?; Ok(data_dir) } pub fn v0049_config_path() -> Result { let home_dir = dirs::home_dir() .ok_or_else(|| Error::Config("cannot resolve user home directory".into()))?; Ok(home_dir .join(V0049_DATA_DIR_NAME) .join(V0049_CONFIG_FILE_NAME)) } #[derive(Clone, Debug, PartialEq, Eq)] pub enum ProviderKind { OpenAiChat, OpenAiResponses, Anthropic, } #[derive(Clone)] pub struct ProviderConfig { pub kind: ProviderKind, pub request_url: String, pub api_key: String, pub custom_headers: reqwest::header::HeaderMap, pub max_output_tokens: Option, pub request_timeout: Duration, } #[derive(Clone)] pub struct Config { pub listen_addr: SocketAddr, pub database_url: String, pub provider_request_timeout: Duration, pub console: Option, pub use_persisted_ports: bool, } #[derive(Clone)] pub enum ConsoleSource { Directory(PathBuf), Proxy(url::Url), } impl Config { pub fn from_env() -> Result { let listen_addr = env::var("CURSOR_LISTEN_ADDR") .unwrap_or_else(|_| "127.0.0.1:3000".into()) .parse() .map_err(|error| Error::Config(format!("invalid CURSOR_LISTEN_ADDR: {error}")))?; let request_timeout = match env::var("CURSOR_PROVIDER_TIMEOUT_SECONDS") { Ok(value) => Duration::from_secs(value.parse().map_err(|error| { Error::Config(format!("invalid CURSOR_PROVIDER_TIMEOUT_SECONDS: {error}")) })?), Err(env::VarError::NotPresent) => DEFAULT_PROVIDER_REQUEST_TIMEOUT, Err(error) => { return Err(Error::Config(format!( "invalid CURSOR_PROVIDER_TIMEOUT_SECONDS: {error}" ))) } }; let console_dir = env::var_os("CURSOR_CONSOLE_DIR").map(PathBuf::from); let console_proxy = env::var("CURSOR_CONSOLE_PROXY") .ok() .map(|value| { value.parse().map_err(|error| { Error::Config(format!("invalid CURSOR_CONSOLE_PROXY: {error}")) }) }) .transpose()?; let console = match (console_dir, console_proxy) { (Some(_), Some(_)) => { return Err(Error::Config( "CURSOR_CONSOLE_DIR and CURSOR_CONSOLE_PROXY cannot both be set".into(), )) } (Some(directory), None) => Some(ConsoleSource::Directory(directory)), (None, Some(proxy)) => Some(ConsoleSource::Proxy(proxy)), (None, None) => None, }; Ok(Self { listen_addr, database_url: database_url_from_env()?, provider_request_timeout: request_timeout, console, use_persisted_ports: false, }) } pub fn desktop() -> Result { Ok(Self { listen_addr: "127.0.0.1:0" .parse() .expect("desktop listen address is static"), database_url: default_database_url()?, provider_request_timeout: DEFAULT_PROVIDER_REQUEST_TIMEOUT, console: None, use_persisted_ports: true, }) } } fn database_url_from_env() -> Result { match env::var("CURSOR_DATABASE_URL") { Ok(database_url) => Ok(database_url), Err(env::VarError::NotPresent) => default_database_url(), Err(error) => Err(Error::Config(format!( "invalid CURSOR_DATABASE_URL: {error}" ))), } } fn default_database_url() -> Result { let data_dir = managed_data_dir()?; database_url_for_dir(&data_dir) } #[cfg(test)] fn database_url_in(home_dir: &std::path::Path) -> Result { let data_dir = home_dir.join(DATA_DIR_NAME); fs::create_dir_all(&data_dir)?; #[cfg(unix)] fs::set_permissions(&data_dir, fs::Permissions::from_mode(0o700))?; database_url_for_dir(&data_dir) } fn database_url_for_dir(data_dir: &std::path::Path) -> Result { let database_path = data_dir.join(DATABASE_FILE_NAME); let database_path = database_path .to_str() .ok_or_else(|| Error::Config("database path is not valid UTF-8".into()))?; Ok(format!("sqlite://{database_path}")) } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn managed_database_supports_home_paths_with_spaces() { let directory = tempfile::tempdir().unwrap(); let home_dir = directory.path().join("home with spaces"); let database_url = database_url_in(&home_dir).unwrap(); let store = crate::store::Store::connect(&database_url).await.unwrap(); drop(store); let data_dir = home_dir.join(DATA_DIR_NAME); assert!(data_dir.join(DATABASE_FILE_NAME).is_file()); #[cfg(unix)] assert_eq!( fs::metadata(data_dir).unwrap().permissions().mode() & 0o777, 0o700 ); } }