envmgr/environment/
manager.rs1use std::collections::HashMap;
2
3use log::{debug, info, warn};
4
5use crate::{
6 cli::Shell,
7 config::{BASE_ENV_NAME, EnvVarsConfig, EnvironmentConfig},
8 environment::Environment,
9 error::EnvMgrResult,
10 integrations::one_password_ssh_agent::OnePasswordSSHAgent,
11 state::State,
12};
13
14pub struct EnvironmentManager {
15 pub shell: Shell,
16}
17
18impl EnvironmentManager {
19 pub fn list_environments() -> EnvMgrResult<Vec<(bool, Environment)>> {
20 let state = State::get_state()?;
21 let envs_dir = EnvironmentConfig::get_all_envs_dir();
22 if !envs_dir.exists() {
23 return Ok(vec![]);
24 }
25 let base = Environment::load_base_environment()?;
26
27 let mut environments = vec![(state.current_env_key == base.key, base)];
28 for entry in std::fs::read_dir(envs_dir)? {
29 let entry = entry?;
30 if entry.file_type()?.is_dir()
31 && let Some(env_key) = entry.file_name().to_str()
32 {
33 let env = Environment::load_environment_by_key(env_key)?;
34 environments.push((state.current_env_key == env.key, env));
35 }
36 }
37 Ok(environments)
38 }
39
40 pub fn use_environment(&self) -> EnvMgrResult<()> {
41 let mut state = State::get_state()?;
43 let target_env_key = state.current_env_key.clone();
44
45 state.applied_env_vars.clear();
46 let base_environment = Environment::load_base_environment()?;
48
49 let mut new_vars = HashMap::new();
50
51 for EnvVarsConfig { key, value } in base_environment.env_vars {
52 new_vars.insert(key, value);
53 }
54
55 if target_env_key != BASE_ENV_NAME {
56 let environment = Environment::load_environment_by_key(&target_env_key)?;
57 state.current_env_key = environment.key.to_string();
58 for EnvVarsConfig { key, value } in environment.env_vars {
59 new_vars.insert(key, value);
60 }
61 } else {
62 state.current_env_key = BASE_ENV_NAME.to_string();
63 }
64
65 let keys_to_remove: Vec<String> = state
67 .applied_env_vars
68 .keys()
69 .filter(|k| !new_vars.contains_key(*k))
70 .cloned()
71 .collect();
72
73 for key in keys_to_remove {
74 println!("{}", self.shell.unset_env_var_cmd(&key));
75 state.applied_env_vars.remove(&key);
76 }
77
78 for (key, value) in new_vars {
80 println!("{}", self.shell.set_env_var_cmd(&key, &value));
81 state.applied_env_vars.insert(key, value);
82 }
83
84 state.store_state()?;
85 Ok(())
86 }
87
88 fn switch_environment(environment: &Environment) -> EnvMgrResult<()> {
89 let mut state = State::get_state()?;
90 if state.current_env_key == environment.key {
91 debug!("Environment {} is already active", environment.name);
93 return Ok(());
94 }
95 info!(
96 "Switching to environment: {} ({})",
97 environment.name, environment.key
98 );
99 state.current_env_key = environment.key.to_string();
100
101 if let Some(op_ssh_config) = environment.one_password_ssh.as_ref() {
103 OnePasswordSSHAgent::on_switch_to(op_ssh_config)?;
104 }
105
106 if let Some(gh_cli_config) = environment.gh_cli.as_ref() {
107 crate::integrations::gh_cli::GhCli::on_switch_to(gh_cli_config)?;
108 }
109
110 if let Some(tailscale_config) = environment.tailscale.as_ref() {
111 crate::integrations::tailscale::Tailscale::on_switch_to(tailscale_config)?;
112 }
113
114 state.store_state()?;
115 Self::link_files()?;
116 Ok(())
117 }
118
119 pub fn switch_environment_by_key(key: &str) -> EnvMgrResult<()> {
120 let environment = Environment::load_environment_by_key(key)?;
121
122 Self::switch_environment(&environment)?;
124
125 Ok(())
126 }
127
128 pub fn switch_base_environment() -> EnvMgrResult<()> {
129 let base_environment = Environment::load_base_environment()?;
130
131 Self::switch_environment(&base_environment)?;
132
133 Ok(())
134 }
135
136 pub fn link_files() -> EnvMgrResult<()> {
137 let mut state = State::get_state()?;
138
139 let base_environment = Environment::load_base_environment()?;
140 let mut files_map = base_environment.files_to_link()?;
141
142 if state.current_env_key != BASE_ENV_NAME {
143 let environment = Environment::load_environment_by_key(&state.current_env_key)?;
144 files_map.extend(environment.files_to_link()?);
145 }
146
147 for managed_file in state
148 .managed_files
149 .iter()
150 .filter(|f| !files_map.contains_key(*f))
151 {
152 if managed_file.is_symlink() {
154 info!("Removing stale symlink: {}", managed_file.display());
155 std::fs::remove_file(managed_file)?;
156 } else if managed_file.exists() {
157 warn!(
158 "Managed file exists and is not a symlink, skipping removal: {}",
159 managed_file.display()
160 );
161 }
162 }
163
164 state.managed_files.clear();
165
166 for (target_path, source_path) in files_map {
167 let mut need_link = true;
168
169 if target_path.is_symlink() {
170 let existing_link = std::fs::read_link(&target_path)?;
172 if existing_link == source_path {
173 debug!(
174 "Symlink already exists and is correct: {} -> {}",
175 target_path.display(),
176 source_path.display()
177 );
178 state.managed_files.push(target_path.clone());
179 need_link = false;
180 } else {
181 info!(
182 "Updating symlink: {} (was {}) -> {}",
183 target_path.display(),
184 existing_link.display(),
185 source_path.display()
186 );
187 std::fs::remove_file(&target_path)?;
188 }
189 } else if target_path.exists() {
190 warn!(
192 "Target path exists and is not a symlink, skipping: {}",
193 target_path.display()
194 );
195 need_link = false;
196 } else if let Some(parent) = target_path.parent()
197 && !parent.exists()
198 {
199 info!("Creating parent directory: {}", parent.display());
200 std::fs::create_dir_all(parent)?;
201 }
202
203 if need_link {
204 info!(
205 "Creating symlink: {} -> {}",
206 target_path.display(),
207 source_path.display()
208 );
209 std::os::unix::fs::symlink(&source_path, &target_path)?;
210 state.managed_files.push(target_path.clone());
211 }
212 }
213
214 state.store_state()?;
215
216 Ok(())
217 }
218}