Skip to main content

hx_plugins/
config.rs

1//! Plugin configuration types.
2
3use crate::hooks::HookEvent;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::PathBuf;
7
8/// Plugin system configuration from hx.toml `[plugins]` section.
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10#[serde(default)]
11pub struct PluginConfig {
12    /// Whether plugins are enabled.
13    pub enabled: bool,
14
15    /// Timeout for hook execution in milliseconds.
16    pub hook_timeout_ms: u64,
17
18    /// Additional paths to search for plugins.
19    pub paths: Vec<String>,
20
21    /// Whether to continue on hook failure.
22    pub continue_on_error: bool,
23
24    /// Hook configuration.
25    #[serde(default)]
26    pub hooks: HookConfig,
27
28    /// Whether project-local plugins (`.hx/plugins`) may be loaded.
29    ///
30    /// Never read from the manifest: local plugins run arbitrary code, so the
31    /// project that ships them cannot be the one to authorize them. The CLI
32    /// sets this after checking the user's trust list in the global config.
33    #[serde(skip)]
34    pub trust_local: bool,
35}
36
37impl PluginConfig {
38    /// Create a new plugin config with defaults.
39    pub fn new() -> Self {
40        PluginConfig {
41            enabled: true,
42            hook_timeout_ms: 5000,
43            paths: vec![],
44            continue_on_error: false,
45            hooks: HookConfig::default(),
46            trust_local: false,
47        }
48    }
49
50    /// Path to a project's local plugins directory.
51    pub fn local_plugins_dir(project_root: &std::path::Path) -> PathBuf {
52        project_root.join(".hx").join("plugins")
53    }
54
55    /// Get the timeout as a Duration.
56    pub fn hook_timeout(&self) -> std::time::Duration {
57        std::time::Duration::from_millis(self.hook_timeout_ms)
58    }
59
60    /// Get all plugin search paths, including defaults.
61    ///
62    /// The project-local directory is only included when the project has been
63    /// trusted (see [`PluginConfig::trust_local`]).
64    pub fn all_paths(&self, project_root: &std::path::Path) -> Vec<PathBuf> {
65        let mut paths = Vec::new();
66
67        // Project-local plugins first, but only for trusted projects
68        if self.trust_local {
69            paths.push(Self::local_plugins_dir(project_root));
70        }
71
72        // User-specified paths
73        for path in &self.paths {
74            let expanded = shellexpand::tilde(path);
75            paths.push(PathBuf::from(expanded.as_ref()));
76        }
77
78        // Global plugins last
79        if let Some(config_dir) = dirs::config_dir() {
80            paths.push(config_dir.join("hx").join("plugins"));
81        }
82
83        paths
84    }
85
86    /// Get scripts for a specific hook event.
87    pub fn scripts_for_hook(&self, event: HookEvent) -> &[String] {
88        match event {
89            HookEvent::PreBuild => &self.hooks.pre_build,
90            HookEvent::PostBuild => &self.hooks.post_build,
91            HookEvent::PreTest => &self.hooks.pre_test,
92            HookEvent::PostTest => &self.hooks.post_test,
93            HookEvent::PreRun => &self.hooks.pre_run,
94            HookEvent::PostRun => &self.hooks.post_run,
95            HookEvent::PreClean => &self.hooks.pre_clean,
96            HookEvent::PostClean => &self.hooks.post_clean,
97            HookEvent::PreLock => &self.hooks.pre_lock,
98            HookEvent::PostLock => &self.hooks.post_lock,
99            HookEvent::Init => &self.hooks.init,
100        }
101    }
102}
103
104/// Hook-specific configuration mapping events to scripts.
105#[derive(Debug, Clone, Default, Serialize, Deserialize)]
106#[serde(default)]
107pub struct HookConfig {
108    /// Scripts to run before build.
109    pub pre_build: Vec<String>,
110
111    /// Scripts to run after build.
112    pub post_build: Vec<String>,
113
114    /// Scripts to run before tests.
115    pub pre_test: Vec<String>,
116
117    /// Scripts to run after tests.
118    pub post_test: Vec<String>,
119
120    /// Scripts to run before run command.
121    pub pre_run: Vec<String>,
122
123    /// Scripts to run after run completes.
124    pub post_run: Vec<String>,
125
126    /// Scripts to run before clean.
127    pub pre_clean: Vec<String>,
128
129    /// Scripts to run after clean.
130    pub post_clean: Vec<String>,
131
132    /// Scripts to run before lock generation.
133    pub pre_lock: Vec<String>,
134
135    /// Scripts to run after lock completes.
136    pub post_lock: Vec<String>,
137
138    /// Scripts to run on project initialization.
139    pub init: Vec<String>,
140}
141
142impl HookConfig {
143    /// Check if any hooks are configured.
144    pub fn has_any_hooks(&self) -> bool {
145        !self.pre_build.is_empty()
146            || !self.post_build.is_empty()
147            || !self.pre_test.is_empty()
148            || !self.post_test.is_empty()
149            || !self.pre_run.is_empty()
150            || !self.post_run.is_empty()
151            || !self.pre_clean.is_empty()
152            || !self.post_clean.is_empty()
153            || !self.pre_lock.is_empty()
154            || !self.post_lock.is_empty()
155            || !self.init.is_empty()
156    }
157
158    /// Get a map of event to scripts.
159    pub fn as_map(&self) -> HashMap<HookEvent, &[String]> {
160        let mut map = HashMap::new();
161        map.insert(HookEvent::PreBuild, self.pre_build.as_slice());
162        map.insert(HookEvent::PostBuild, self.post_build.as_slice());
163        map.insert(HookEvent::PreTest, self.pre_test.as_slice());
164        map.insert(HookEvent::PostTest, self.post_test.as_slice());
165        map.insert(HookEvent::PreRun, self.pre_run.as_slice());
166        map.insert(HookEvent::PostRun, self.post_run.as_slice());
167        map.insert(HookEvent::PreClean, self.pre_clean.as_slice());
168        map.insert(HookEvent::PostClean, self.post_clean.as_slice());
169        map.insert(HookEvent::PreLock, self.pre_lock.as_slice());
170        map.insert(HookEvent::PostLock, self.post_lock.as_slice());
171        map.insert(HookEvent::Init, self.init.as_slice());
172        map
173    }
174}
175
176// Use dirs crate for platform-independent config directory
177mod dirs {
178    use std::path::PathBuf;
179
180    pub fn config_dir() -> Option<PathBuf> {
181        directories::BaseDirs::new().map(|dirs| dirs.config_dir().to_path_buf())
182    }
183}
184
185// Simple tilde expansion
186mod shellexpand {
187    use std::borrow::Cow;
188
189    pub fn tilde(path: &str) -> Cow<'_, str> {
190        if path.starts_with("~/")
191            && let Some(home) = directories::BaseDirs::new()
192        {
193            let home_str = home.home_dir().to_string_lossy();
194            return Cow::Owned(format!("{}{}", home_str, &path[1..]));
195        }
196        Cow::Borrowed(path)
197    }
198}
199
200// Conversion from hx_config::PluginConfig to hx_plugins::PluginConfig
201impl From<hx_config::PluginConfig> for PluginConfig {
202    fn from(config: hx_config::PluginConfig) -> Self {
203        PluginConfig {
204            enabled: config.enabled,
205            hook_timeout_ms: config.hook_timeout_ms,
206            paths: config.paths,
207            continue_on_error: config.continue_on_error,
208            hooks: HookConfig::from(config.hooks),
209            trust_local: false,
210        }
211    }
212}
213
214impl From<hx_config::PluginHookConfig> for HookConfig {
215    fn from(hooks: hx_config::PluginHookConfig) -> Self {
216        HookConfig {
217            pre_build: hooks.pre_build,
218            post_build: hooks.post_build,
219            pre_test: hooks.pre_test,
220            post_test: hooks.post_test,
221            pre_run: hooks.pre_run,
222            post_run: hooks.post_run,
223            pre_clean: hooks.pre_clean,
224            post_clean: hooks.post_clean,
225            pre_lock: hooks.pre_lock,
226            post_lock: hooks.post_lock,
227            init: hooks.init,
228        }
229    }
230}