返回 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: super::builtin::materialized_dirs().into(),
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 let mut registry = super::discovery::discover_with_context(&config, Arc::clone(self));
115 // An upgrade re-roots the built-ins; keep their review (K4).
116 registry.carry_forward_builtin_trust();
117 Arc::new(registry)
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 #[must_use]
134 pub fn catalog_stamp_for_workspace(
135 &self,
136 workspace: &Path,
137 ) -> super::discovery::PluginCatalogStamp {
138 let mut roots = vec![
139 self.user_plugins_dir.clone(),
140 super::discovery::default_workspace_plugins_dir(workspace),
141 ];
142 roots.extend(self.builtin_plugin_dirs.iter().cloned());
143 super::discovery::PluginCatalogStamp::capture(roots)
144 }
145 }
146
147 #[cfg(test)]
148 mod tests {
149 use std::ffi::OsString;
150 use std::fs;
151
152 use super::{HostEnvironment, PluginDiscoveryContext};
153
154 #[test]
155 fn host_environment_is_an_immutable_value_snapshot() {
156 let snapshot = HostEnvironment::from_entries([(
157 OsString::from("PLUGIN_TOKEN"),
158 OsString::from("before"),
159 )]);
160
161 assert_eq!(snapshot.var("PLUGIN_TOKEN").unwrap(), "before");
162 assert!(snapshot.var("MISSING").is_err());
163 }
164
165 #[test]
166 fn discovery_roots_are_frozen_before_later_environment_changes() {
167 let _lock = crate::test_support::lock_test_env();
168 let temp = tempfile::tempdir().unwrap();
169 let first_home = temp.path().join("first-home");
170 let second_home = temp.path().join("second-home");
171 let workspace = temp.path().join("workspace");
172 fs::create_dir_all(&workspace).unwrap();
173 let _first = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &first_home);
174 let context = PluginDiscoveryContext::capture_pre_dotenv();
175 let _second = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &second_home);
176
177 for (home, name) in [(&first_home, "before"), (&second_home, "after")] {
178 let bundle = home.join("plugins").join(name);
179 fs::create_dir_all(&bundle).unwrap();
180 fs::write(
181 bundle.join("plugin.toml"),
182 format!("schema_version = 1\n[plugin]\nname = \"{name}\"\nversion = \"1.0.0\"\n"),
183 )
184 .unwrap();
185 }
186
187 let registry = context.registry_for_workspace(&workspace);
188 assert!(registry.get("before").is_some());
189 assert!(registry.get("after").is_none());
190 assert_eq!(
191 registry.state_path(),
192 Some(first_home.join("plugins/state.json").as_path())
193 );
194 }
195
196 #[test]
197 fn contextless_rediscovery_remains_empty_and_tracks_the_requested_workspace() {
198 let temp = tempfile::tempdir().unwrap();
199 let workspace = temp.path().join("workspace");
200 let next_workspace = temp.path().join("next-workspace");
201 let bundle = next_workspace.join(".codewhale/plugins/ambient");
202 fs::create_dir_all(&bundle).unwrap();
203 fs::write(
204 bundle.join("plugin.toml"),
205 "schema_version = 1\n[plugin]\nname = \"ambient\"\nversion = \"1.0.0\"\n",
206 )
207 .unwrap();
208
209 let registry = crate::plugins::PluginRegistry::empty(&workspace)
210 .rediscover_for_workspace(&next_workspace);
211 assert!(registry.is_empty());
212 assert_eq!(registry.workspace(), next_workspace);
213 }
214 }
215
215 lines RUST