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
29impl PluginConfig {
30    /// Create a new plugin config with defaults.
31    pub fn new() -> Self {
32        PluginConfig {
33            enabled: true,
34            hook_timeout_ms: 5000,
35            paths: vec![],
36            continue_on_error: false,
37            hooks: HookConfig::default(),
38        }
39    }
40
41    /// Get the timeout as a Duration.
42    pub fn hook_timeout(&self) -> std::time::Duration {
43        std::time::Duration::from_millis(self.hook_timeout_ms)
44    }
45
46    /// Get all plugin search paths, including defaults.
47    pub fn all_paths(&self, project_root: &std::path::Path) -> Vec<PathBuf> {
48        let mut paths = Vec::new();
49
50        // Project-local plugins first
51        paths.push(project_root.join(".hx").join("plugins"));
52
53        // User-specified paths
54        for path in &self.paths {
55            let expanded = shellexpand::tilde(path);
56            paths.push(PathBuf::from(expanded.as_ref()));
57        }
58
59        // Global plugins last
60        if let Some(config_dir) = dirs::config_dir() {
61            paths.push(config_dir.join("hx").join("plugins"));
62        }
63
64        paths
65    }
66
67    /// Get scripts for a specific hook event.
68    pub fn scripts_for_hook(&self, event: HookEvent) -> &[String] {
69        match event {
70            HookEvent::PreBuild => &self.hooks.pre_build,
71            HookEvent::PostBuild => &self.hooks.post_build,
72            HookEvent::PreTest => &self.hooks.pre_test,
73            HookEvent::PostTest => &self.hooks.post_test,
74            HookEvent::PreRun => &self.hooks.pre_run,
75            HookEvent::PostRun => &self.hooks.post_run,
76            HookEvent::PreClean => &self.hooks.pre_clean,
77            HookEvent::PostClean => &self.hooks.post_clean,
78            HookEvent::PreLock => &self.hooks.pre_lock,
79            HookEvent::PostLock => &self.hooks.post_lock,
80            HookEvent::Init => &self.hooks.init,
81        }
82    }
83}
84
85/// Hook-specific configuration mapping events to scripts.
86#[derive(Debug, Clone, Default, Serialize, Deserialize)]
87#[serde(default)]
88pub struct HookConfig {
89    /// Scripts to run before build.
90    pub pre_build: Vec<String>,
91
92    /// Scripts to run after build.
93    pub post_build: Vec<String>,
94
95    /// Scripts to run before tests.
96    pub pre_test: Vec<String>,
97
98    /// Scripts to run after tests.
99    pub post_test: Vec<String>,
100
101    /// Scripts to run before run command.
102    pub pre_run: Vec<String>,
103
104    /// Scripts to run after run completes.
105    pub post_run: Vec<String>,
106
107    /// Scripts to run before clean.
108    pub pre_clean: Vec<String>,
109
110    /// Scripts to run after clean.
111    pub post_clean: Vec<String>,
112
113    /// Scripts to run before lock generation.
114    pub pre_lock: Vec<String>,
115
116    /// Scripts to run after lock completes.
117    pub post_lock: Vec<String>,
118
119    /// Scripts to run on project initialization.
120    pub init: Vec<String>,
121}
122
123impl HookConfig {
124    /// Check if any hooks are configured.
125    pub fn has_any_hooks(&self) -> bool {
126        !self.pre_build.is_empty()
127            || !self.post_build.is_empty()
128            || !self.pre_test.is_empty()
129            || !self.post_test.is_empty()
130            || !self.pre_run.is_empty()
131            || !self.post_run.is_empty()
132            || !self.pre_clean.is_empty()
133            || !self.post_clean.is_empty()
134            || !self.pre_lock.is_empty()
135            || !self.post_lock.is_empty()
136            || !self.init.is_empty()
137    }
138
139    /// Get a map of event to scripts.
140    pub fn as_map(&self) -> HashMap<HookEvent, &[String]> {
141        let mut map = HashMap::new();
142        map.insert(HookEvent::PreBuild, self.pre_build.as_slice());
143        map.insert(HookEvent::PostBuild, self.post_build.as_slice());
144        map.insert(HookEvent::PreTest, self.pre_test.as_slice());
145        map.insert(HookEvent::PostTest, self.post_test.as_slice());
146        map.insert(HookEvent::PreRun, self.pre_run.as_slice());
147        map.insert(HookEvent::PostRun, self.post_run.as_slice());
148        map.insert(HookEvent::PreClean, self.pre_clean.as_slice());
149        map.insert(HookEvent::PostClean, self.post_clean.as_slice());
150        map.insert(HookEvent::PreLock, self.pre_lock.as_slice());
151        map.insert(HookEvent::PostLock, self.post_lock.as_slice());
152        map.insert(HookEvent::Init, self.init.as_slice());
153        map
154    }
155}
156
157// Use dirs crate for platform-independent config directory
158mod dirs {
159    use std::path::PathBuf;
160
161    pub fn config_dir() -> Option<PathBuf> {
162        directories::BaseDirs::new().map(|dirs| dirs.config_dir().to_path_buf())
163    }
164}
165
166// Simple tilde expansion
167mod shellexpand {
168    use std::borrow::Cow;
169
170    pub fn tilde(path: &str) -> Cow<'_, str> {
171        if path.starts_with("~/")
172            && let Some(home) = directories::BaseDirs::new()
173        {
174            let home_str = home.home_dir().to_string_lossy();
175            return Cow::Owned(format!("{}{}", home_str, &path[1..]));
176        }
177        Cow::Borrowed(path)
178    }
179}
180
181// Conversion from hx_config::PluginConfig to hx_plugins::PluginConfig
182impl From<hx_config::PluginConfig> for PluginConfig {
183    fn from(config: hx_config::PluginConfig) -> Self {
184        PluginConfig {
185            enabled: config.enabled,
186            hook_timeout_ms: config.hook_timeout_ms,
187            paths: config.paths,
188            continue_on_error: config.continue_on_error,
189            hooks: HookConfig::from(config.hooks),
190        }
191    }
192}
193
194impl From<hx_config::PluginHookConfig> for HookConfig {
195    fn from(hooks: hx_config::PluginHookConfig) -> Self {
196        HookConfig {
197            pre_build: hooks.pre_build,
198            post_build: hooks.post_build,
199            pre_test: hooks.pre_test,
200            post_test: hooks.post_test,
201            pre_run: hooks.pre_run,
202            post_run: hooks.post_run,
203            pre_clean: hooks.pre_clean,
204            post_clean: hooks.post_clean,
205            pre_lock: hooks.pre_lock,
206            post_lock: hooks.post_lock,
207            init: hooks.init,
208        }
209    }
210}