use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::ai_hooks::ConfigOrigin;
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct SkillsConfig {
#[serde(default = "default_true")]
pub auto_install: bool,
#[serde(default)]
pub agents: Vec<String>,
#[serde(default)]
pub install: HashMap<String, SkillEntry>,
#[serde(default)]
pub library_sources: Vec<crate::library_registry::LibrarySource>,
#[serde(skip)]
pub origin: ConfigOrigin,
}
impl SkillsConfig {
#[allow(dead_code)] pub fn is_empty(&self) -> bool {
self.install.is_empty()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum SkillEntry {
Version(String),
Detailed {
version: String,
#[serde(default)]
agents: Vec<String>,
},
}
impl SkillEntry {
pub fn version(&self) -> &str {
match self {
SkillEntry::Version(v) => v,
SkillEntry::Detailed { version, .. } => version,
}
}
pub fn agents(&self) -> &[String] {
match self {
SkillEntry::Version(_) => &[],
SkillEntry::Detailed { agents, .. } => agents,
}
}
}
fn default_true() -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_via_serde() {
let cfg: SkillsConfig = toml::from_str("").unwrap();
assert!(cfg.auto_install);
assert!(cfg.agents.is_empty());
assert!(cfg.install.is_empty());
assert!(cfg.library_sources.is_empty());
}
#[test]
fn parses_bare_version() {
let toml_str = r#"
[install]
myorg-code-review = "2.1.0"
"#;
let cfg: SkillsConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.install.len(), 1);
match &cfg.install["myorg-code-review"] {
SkillEntry::Version(v) => assert_eq!(v, "2.1.0"),
other => panic!("expected Version, got {other:?}"),
}
}
#[test]
fn parses_detailed_with_agents() {
let toml_str = r#"
[install]
myorg-code-review = { version = "2.1.0", agents = ["claude-code"] }
"#;
let cfg: SkillsConfig = toml::from_str(toml_str).unwrap();
match &cfg.install["myorg-code-review"] {
SkillEntry::Detailed { version, agents } => {
assert_eq!(version, "2.1.0");
assert_eq!(agents, &["claude-code"]);
}
other => panic!("expected Detailed, got {other:?}"),
}
}
#[test]
fn parses_library_sources() {
let toml_str = r#"
[[library_sources]]
url = "https://cdn.example.com/manifest.json"
"#;
let cfg: SkillsConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.library_sources.len(), 1);
}
#[test]
fn rejects_unknown_top_level_field() {
let toml_str = "garbage = true";
let err = toml::from_str::<SkillsConfig>(toml_str).unwrap_err();
assert!(format!("{err}").contains("garbage"));
}
}