systemprompt_loader/extension_loader/
mod.rs1mod manifest;
13mod result;
14
15use std::collections::HashMap;
16use std::fs;
17use std::path::Path;
18
19use systemprompt_models::DiscoveredExtension;
20
21use manifest::{load_manifest, mtime_of};
22
23pub use result::ExtensionValidationResult;
24
25const CARGO_TARGET: &str = "target";
26
27#[derive(Debug, Clone, Copy)]
28pub struct ExtensionLoader;
29
30impl ExtensionLoader {
31 #[must_use]
32 pub fn discover(project_root: &Path) -> Vec<DiscoveredExtension> {
33 let extensions_dir = project_root.join("extensions");
34
35 if !extensions_dir.exists() {
36 return vec![];
37 }
38
39 let mut discovered = vec![];
40
41 Self::scan_directory(&extensions_dir, &mut discovered);
42
43 if let Ok(entries) = fs::read_dir(&extensions_dir) {
44 for entry in entries.flatten() {
45 let path = entry.path();
46 if path.is_dir() {
47 Self::scan_directory(&path, &mut discovered);
48 }
49 }
50 }
51
52 discovered
53 }
54
55 fn scan_directory(dir: &Path, discovered: &mut Vec<DiscoveredExtension>) {
56 let Ok(entries) = fs::read_dir(dir) else {
57 return;
58 };
59
60 for entry in entries.flatten() {
61 let ext_dir = entry.path();
62 if !ext_dir.is_dir() {
63 continue;
64 }
65
66 let manifest_path = ext_dir.join("manifest.yaml");
67 if manifest_path.exists() {
68 match load_manifest(&manifest_path) {
69 Ok(manifest) => {
70 discovered.push(DiscoveredExtension::new(manifest, ext_dir, manifest_path));
71 },
72 Err(e) => {
73 tracing::warn!(
74 path = %manifest_path.display(),
75 error = %e,
76 "Failed to parse extension manifest, skipping"
77 );
78 },
79 }
80 }
81 }
82 }
83
84 #[must_use]
85 pub fn get_enabled_mcp_extensions(project_root: &Path) -> Vec<DiscoveredExtension> {
86 Self::discover(project_root)
87 .into_iter()
88 .filter(|e| e.is_mcp() && e.is_enabled())
89 .collect()
90 }
91
92 #[must_use]
93 pub fn get_enabled_cli_extensions(project_root: &Path) -> Vec<DiscoveredExtension> {
94 Self::discover(project_root)
95 .into_iter()
96 .filter(|e| e.is_cli() && e.is_enabled())
97 .collect()
98 }
99
100 #[must_use]
101 pub fn find_cli_extension(project_root: &Path, name: &str) -> Option<DiscoveredExtension> {
102 Self::get_enabled_cli_extensions(project_root)
103 .into_iter()
104 .find(|e| {
105 e.binary_name()
106 .is_some_and(|b| b == name || e.manifest.extension.name == name)
107 })
108 }
109
110 #[must_use]
111 pub fn get_cli_binary_path(
112 project_root: &Path,
113 binary_name: &str,
114 ) -> Option<std::path::PathBuf> {
115 let release_path = project_root
116 .join(CARGO_TARGET)
117 .join("release")
118 .join(binary_name);
119 if release_path.exists() {
120 return Some(release_path);
121 }
122
123 let debug_path = project_root
124 .join(CARGO_TARGET)
125 .join("debug")
126 .join(binary_name);
127 if debug_path.exists() {
128 return Some(debug_path);
129 }
130
131 None
132 }
133
134 #[must_use]
135 pub fn resolve_bin_directory(
136 project_root: &Path,
137 override_path: Option<&Path>,
138 ) -> std::path::PathBuf {
139 if let Some(path) = override_path {
140 return path.to_path_buf();
141 }
142
143 let release_dir = project_root.join(CARGO_TARGET).join("release");
144 let debug_dir = project_root.join(CARGO_TARGET).join("debug");
145
146 let release_binary = release_dir.join("systemprompt");
147 let debug_binary = debug_dir.join("systemprompt");
148
149 match (release_binary.exists(), debug_binary.exists()) {
150 (true, true) => {
151 let release_mtime = mtime_of(&release_binary);
152 let debug_mtime = mtime_of(&debug_binary);
153
154 match (release_mtime, debug_mtime) {
155 (Some(r), Some(d)) if d > r => debug_dir,
156 _ => release_dir,
157 }
158 },
159 (true | false, false) => release_dir,
160 (false, true) => debug_dir,
161 }
162 }
163
164 #[must_use]
165 pub fn validate_mcp_binaries(project_root: &Path) -> Vec<(String, std::path::PathBuf)> {
166 let extensions = Self::get_enabled_mcp_extensions(project_root);
167 let target_dir = Self::resolve_bin_directory(project_root, None);
168
169 extensions
170 .into_iter()
171 .filter_map(|ext| {
172 ext.binary_name().and_then(|binary| {
173 let binary_path = target_dir.join(binary);
174 if binary_path.exists() {
175 None
176 } else {
177 Some((binary.to_owned(), ext.path.clone()))
178 }
179 })
180 })
181 .collect()
182 }
183
184 #[must_use]
185 pub fn get_mcp_binary_names(project_root: &Path) -> Vec<String> {
186 Self::get_enabled_mcp_extensions(project_root)
187 .iter()
188 .filter_map(|e| e.binary_name().map(String::from))
189 .collect()
190 }
191
192 #[must_use]
193 pub fn get_production_mcp_binary_names(
194 project_root: &Path,
195 services_config: &systemprompt_models::ServicesConfig,
196 ) -> Vec<String> {
197 Self::get_enabled_mcp_extensions(project_root)
198 .iter()
199 .filter_map(|e| {
200 let binary = e.binary_name()?;
201 let is_dev_only = services_config
202 .mcp_servers
203 .values()
204 .find(|d| d.binary == binary)
205 .is_some_and(|d| d.dev_only);
206 (!is_dev_only).then(|| binary.to_owned())
207 })
208 .collect()
209 }
210
211 #[must_use]
212 pub fn build_binary_map(project_root: &Path) -> HashMap<String, DiscoveredExtension> {
213 Self::discover(project_root)
214 .into_iter()
215 .filter_map(|ext| {
216 let name = ext.binary_name()?.to_owned();
217 Some((name, ext))
218 })
219 .collect()
220 }
221
222 #[must_use]
223 pub fn validate(project_root: &Path) -> ExtensionValidationResult {
224 ExtensionValidationResult {
225 discovered: Self::discover(project_root),
226 missing_binaries: Self::validate_mcp_binaries(project_root),
227 missing_manifests: vec![],
228 }
229 }
230}