Skip to main content

envmgr/environment/
manager.rs

1use 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        // Unset current environment variables
42        let mut state = State::get_state()?;
43        let target_env_key = state.current_env_key.clone();
44
45        state.applied_env_vars.clear();
46        // Set new environment variables
47        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        // Remove keys that are no longer present
66        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        // Set all new/updated variables
79        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            // No change
92            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        // Integrations
102        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        // Switch
123        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            // Remove previously managed dangling symlink.
153            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                // Handle both valid and dangling symlinks
171                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                // A real file/dir exists at the target and it's not a symlink – do not overwrite
191                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}