trace_share_core/
config.rs1use 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}