1use std::fs;
2use std::path::Path;
3
4use serde::{Deserialize, Serialize};
5
6use crate::error::ConfigError;
7
8#[derive(Debug, Clone, Serialize, Deserialize, Default)]
9pub struct PluginsConfig {
10 #[serde(default)]
11 pub hooks: Vec<HookConfig>,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct HookConfig {
16 pub event: HookEvent,
17 pub command: String,
18}
19
20#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
21#[serde(rename_all = "snake_case")]
22pub enum HookEvent {
23 BeforeTurn,
24 AfterTurn,
25}
26
27pub fn load_plugins(project_root: &Path) -> PluginsConfig {
28 let path = project_root.join(".codei").join("hooks.toml");
29 if !path.is_file() {
30 return PluginsConfig::default();
31 }
32 match fs::read_to_string(&path) {
33 Ok(text) => toml::from_str(&text).unwrap_or_default(),
34 Err(_) => PluginsConfig::default(),
35 }
36}
37
38pub fn run_hook(hook: &HookConfig, cwd: &Path, env: &[(&str, String)]) -> Result<(), ConfigError> {
39 let mut cmd = std::process::Command::new("sh");
40 cmd.arg("-lc").arg(&hook.command).current_dir(cwd);
41 for (key, value) in env {
42 cmd.env(key, value);
43 }
44 let status = cmd.status().map_err(|source| ConfigError::Read {
45 path: cwd.to_path_buf(),
46 source,
47 })?;
48 if !status.success() {
49 return Err(ConfigError::HookFailed {
50 command: hook.command.clone(),
51 code: status.code(),
52 });
53 }
54 Ok(())
55}
56
57pub fn run_hooks(
58 config: &PluginsConfig,
59 event: HookEvent,
60 cwd: &Path,
61 env: &[(&str, String)],
62) -> Result<(), ConfigError> {
63 for hook in config.hooks.iter().filter(|hook| hook.event == event) {
64 run_hook(hook, cwd, env)?;
65 }
66 Ok(())
67}