use std::env;
const DEPRECATED_PAIRS: &[(&str, &str)] = &[
("SKILLBOX_AUDIT_LOG", "SKILLLITE_AUDIT_LOG"),
("SKILLBOX_QUIET", "SKILLLITE_QUIET"),
("SKILLBOX_CACHE_DIR", "SKILLLITE_CACHE_DIR"),
("AGENTSKILL_CACHE_DIR", "SKILLLITE_CACHE_DIR"),
("SKILLBOX_LOG_LEVEL", "SKILLLITE_LOG_LEVEL"),
("SKILLBOX_LOG_JSON", "SKILLLITE_LOG_JSON"),
("SKILLBOX_SANDBOX_LEVEL", "SKILLLITE_SANDBOX_LEVEL"),
("SKILLBOX_MAX_MEMORY_MB", "SKILLLITE_MAX_MEMORY_MB"),
("SKILLBOX_TIMEOUT_SECS", "SKILLLITE_TIMEOUT_SECS"),
("SKILLBOX_AUTO_APPROVE", "SKILLLITE_AUTO_APPROVE"),
("SKILLBOX_NO_SANDBOX", "SKILLLITE_NO_SANDBOX"),
("SKILLBOX_ALLOW_PLAYWRIGHT", "SKILLLITE_ALLOW_PLAYWRIGHT"),
("SKILLBOX_SCRIPT_ARGS", "SKILLLITE_SCRIPT_ARGS"),
];
fn warn_deprecated_env_vars() {
use std::sync::Once;
static WARNED: Once = Once::new();
WARNED.call_once(|| {
let mut hints = Vec::new();
for (deprecated, recommended) in DEPRECATED_PAIRS {
if env::var(deprecated).is_ok() && env::var(recommended).is_err() {
hints.push(format!("{} → {}", deprecated, recommended));
}
}
if !hints.is_empty() {
tracing::warn!(
"[DEPRECATED] 以下环境变量已废弃,建议迁移:\n {}\n 详见 docs/zh/ENV_REFERENCE.md",
hints.join("\n ")
);
}
});
}
fn parse_dotenv_content(content: &str) -> Vec<(String, String)> {
let mut vars = Vec::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(eq_pos) = line.find('=') {
let key = line[..eq_pos].trim().to_string();
let mut value = line[eq_pos + 1..].trim();
if let Some(hash_pos) = value.find('#') {
let before_hash = value[..hash_pos].trim_end();
if !before_hash.contains('"') && !before_hash.contains('\'') {
value = before_hash;
}
}
if (value.starts_with('"') && value.ends_with('"'))
|| (value.starts_with('\'') && value.ends_with('\''))
{
value = &value[1..value.len() - 1];
}
if !key.is_empty() {
vars.push((key, value.to_string()));
}
}
}
vars
}
pub fn parse_dotenv_from_dir(dir: &std::path::Path) -> Vec<(String, String)> {
let path = dir.join(".env");
if let Ok(content) = std::fs::read_to_string(&path) {
parse_dotenv_content(&content)
} else {
vec![]
}
}
pub fn parse_dotenv_walking_up(
start: &std::path::Path,
max_levels: usize,
) -> Vec<(String, String)> {
let mut dir = start.canonicalize().unwrap_or_else(|_| start.to_path_buf());
for _ in 0..max_levels {
let vars = parse_dotenv_from_dir(&dir);
if !vars.is_empty() {
return vars;
}
if !dir.pop() {
break;
}
}
vec![]
}
pub fn load_dotenv_from_dir(dir: &std::path::Path) {
for (key, value) in parse_dotenv_from_dir(dir) {
if env::var(&key).is_err() {
#[allow(unsafe_code)]
unsafe {
env::set_var(&key, &value);
}
}
}
}
pub fn load_dotenv() {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| {
let path = env::current_dir()
.map(|d| d.join(".env"))
.unwrap_or_else(|_| std::path::PathBuf::from(".env"));
if let Ok(content) = std::fs::read_to_string(&path) {
for (key, value) in parse_dotenv_content(&content) {
if env::var(&key).is_err() {
#[allow(unsafe_code)]
unsafe {
env::set_var(&key, &value);
}
}
}
}
warn_deprecated_env_vars();
});
}
pub fn env_or<F>(primary: &str, aliases: &[&str], default: F) -> String
where
F: FnOnce() -> String,
{
env::var(primary)
.ok()
.or_else(|| aliases.iter().find_map(|a| env::var(a).ok()))
.filter(|s| !s.is_empty())
.unwrap_or_else(default)
}
pub fn env_optional(primary: &str, aliases: &[&str]) -> Option<String> {
env::var(primary)
.ok()
.or_else(|| aliases.iter().find_map(|a| env::var(a).ok()))
.and_then(|s| {
let s = s.trim().to_string();
if s.is_empty() {
None
} else {
Some(s)
}
})
}
pub fn env_bool(primary: &str, aliases: &[&str], default: bool) -> bool {
let v = env::var(primary)
.ok()
.or_else(|| aliases.iter().find_map(|a| env::var(a).ok()));
match v.as_deref() {
Some(s) => !matches!(
s.trim().to_lowercase().as_str(),
"0" | "false" | "no" | "off"
),
None => default,
}
}
pub fn supply_chain_block_enabled() -> bool {
use crate::config::env_keys::observability;
env_bool(observability::SKILLLITE_SUPPLY_CHAIN_BLOCK, &[], false)
}
#[allow(dead_code)] pub fn env_is_set(primary: &str, aliases: &[&str]) -> bool {
env::var(primary).is_ok() || aliases.iter().any(|a| env::var(a).is_ok())
}
#[allow(unsafe_code)]
pub fn set_env_var(key: &str, value: &str) {
unsafe { env::set_var(key, value) };
}
#[allow(unsafe_code)]
pub fn remove_env_var(key: &str) {
unsafe { env::remove_var(key) };
}
pub fn init_llm_env(api_base: &str, api_key: &str, model: &str) {
set_env_var("OPENAI_API_BASE", api_base);
set_env_var("OPENAI_API_KEY", api_key);
set_env_var("SKILLLITE_MODEL", model);
}
pub fn init_daemon_env() {
set_env_var("SKILLLITE_AUTO_APPROVE", "1");
set_env_var("SKILLLITE_QUIET", "1");
}
pub fn ensure_default_output_dir() {
let paths = super::PathsConfig::from_env();
if paths.output_dir.is_none() {
let chat_output = crate::paths::chat_root().join("output");
let s = chat_output.to_string_lossy().to_string();
set_env_var("SKILLLITE_OUTPUT_DIR", &s);
if !chat_output.exists() {
let _ = std::fs::create_dir_all(&chat_output);
}
} else if let Some(ref output_dir) = paths.output_dir {
let p = std::path::PathBuf::from(output_dir);
if !p.exists() {
let _ = std::fs::create_dir_all(&p);
}
}
}
pub struct ScopedEnvGuard(pub &'static str);
impl Drop for ScopedEnvGuard {
fn drop(&mut self) {
remove_env_var(self.0);
}
}