systemprompt_loader/config_loader/
mod.rs1mod discovery;
15mod includes;
16mod merge;
17mod types;
18
19use std::collections::HashSet;
20use std::fs;
21use std::path::{Path, PathBuf};
22
23use systemprompt_config::ProfileBootstrap;
24use systemprompt_models::services::ServicesConfig;
25
26use crate::error::{ConfigLoadError, ConfigLoadResult};
27
28use discovery::{discover_marketplaces, discover_plugins, discover_skills};
29use includes::resolve_includes_recursively;
30use merge::{
31 resolve_skill_instruction_includes, resolve_system_prompt_includes,
32 warn_on_authored_card_skills,
33};
34use types::IncludeResolveCtx;
35
36#[derive(Debug)]
37pub struct ConfigLoader {
38 base_path: PathBuf,
39 config_path: PathBuf,
40}
41
42impl ConfigLoader {
43 #[must_use]
44 pub fn new(config_path: PathBuf) -> Self {
45 let base_path = config_path
46 .parent()
47 .unwrap_or_else(|| Path::new("."))
48 .to_path_buf();
49 Self {
50 base_path,
51 config_path,
52 }
53 }
54
55 pub fn for_active_profile() -> ConfigLoadResult<Self> {
56 let profile = ProfileBootstrap::get()?;
57 let config_path = PathBuf::from(profile.paths.config());
58 Ok(Self::new(config_path))
59 }
60
61 pub fn load() -> ConfigLoadResult<ServicesConfig> {
62 Self::for_active_profile()?.run()
63 }
64
65 pub fn load_from_path(path: &Path) -> ConfigLoadResult<ServicesConfig> {
66 Self::new(path.to_path_buf()).run()
67 }
68
69 #[cfg(any(test, feature = "expose-internals"))]
70 pub fn load_from_content(content: &str, path: &Path) -> ConfigLoadResult<ServicesConfig> {
71 Self::new(path.to_path_buf()).run_from_content(content)
72 }
73
74 pub fn validate_file(path: &Path) -> ConfigLoadResult<()> {
75 Self::load_from_path(path).map(|_| ())
76 }
77
78 fn run(&self) -> ConfigLoadResult<ServicesConfig> {
79 let content = fs::read_to_string(&self.config_path).map_err(|e| ConfigLoadError::Io {
80 path: self.config_path.clone(),
81 source: e,
82 })?;
83 self.run_from_content(&content)
84 }
85
86 fn run_from_content(&self, content: &str) -> ConfigLoadResult<ServicesConfig> {
87 let mut merged: ServicesConfig =
88 serde_yaml::from_str(content).map_err(|e| ConfigLoadError::Yaml {
89 path: self.config_path.clone(),
90 source: e,
91 })?;
92
93 let includes = std::mem::take(&mut merged.includes);
94
95 let mut visited: HashSet<PathBuf> = HashSet::new();
96 if let Ok(canonical_root) = fs::canonicalize(&self.config_path) {
97 visited.insert(canonical_root);
98 }
99 {
100 let mut ctx = IncludeResolveCtx {
101 visited: &mut visited,
102 merged: &mut merged,
103 chain: vec![self.config_path.clone()],
104 };
105 for include_path in &includes {
106 resolve_includes_recursively(
107 &self.base_path,
108 include_path,
109 &self.config_path,
110 &mut ctx,
111 )?;
112 }
113 }
114
115 resolve_system_prompt_includes(&self.base_path, &mut merged)?;
116 resolve_skill_instruction_includes(&self.base_path, &mut merged)?;
117 warn_on_authored_card_skills(&merged);
118
119 discover_skills(&self.base_path, &mut merged)?;
120 discover_plugins(&self.base_path, &mut merged)?;
121 discover_marketplaces(&self.base_path, &mut merged)?;
122
123 if let Ok(val) = std::env::var("SYSTEMPROMPT_SERVICES_PATH") {
124 merged.settings.services_path = Some(val);
125 }
126 if let Ok(val) = std::env::var("SYSTEMPROMPT_SKILLS_PATH") {
127 merged.settings.skills_path = Some(val);
128 }
129 if let Ok(val) = std::env::var("SYSTEMPROMPT_CONFIG_PATH") {
130 merged.settings.config_path = Some(val);
131 }
132
133 merged
134 .validate()
135 .map_err(|e| ConfigLoadError::Validation(e.to_string()))?;
136
137 Ok(merged)
138 }
139
140 pub fn get_includes(&self) -> ConfigLoadResult<Vec<String>> {
141 #[derive(serde::Deserialize)]
142 struct IncludesOnly {
143 #[serde(default)]
144 includes: Vec<String>,
145 }
146
147 let content = fs::read_to_string(&self.config_path).map_err(|e| ConfigLoadError::Io {
148 path: self.config_path.clone(),
149 source: e,
150 })?;
151 let parsed: IncludesOnly =
152 serde_yaml::from_str(&content).map_err(|e| ConfigLoadError::Yaml {
153 path: self.config_path.clone(),
154 source: e,
155 })?;
156 Ok(parsed.includes)
157 }
158
159 pub fn list_all_includes(&self) -> ConfigLoadResult<Vec<(String, bool)>> {
160 self.get_includes()?
161 .into_iter()
162 .map(|include| {
163 let exists = self.base_path.join(&include).exists();
164 Ok((include, exists))
165 })
166 .collect()
167 }
168
169 #[must_use]
170 pub fn base_path(&self) -> &Path {
171 &self.base_path
172 }
173}