systemprompt_loader/config_loader/
mod.rs1mod discovery;
12mod includes;
13mod merge;
14mod types;
15
16use std::collections::HashSet;
17use std::fs;
18use std::path::{Path, PathBuf};
19
20use systemprompt_config::ProfileBootstrap;
21use systemprompt_models::services::ServicesConfig;
22
23use crate::error::{ConfigLoadError, ConfigLoadResult};
24
25use discovery::{discover_marketplaces, discover_plugins, discover_skills};
26use includes::resolve_includes_recursively;
27use merge::{
28 resolve_skill_instruction_includes, resolve_system_prompt_includes,
29 warn_on_authored_card_skills,
30};
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 warn_on_authored_card_skills(&merged);
115
116 discover_skills(&self.base_path, &mut merged)?;
117 discover_plugins(&self.base_path, &mut merged)?;
118 discover_marketplaces(&self.base_path, &mut merged)?;
119
120 if let Ok(val) = std::env::var("SYSTEMPROMPT_SERVICES_PATH") {
121 merged.settings.services_path = Some(val);
122 }
123 if let Ok(val) = std::env::var("SYSTEMPROMPT_SKILLS_PATH") {
124 merged.settings.skills_path = Some(val);
125 }
126 if let Ok(val) = std::env::var("SYSTEMPROMPT_CONFIG_PATH") {
127 merged.settings.config_path = Some(val);
128 }
129
130 merged
131 .validate()
132 .map_err(|e| ConfigLoadError::Validation(e.to_string()))?;
133
134 Ok(merged)
135 }
136
137 pub fn get_includes(&self) -> ConfigLoadResult<Vec<String>> {
138 #[derive(serde::Deserialize)]
139 struct IncludesOnly {
140 #[serde(default)]
141 includes: Vec<String>,
142 }
143
144 let content = fs::read_to_string(&self.config_path).map_err(|e| ConfigLoadError::Io {
145 path: self.config_path.clone(),
146 source: e,
147 })?;
148 let parsed: IncludesOnly =
149 serde_yaml::from_str(&content).map_err(|e| ConfigLoadError::Yaml {
150 path: self.config_path.clone(),
151 source: e,
152 })?;
153 Ok(parsed.includes)
154 }
155
156 pub fn list_all_includes(&self) -> ConfigLoadResult<Vec<(String, bool)>> {
157 self.get_includes()?
158 .into_iter()
159 .map(|include| {
160 let exists = self.base_path.join(&include).exists();
161 Ok((include, exists))
162 })
163 .collect()
164 }
165
166 #[must_use]
167 pub fn base_path(&self) -> &Path {
168 &self.base_path
169 }
170}