Skip to main content

trace_share_core/
config.rs

1use anyhow::{Context, Result};
2use directories::ProjectDirs;
3use serde::{Deserialize, Serialize};
4use std::{env, fs, path::PathBuf};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct UpstashConfig {
8    pub rest_url: Option<String>,
9    pub rest_token: Option<String>,
10    pub namespace: Option<String>,
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct WorkerConfig {
15    pub base_url: Option<String>,
16    pub api_token: Option<String>,
17    pub timeout_seconds: u64,
18    pub upload_mode: String,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct PolicyConfig {
23    pub path: Option<PathBuf>,
24    pub allowlist_mode: bool,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct RemoteRegistryConfig {
29    pub enabled: bool,
30    pub url: Option<String>,
31    pub cache_ttl_hours: u64,
32    pub require_consent: bool,
33    pub last_accepted_digest: Option<String>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct AppConfig {
38    pub upstash: UpstashConfig,
39    pub worker: WorkerConfig,
40    pub policy: PolicyConfig,
41    pub sources_path: Option<PathBuf>,
42    pub remote_registry: RemoteRegistryConfig,
43}
44
45impl Default for AppConfig {
46    fn default() -> Self {
47        Self {
48            upstash: UpstashConfig {
49                rest_url: None,
50                rest_token: None,
51                namespace: None,
52            },
53            worker: WorkerConfig {
54                base_url: None,
55                api_token: None,
56                timeout_seconds: 30,
57                upload_mode: "legacy".to_string(),
58            },
59            policy: PolicyConfig {
60                path: None,
61                allowlist_mode: true,
62            },
63            sources_path: None,
64            remote_registry: RemoteRegistryConfig {
65                enabled: false,
66                url: Some("https://raw.githubusercontent.com/frumu-ai/trace-share-registry/main/registry/sources.toml".to_string()),
67                cache_ttl_hours: 24,
68                require_consent: true,
69                last_accepted_digest: None,
70            },
71        }
72    }
73}
74
75pub fn config_dir() -> Result<PathBuf> {
76    app_home_dir()
77}
78
79pub fn data_dir() -> Result<PathBuf> {
80    app_home_dir()
81}
82
83pub fn default_config_path() -> Result<PathBuf> {
84    Ok(config_dir()?.join("config.toml"))
85}
86
87pub fn default_sources_path() -> Result<PathBuf> {
88    Ok(config_dir()?.join("sources.toml"))
89}
90
91pub fn app_home_dir() -> Result<PathBuf> {
92    if let Ok(custom) = env::var("TRACE_SHARE_HOME") {
93        return Ok(PathBuf::from(custom));
94    }
95
96    if cfg!(windows) {
97        let dirs = ProjectDirs::from("ai", "trace-share", "trace-share")
98            .context("failed to resolve Windows app directory")?;
99        return Ok(dirs.data_local_dir().to_path_buf());
100    }
101
102    let home = env::var("HOME").context("HOME environment variable is not set")?;
103    Ok(PathBuf::from(home).join(".trace-share"))
104}
105
106pub fn load_config() -> Result<AppConfig> {
107    let config_path = env::var("TRACE_SHARE_CONFIG")
108        .ok()
109        .map(PathBuf::from)
110        .unwrap_or(default_config_path()?);
111
112    let mut config = if config_path.exists() {
113        let text = fs::read_to_string(&config_path)
114            .with_context(|| format!("failed reading config file {}", config_path.display()))?;
115        toml::from_str::<AppConfig>(&text).context("invalid config.toml format")?
116    } else {
117        AppConfig::default()
118    };
119
120    if let Ok(v) = env::var("UPSTASH_VECTOR_REST_URL") {
121        config.upstash.rest_url = Some(v);
122    }
123    if let Ok(v) = env::var("UPSTASH_VECTOR_REST_TOKEN") {
124        config.upstash.rest_token = Some(v);
125    }
126    if let Ok(v) = env::var("TRACE_SHARE_WORKER_BASE_URL") {
127        config.worker.base_url = Some(v);
128    }
129    if let Ok(v) = env::var("TRACE_SHARE_WORKER_API_TOKEN") {
130        config.worker.api_token = Some(v);
131    }
132    if let Ok(v) = env::var("TRACE_SHARE_WORKER_UPLOAD_MODE") {
133        config.worker.upload_mode = v;
134    }
135    if let Ok(v) = env::var("TRACE_SHARE_NAMESPACE") {
136        config.upstash.namespace = Some(v);
137    }
138    if let Ok(v) = env::var("TRACE_SHARE_POLICY_PATH") {
139        config.policy.path = Some(PathBuf::from(v));
140    }
141    if let Ok(v) = env::var("TRACE_SHARE_SOURCES_PATH") {
142        config.sources_path = Some(PathBuf::from(v));
143    }
144    if let Ok(v) = env::var("TRACE_SHARE_ALLOWLIST_MODE") {
145        config.policy.allowlist_mode = matches!(v.as_str(), "1" | "true" | "TRUE" | "yes");
146    }
147    if let Ok(v) = env::var("TRACE_SHARE_REMOTE_REGISTRY_ENABLED") {
148        config.remote_registry.enabled = matches!(v.as_str(), "1" | "true" | "TRUE" | "yes");
149    }
150    if let Ok(v) = env::var("TRACE_SHARE_REMOTE_REGISTRY_URL") {
151        config.remote_registry.url = Some(v);
152    }
153
154    Ok(config)
155}
156
157pub fn ensure_dirs() -> Result<()> {
158    let config_dir = config_dir()?;
159    if config_dir.exists() && !config_dir.is_dir() {
160        anyhow::bail!(
161            "trace-share home path exists but is not a directory: {}",
162            config_dir.display()
163        );
164    }
165    fs::create_dir_all(&config_dir)?;
166
167    let data_dir = data_dir()?;
168    if data_dir.exists() && !data_dir.is_dir() {
169        anyhow::bail!(
170            "trace-share data path exists but is not a directory: {}",
171            data_dir.display()
172        );
173    }
174    fs::create_dir_all(&data_dir)?;
175    Ok(())
176}