1pub mod broker;
2pub mod catalog;
3pub mod commands;
4pub mod identity;
5pub mod proxy;
6pub mod upgrade;
7pub mod workspace;
8
9use crate::config::Config;
10use anyhow::{Result, bail};
11use std::collections::{BTreeMap, BTreeSet};
12
13#[derive(Clone, Debug, Default)]
18pub struct EnvConfig {
19 vars: BTreeMap<String, String>,
20 descriptions: BTreeMap<String, String>,
21}
22
23impl EnvConfig {
24 pub fn from_config(config: &Config) -> Self {
25 Self {
26 vars: config.env.clone(),
27 descriptions: config.env_descriptions.clone(),
28 }
29 }
30
31 pub async fn load_or_init(config: &Config) -> Result<Self> {
32 Ok(Self::from_config(config))
33 }
34
35 pub fn get(&self, key: &str) -> Option<&str> {
36 self.vars.get(key).map(|s| s.as_str())
37 }
38
39 pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
40 self.vars.insert(key.into(), value.into());
41 }
42
43 pub fn remove(&mut self, key: &str) -> Option<String> {
44 self.vars.remove(key)
45 }
46
47 pub fn as_map(&self) -> &BTreeMap<String, String> {
48 &self.vars
49 }
50
51 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
52 self.vars.iter().map(|(k, v)| (k.as_str(), v.as_str()))
53 }
54
55 pub fn description(&self, key: &str) -> Option<&str> {
56 self.descriptions.get(key).map(String::as_str)
57 }
58
59 pub async fn save(&self, config: &Config) -> Result<()> {
60 let mut updated = config.clone();
61 updated.env = self.vars.clone();
62 updated.save().await
63 }
64}
65
66impl From<EnvConfig> for BTreeMap<String, String> {
67 fn from(value: EnvConfig) -> Self {
68 value.vars
69 }
70}
71
72#[derive(Debug, PartialEq, Eq)]
73pub enum StoredValue<'a> {
75 Secret { key: String, value: &'a str },
76 Plaintext(&'a str),
77}
78
79pub fn resolve_stored_value<'a>(env: &'a EnvConfig, key: &str) -> Result<StoredValue<'a>> {
81 let secret_key = secret_key(key);
82 if let Some(value) = env.get(&secret_key) {
83 return Ok(StoredValue::Secret {
84 key: secret_key,
85 value,
86 });
87 }
88 if let Some(value) = env.get(key) {
89 return Ok(StoredValue::Plaintext(value));
90 }
91 bail!("{secret_key} or {key} is not set in the active config [env]");
92}
93
94pub fn secret_key(key: &str) -> String {
96 format!("{key}_SECRET")
97}
98
99#[derive(Clone, Debug, PartialEq, Eq)]
102pub struct EnvVarSpec {
103 pub source: String,
104 pub target: String,
105}
106
107impl EnvVarSpec {
108 pub fn to_with_arg(&self) -> String {
111 if self.source == self.target {
112 self.source.clone()
113 } else {
114 format!("{}={}", self.source, self.target)
115 }
116 }
117}
118
119pub(crate) fn parse_env_specs(specs: &[String]) -> Result<Vec<EnvVarSpec>> {
127 let mut parsed = Vec::with_capacity(specs.len());
128 let mut targets = BTreeSet::new();
129 for spec in specs {
130 let (source, target) = spec.split_once('=').unwrap_or((spec, spec));
131 validate_env_key(source)?;
132 validate_env_key(target)?;
133 if !targets.insert(target.to_string()) {
134 bail!("duplicate target variable: {target}");
135 }
136 parsed.push(EnvVarSpec {
137 source: source.to_string(),
138 target: target.to_string(),
139 });
140 }
141 Ok(parsed)
142}
143
144pub(crate) fn validate_env_key(key: &str) -> Result<()> {
147 let mut chars = key.chars();
148 let Some(first) = chars.next() else {
149 bail!("environment variable name must not be empty");
150 };
151 if !(first == '_' || first.is_ascii_alphabetic())
152 || !chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
153 {
154 bail!("invalid environment variable name: {key}");
155 }
156 Ok(())
157}
158
159impl EnvConfig {
160 #[cfg(test)]
161 fn with_defaults() -> Self {
162 Self {
163 vars: crate::config::default_env_map(),
164 descriptions: BTreeMap::new(),
165 }
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn from_config_reads_env_table() {
175 let dir = std::env::temp_dir().join(format!("shine-env-test-{}", uuid::Uuid::new_v4()));
176 let mut config = Config::new_for_test(&dir);
177 config.env.insert("HTTP_PROXY_PORT".into(), "7890".into());
178
179 let env = EnvConfig::from_config(&config);
180
181 assert_eq!(env.get("HTTP_PROXY_PORT"), Some("7890"));
182 }
183
184 #[test]
185 fn from_config_reads_description() {
186 let dir = std::env::temp_dir().join(format!("shine-env-test-{}", uuid::Uuid::new_v4()));
187 let mut config = Config::new_for_test(&dir);
188 config
189 .env_descriptions
190 .insert("MY_TOKEN".into(), "Internal token".into());
191
192 let env = EnvConfig::from_config(&config);
193
194 assert_eq!(env.description("MY_TOKEN"), Some("Internal token"));
195 }
196
197 #[test]
198 fn set_and_get_roundtrip() {
199 let mut env = EnvConfig::default();
200 env.set("MY_VAR", "hello");
201 assert_eq!(env.get("MY_VAR"), Some("hello"));
202 assert_eq!(env.get("OTHER"), None);
203 }
204
205 #[test]
206 fn remove_deletes_existing_key() {
207 let mut env = EnvConfig::default();
208 env.set("MY_VAR", "hello");
209
210 assert_eq!(env.remove("MY_VAR"), Some("hello".to_string()));
211 assert_eq!(env.get("MY_VAR"), None);
212 }
213
214 #[test]
215 fn remove_missing_key_returns_none() {
216 let mut env = EnvConfig::default();
217
218 assert_eq!(env.remove("OTHER"), None);
219 }
220
221 #[test]
222 fn as_map_reflects_all_vars() {
223 let mut env = EnvConfig::default();
224 env.set("A", "1");
225 env.set("B", "2");
226 let map = env.as_map();
227 assert_eq!(map.get("A").map(|s| s.as_str()), Some("1"));
228 assert_eq!(map.get("B").map(|s| s.as_str()), Some("2"));
229 }
230
231 #[test]
232 fn defaults_are_available_for_tests() {
233 let env = EnvConfig::with_defaults();
234 assert_eq!(env.get("HTTP_PROXY_PORT"), Some("6152"));
235 assert_eq!(env.get("SOCKS5_PROXY_PORT"), Some("6153"));
236 assert_eq!(env.get("PROXY_HOST"), Some("127.0.0.1"));
237 assert_eq!(env.get("PROXY_NO_PROXY"), Some("localhost,127.0.0.1,::1"));
238 assert_eq!(env.get("GHOSTTY_BG_LIGHT"), Some(""));
239 assert_eq!(env.get("GHOSTTY_BG_DARK"), Some(""));
240 }
241}