skilllite_core/config/
loader.rs1use std::env;
6
7const DEPRECATED_PAIRS: &[(&str, &str)] = &[
9 ("SKILLBOX_AUDIT_LOG", "SKILLLITE_AUDIT_LOG"),
10 ("SKILLBOX_QUIET", "SKILLLITE_QUIET"),
11 ("SKILLBOX_CACHE_DIR", "SKILLLITE_CACHE_DIR"),
12 ("AGENTSKILL_CACHE_DIR", "SKILLLITE_CACHE_DIR"),
13 ("SKILLBOX_LOG_LEVEL", "SKILLLITE_LOG_LEVEL"),
14 ("SKILLBOX_LOG_JSON", "SKILLLITE_LOG_JSON"),
15 ("SKILLBOX_SANDBOX_LEVEL", "SKILLLITE_SANDBOX_LEVEL"),
16 ("SKILLBOX_MAX_MEMORY_MB", "SKILLLITE_MAX_MEMORY_MB"),
17 ("SKILLBOX_TIMEOUT_SECS", "SKILLLITE_TIMEOUT_SECS"),
18 ("SKILLBOX_AUTO_APPROVE", "SKILLLITE_AUTO_APPROVE"),
19 ("SKILLBOX_NO_SANDBOX", "SKILLLITE_NO_SANDBOX"),
20 ("SKILLBOX_ALLOW_PLAYWRIGHT", "SKILLLITE_ALLOW_PLAYWRIGHT"),
21 ("SKILLBOX_SCRIPT_ARGS", "SKILLLITE_SCRIPT_ARGS"),
22];
23
24fn warn_deprecated_env_vars() {
26 use std::sync::Once;
27 static WARNED: Once = Once::new();
28 WARNED.call_once(|| {
29 let mut hints = Vec::new();
30 for (deprecated, recommended) in DEPRECATED_PAIRS {
31 if env::var(deprecated).is_ok() && env::var(recommended).is_err() {
32 hints.push(format!("{} → {}", deprecated, recommended));
33 }
34 }
35 if !hints.is_empty() {
36 tracing::warn!(
37 "[DEPRECATED] 以下环境变量已废弃,建议迁移:\n {}\n 详见 docs/zh/ENV_REFERENCE.md",
38 hints.join("\n ")
39 );
40 }
41 });
42}
43
44fn parse_dotenv_content(content: &str) -> Vec<(String, String)> {
47 let mut vars = Vec::new();
48 for line in content.lines() {
49 let line = line.trim();
50 if line.is_empty() || line.starts_with('#') {
51 continue;
52 }
53 if let Some(eq_pos) = line.find('=') {
54 let key = line[..eq_pos].trim().to_string();
55 let mut value = line[eq_pos + 1..].trim();
56 if let Some(hash_pos) = value.find('#') {
57 let before_hash = value[..hash_pos].trim_end();
58 if !before_hash.contains('"') && !before_hash.contains('\'') {
59 value = before_hash;
60 }
61 }
62 if (value.starts_with('"') && value.ends_with('"'))
63 || (value.starts_with('\'') && value.ends_with('\''))
64 {
65 value = &value[1..value.len() - 1];
66 }
67 if !key.is_empty() {
68 vars.push((key, value.to_string()));
69 }
70 }
71 }
72 vars
73}
74
75pub fn parse_dotenv_from_dir(dir: &std::path::Path) -> Vec<(String, String)> {
78 let path = dir.join(".env");
79 if let Ok(content) = std::fs::read_to_string(&path) {
80 parse_dotenv_content(&content)
81 } else {
82 vec![]
83 }
84}
85
86pub fn parse_dotenv_walking_up(
89 start: &std::path::Path,
90 max_levels: usize,
91) -> Vec<(String, String)> {
92 let mut dir = start.canonicalize().unwrap_or_else(|_| start.to_path_buf());
93 for _ in 0..max_levels {
94 let vars = parse_dotenv_from_dir(&dir);
95 if !vars.is_empty() {
96 return vars;
97 }
98 if !dir.pop() {
99 break;
100 }
101 }
102 vec![]
103}
104
105pub fn load_dotenv_from_dir(dir: &std::path::Path) {
108 for (key, value) in parse_dotenv_from_dir(dir) {
109 if env::var(&key).is_err() {
110 #[allow(unsafe_code)]
111 unsafe {
112 env::set_var(&key, &value);
113 }
114 }
115 }
116}
117
118pub fn load_dotenv() {
120 use std::sync::Once;
121 static INIT: Once = Once::new();
122 INIT.call_once(|| {
123 let path = env::current_dir()
124 .map(|d| d.join(".env"))
125 .unwrap_or_else(|_| std::path::PathBuf::from(".env"));
126 if let Ok(content) = std::fs::read_to_string(&path) {
127 for (key, value) in parse_dotenv_content(&content) {
128 if env::var(&key).is_err() {
129 #[allow(unsafe_code)]
130 unsafe {
131 env::set_var(&key, &value);
132 }
133 }
134 }
135 }
136 warn_deprecated_env_vars();
137 });
138}
139
140pub fn env_or<F>(primary: &str, aliases: &[&str], default: F) -> String
142where
143 F: FnOnce() -> String,
144{
145 env::var(primary)
146 .ok()
147 .or_else(|| aliases.iter().find_map(|a| env::var(a).ok()))
148 .filter(|s| !s.is_empty())
149 .unwrap_or_else(default)
150}
151
152pub fn env_optional(primary: &str, aliases: &[&str]) -> Option<String> {
154 env::var(primary)
155 .ok()
156 .or_else(|| aliases.iter().find_map(|a| env::var(a).ok()))
157 .and_then(|s| {
158 let s = s.trim().to_string();
159 if s.is_empty() {
160 None
161 } else {
162 Some(s)
163 }
164 })
165}
166
167pub fn env_bool(primary: &str, aliases: &[&str], default: bool) -> bool {
169 let v = env::var(primary)
170 .ok()
171 .or_else(|| aliases.iter().find_map(|a| env::var(a).ok()));
172 match v.as_deref() {
173 Some(s) => !matches!(
174 s.trim().to_lowercase().as_str(),
175 "0" | "false" | "no" | "off"
176 ),
177 None => default,
178 }
179}
180
181pub fn supply_chain_block_enabled() -> bool {
183 use crate::config::env_keys::observability;
184 env_bool(observability::SKILLLITE_SUPPLY_CHAIN_BLOCK, &[], false)
185}
186
187#[allow(dead_code)] pub fn env_is_set(primary: &str, aliases: &[&str]) -> bool {
190 env::var(primary).is_ok() || aliases.iter().any(|a| env::var(a).is_ok())
191}
192
193#[allow(unsafe_code)]
202pub fn set_env_var(key: &str, value: &str) {
203 unsafe { env::set_var(key, value) };
204}
205
206#[allow(unsafe_code)]
208pub fn remove_env_var(key: &str) {
209 unsafe { env::remove_var(key) };
210}
211
212pub fn init_llm_env(api_base: &str, api_key: &str, model: &str) {
216 set_env_var("OPENAI_API_BASE", api_base);
217 set_env_var("OPENAI_API_KEY", api_key);
218 set_env_var("SKILLLITE_MODEL", model);
219}
220
221pub fn init_daemon_env() {
223 set_env_var("SKILLLITE_AUTO_APPROVE", "1");
224 set_env_var("SKILLLITE_QUIET", "1");
225}
226
227pub fn ensure_default_output_dir() {
231 let paths = super::PathsConfig::from_env();
232 if paths.output_dir.is_none() {
233 let chat_output = crate::paths::chat_root().join("output");
234 let s = chat_output.to_string_lossy().to_string();
235 set_env_var("SKILLLITE_OUTPUT_DIR", &s);
236 if !chat_output.exists() {
237 let _ = std::fs::create_dir_all(&chat_output);
238 }
239 } else if let Some(ref output_dir) = paths.output_dir {
240 let p = std::path::PathBuf::from(output_dir);
241 if !p.exists() {
242 let _ = std::fs::create_dir_all(&p);
243 }
244 }
245}
246
247pub struct ScopedEnvGuard(pub &'static str);
251
252impl Drop for ScopedEnvGuard {
253 fn drop(&mut self) {
254 remove_env_var(self.0);
255 }
256}