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