1use std::path::Path;
2
3use serde::Deserialize;
4
5const DEFAULT_IGNORE: &[&str] = &[
6 "README.md",
7 "CHANGELOG.md",
8 "CONTRIBUTING.md",
9 "ARCHIVED.md",
10];
11
12#[derive(Debug, Default, Deserialize)]
13pub struct HubConfig {
14 #[serde(default)]
15 pub hub: HubSection,
16 #[serde(default)]
17 pub generate: GenerateSection,
18}
19
20#[derive(Debug, Default, Deserialize)]
21pub struct HubSection {
22 pub id: Option<String>,
23}
24
25#[derive(Debug, Default, Deserialize)]
26pub struct GenerateSection {
27 pub ignore: Option<Vec<String>>,
28}
29
30impl HubConfig {
31 pub fn load(hub_path: &Path) -> Self {
33 let toml_path = hub_path.join("agentctl.toml");
34 if !toml_path.exists() {
35 return Self::default();
36 }
37 let content = match std::fs::read_to_string(&toml_path) {
38 Ok(c) => c,
39 Err(_) => return Self::default(),
40 };
41 toml::from_str(&content).unwrap_or_default()
42 }
43
44 pub fn ignore_list(&self) -> Vec<String> {
46 match &self.generate.ignore {
47 Some(list) => list.clone(),
48 None => DEFAULT_IGNORE.iter().map(|s| s.to_string()).collect(),
49 }
50 }
51
52 pub fn is_ignored(&self, filename: &str) -> bool {
54 let lower = filename.to_lowercase();
55 self.ignore_list()
56 .iter()
57 .any(|pattern| glob_match(pattern, &lower))
58 }
59}
60
61fn glob_match(pattern: &str, name: &str) -> bool {
63 let pattern = pattern.to_lowercase();
64 match pattern.find('*') {
65 None => name == pattern.as_str(),
66 Some(i) => name.starts_with(&pattern[..i]) && name.ends_with(&pattern[i + 1..]),
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn default_ignore_list() {
76 let cfg = HubConfig::default();
77 assert!(cfg.is_ignored("README.md"));
78 assert!(cfg.is_ignored("readme.md")); assert!(cfg.is_ignored("CHANGELOG.md"));
80 assert!(!cfg.is_ignored("my-doc.md"));
81 }
82
83 #[test]
84 fn custom_ignore_overrides_defaults() {
85 let cfg = HubConfig {
86 hub: HubSection { id: None },
87 generate: GenerateSection {
88 ignore: Some(vec!["draft-*.md".to_string()]),
89 },
90 };
91 assert!(!cfg.is_ignored("README.md")); assert!(cfg.is_ignored("draft-wip.md"));
93 }
94
95 #[test]
96 fn license_glob() {
97 let cfg = HubConfig::default();
98 assert!(!cfg.is_ignored("LICENSE-MIT"));
100 }
101}