返回 CodeWhale
context.rs
根目录 / crates / tui / src / plugins / context.rs
1 use std::env::VarError;
2 use std::ffi::{OsStr, OsString};
3 use std::path::{Path, PathBuf};
4 use std::sync::Arc;
5
6 use super::discovery::DiscoveryConfig;
7 use super::registry::PluginRegistry;
8
9 /// Immutable environment inherited from the host before workspace dotenv is
10 /// loaded. Reviewed plugins may resolve only values from this snapshot.
11 #[derive(Debug, Clone, Default)]
12 pub struct HostEnvironment {
13 entries: Arc<[(OsString, OsString)]>,
14 }
15
16 impl HostEnvironment {
17 #[must_use]
18 pub fn capture() -> Self {
19 Self {
20 entries: std::env::vars_os().collect::<Vec<_>>().into(),
21 }
22 }
23
24 #[cfg(test)]
25 #[must_use]
26 pub(crate) fn from_entries(entries: impl IntoIterator<Item = (OsString, OsString)>) -> Self {
27 Self {
28 entries: entries.into_iter().collect::<Vec<_>>().into(),
29 }
30 }
31
32 #[must_use]
33 pub fn entries(&self) -> &[(OsString, OsString)] {
34 &self.entries
35 }
36
37 #[must_use]
38 pub fn get_os(&self, name: &OsStr) -> Option<&OsStr> {
39 self.entries
40 .iter()
41 .rev()
42 .find(|(key, _)| environment_names_equal(key, name))
43 .map(|(_, value)| value.as_os_str())
44 }
45
46 pub fn var(&self, name: &str) -> Result<String, VarError> {
47 let Some(value) = self.get_os(OsStr::new(name)) else {
48 return Err(VarError::NotPresent);
49 };
50 value
51 .to_str()
52 .map(str::to_owned)
53 .ok_or_else(|| VarError::NotUnicode(value.to_os_string()))
54 }
55 }
56
57 fn environment_names_equal(left: &OsStr, right: &OsStr) -> bool {
58 #[cfg(windows)]
59 {
60 left.to_string_lossy()
61 .eq_ignore_ascii_case(&right.to_string_lossy())
62 }
63 #[cfg(not(windows))]
64 {
65 left == right
66 }
67 }
68
69 /// Process-lifetime plugin discovery inputs captured before repository-local
70 /// dotenv files can affect the process environment.
71 #[derive(Debug, Clone)]
72 pub struct PluginDiscoveryContext {
73 user_plugins_dir: PathBuf,
74 state_path: PathBuf,
75 builtin_plugin_dirs: Arc<[PathBuf]>,
76 host_environment: Arc<HostEnvironment>,
77 }
78
79 impl PluginDiscoveryContext {
80 #[must_use]
81 pub fn capture_pre_dotenv() -> Arc<Self> {
82 let user_plugins_dir = super::discovery::default_user_plugins_dir();
83 Arc::new(Self {
84 state_path: user_plugins_dir.join("state.json"),
85 user_plugins_dir,
86 builtin_plugin_dirs: Arc::from([]),
87 host_environment: Arc::new(HostEnvironment::capture()),
88 })
89 }
90
91 #[cfg(test)]
92 #[must_use]
93 pub(crate) fn from_config_and_environment(
94 config: &DiscoveryConfig,
95 host_environment: HostEnvironment,
96 ) -> Arc<Self> {
97 Arc::new(Self {
98 user_plugins_dir: config.user_plugins_dir.clone(),
99 state_path: config.state_path.clone(),
100 builtin_plugin_dirs: config.builtin_plugin_dirs.clone().into(),
101 host_environment: Arc::new(host_environment),
102 })
103 }
104
105 #[must_use]
106 pub fn registry_for_workspace(self: &Arc<Self>, workspace: &Path) -> Arc<PluginRegistry> {
107 let config = DiscoveryConfig {
108 workspace: workspace.to_path_buf(),
109 user_plugins_dir: self.user_plugins_dir.clone(),
110 workspace_plugins_dir: super::discovery::default_workspace_plugins_dir(workspace),
111 builtin_plugin_dirs: self.builtin_plugin_dirs.to_vec(),
112 state_path: self.state_path.clone(),
113 };
114 Arc::new(super::discovery::discover_with_context(
115 &config,
116 Arc::clone(self),
117 ))
118 }
119
120 #[must_use]
121 pub fn host_environment(&self) -> Arc<HostEnvironment> {
122 Arc::clone(&self.host_environment)
123 }
124
125 /// The pre-dotenv user plugins root (`~/.codewhale/plugins`). Exposed for
126 /// the install on-ramp (#5182), which must fetch into exactly the root
127 /// this context discovers from.
128 #[must_use]
129 pub fn user_plugins_dir(&self) -> &Path {
130 &self.user_plugins_dir
131 }
132 }
133
134 #[cfg(test)]
135 mod tests {
136 use std::ffi::OsString;
137 use std::fs;
138
139 use super::{HostEnvironment, PluginDiscoveryContext};
140
141 #[test]
142 fn host_environment_is_an_immutable_value_snapshot() {
143 let snapshot = HostEnvironment::from_entries([(
144 OsString::from("PLUGIN_TOKEN"),
145 OsString::from("before"),
146 )]);
147
148 assert_eq!(snapshot.var("PLUGIN_TOKEN").unwrap(), "before");
149 assert!(snapshot.var("MISSING").is_err());
150 }
151
152 #[test]
153 fn discovery_roots_are_frozen_before_later_environment_changes() {
154 let _lock = crate::test_support::lock_test_env();
155 let temp = tempfile::tempdir().unwrap();
156 let first_home = temp.path().join("first-home");
157 let second_home = temp.path().join("second-home");
158 let workspace = temp.path().join("workspace");
159 fs::create_dir_all(&workspace).unwrap();
160 let _first = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &first_home);
161 let context = PluginDiscoveryContext::capture_pre_dotenv();
162 let _second = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &second_home);
163
164 for (home, name) in [(&first_home, "before"), (&second_home, "after")] {
165 let bundle = home.join("plugins").join(name);
166 fs::create_dir_all(&bundle).unwrap();
167 fs::write(
168 bundle.join("plugin.toml"),
169 format!("schema_version = 1\n[plugin]\nname = \"{name}\"\nversion = \"1.0.0\"\n"),
170 )
171 .unwrap();
172 }
173
174 let registry = context.registry_for_workspace(&workspace);
175 assert!(registry.get("before").is_some());
176 assert!(registry.get("after").is_none());
177 assert_eq!(
178 registry.state_path(),
179 Some(first_home.join("plugins/state.json").as_path())
180 );
181 }
182
183 #[test]
184 fn contextless_rediscovery_remains_empty_and_tracks_the_requested_workspace() {
185 let temp = tempfile::tempdir().unwrap();
186 let workspace = temp.path().join("workspace");
187 let next_workspace = temp.path().join("next-workspace");
188 let bundle = next_workspace.join(".codewhale/plugins/ambient");
189 fs::create_dir_all(&bundle).unwrap();
190 fs::write(
191 bundle.join("plugin.toml"),
192 "schema_version = 1\n[plugin]\nname = \"ambient\"\nversion = \"1.0.0\"\n",
193 )
194 .unwrap();
195
196 let registry = crate::plugins::PluginRegistry::empty(&workspace)
197 .rediscover_for_workspace(&next_workspace);
198 assert!(registry.is_empty());
199 assert_eq!(registry.workspace(), next_workspace);
200 }
201 }
202
202 lines RUST