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};
11pub use shine_core::env::EnvVarSpec;
12pub(crate) use shine_core::env::{parse_env_specs, validate_env_key};
13use std::collections::BTreeMap;
14
15#[derive(Clone, Debug, Default)]
20pub struct EnvConfig {
21 vars: BTreeMap<String, String>,
22 descriptions: BTreeMap<String, String>,
23}
24
25impl EnvConfig {
26 pub fn from_config(config: &Config) -> Self {
27 Self {
28 vars: config.env.clone(),
29 descriptions: config.env_descriptions.clone(),
30 }
31 }
32
33 pub async fn load_or_init(config: &Config) -> Result<Self> {
34 Ok(Self::from_config(config))
35 }
36
37 pub fn get(&self, key: &str) -> Option<&str> {
38 self.vars.get(key).map(|s| s.as_str())
39 }
40
41 pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
42 self.vars.insert(key.into(), value.into());
43 }
44
45 pub fn remove(&mut self, key: &str) -> Option<String> {
46 self.vars.remove(key)
47 }
48
49 pub fn as_map(&self) -> &BTreeMap<String, String> {
50 &self.vars
51 }
52
53 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
54 self.vars.iter().map(|(k, v)| (k.as_str(), v.as_str()))
55 }
56
57 pub fn description(&self, key: &str) -> Option<&str> {
58 self.descriptions.get(key).map(String::as_str)
59 }
60
61 pub async fn save(&self, config: &Config) -> Result<()> {
62 let mut updated = config.clone();
63 updated.env = self.vars.clone();
64 updated.save().await
65 }
66}
67
68impl From<EnvConfig> for BTreeMap<String, String> {
69 fn from(value: EnvConfig) -> Self {
70 value.vars
71 }
72}
73
74#[derive(Debug, PartialEq, Eq)]
75pub enum StoredValue<'a> {
77 Secret { key: String, value: &'a str },
78 Plaintext(&'a str),
79}
80
81pub fn resolve_stored_value<'a>(env: &'a EnvConfig, key: &str) -> Result<StoredValue<'a>> {
83 let secret_key = secret_key(key);
84 if let Some(value) = env.get(&secret_key) {
85 return Ok(StoredValue::Secret {
86 key: secret_key,
87 value,
88 });
89 }
90 if let Some(value) = env.get(key) {
91 return Ok(StoredValue::Plaintext(value));
92 }
93 bail!("{secret_key} or {key} is not set in the active config [env]");
94}
95
96pub fn secret_key(key: &str) -> String {
98 format!("{key}_SECRET")
99}
100
101impl EnvConfig {
102 #[cfg(test)]
103 fn with_defaults() -> Self {
104 Self {
105 vars: crate::config::default_env_map(),
106 descriptions: BTreeMap::new(),
107 }
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn from_config_reads_env_table() {
117 let dir = std::env::temp_dir().join(format!("shine-env-test-{}", uuid::Uuid::new_v4()));
118 let mut config = Config::new_for_test(&dir);
119 config.env.insert("HTTP_PROXY_PORT".into(), "7890".into());
120
121 let env = EnvConfig::from_config(&config);
122
123 assert_eq!(env.get("HTTP_PROXY_PORT"), Some("7890"));
124 }
125
126 #[test]
127 fn from_config_reads_description() {
128 let dir = std::env::temp_dir().join(format!("shine-env-test-{}", uuid::Uuid::new_v4()));
129 let mut config = Config::new_for_test(&dir);
130 config
131 .env_descriptions
132 .insert("MY_TOKEN".into(), "Internal token".into());
133
134 let env = EnvConfig::from_config(&config);
135
136 assert_eq!(env.description("MY_TOKEN"), Some("Internal token"));
137 }
138
139 #[test]
140 fn set_and_get_roundtrip() {
141 let mut env = EnvConfig::default();
142 env.set("MY_VAR", "hello");
143 assert_eq!(env.get("MY_VAR"), Some("hello"));
144 assert_eq!(env.get("OTHER"), None);
145 }
146
147 #[test]
148 fn remove_deletes_existing_key() {
149 let mut env = EnvConfig::default();
150 env.set("MY_VAR", "hello");
151
152 assert_eq!(env.remove("MY_VAR"), Some("hello".to_string()));
153 assert_eq!(env.get("MY_VAR"), None);
154 }
155
156 #[test]
157 fn remove_missing_key_returns_none() {
158 let mut env = EnvConfig::default();
159
160 assert_eq!(env.remove("OTHER"), None);
161 }
162
163 #[test]
164 fn as_map_reflects_all_vars() {
165 let mut env = EnvConfig::default();
166 env.set("A", "1");
167 env.set("B", "2");
168 let map = env.as_map();
169 assert_eq!(map.get("A").map(|s| s.as_str()), Some("1"));
170 assert_eq!(map.get("B").map(|s| s.as_str()), Some("2"));
171 }
172
173 #[test]
174 fn defaults_are_available_for_tests() {
175 let env = EnvConfig::with_defaults();
176 assert_eq!(env.get("HTTP_PROXY_PORT"), Some("6152"));
177 assert_eq!(env.get("SOCKS5_PROXY_PORT"), Some("6153"));
178 assert_eq!(env.get("PROXY_HOST"), Some("127.0.0.1"));
179 assert_eq!(env.get("PROXY_NO_PROXY"), Some("localhost,127.0.0.1,::1"));
180 assert_eq!(env.get("GHOSTTY_BG_LIGHT"), Some(""));
181 assert_eq!(env.get("GHOSTTY_BG_DARK"), Some(""));
182 }
183}