1mod exclude;
6mod index;
7mod installer;
8mod skill;
9#[cfg(test)]
10mod test_filesystem;
11
12pub use index::ContextIndex;
13pub use installer::{ContextFile, ContextPackage, Installer};
14pub use skill::{Skill, install_skills, list_skills};
15
16use bake::{Context, Error, Result};
17
18fn installer(context: &mut Context) -> Result<Installer> {
19 if let Some(installer) = context.get::<Installer>() {
20 return Ok(installer.clone());
21 }
22
23 let installer = Installer::new(context.root())?;
24 context.insert(installer.clone());
25 Ok(installer)
26}
27
28#[bake::task(name = "agent:context:list")]
30pub fn list(context: &mut Context, package: Option<String>) -> Result<String> {
31 let installer = installer(context)?;
32
33 if let Some(package) = package {
34 let Some(package) = installer.find_package(&package)? else {
35 return Ok(format!("No context found for crate '{package}'"));
36 };
37
38 let files = installer.list_context_files(&package)?;
39 if files.is_empty() {
40 return Ok(format!(
41 "No context files found for crate '{}'.",
42 package.selector()
43 ));
44 }
45 let mut output = format!("Context files for crate '{}':", package.selector());
46 for file in files {
47 output.push_str(&format!("\n {}", file.path.display()));
48 }
49 return Ok(output);
50 }
51
52 let mut packages = Vec::new();
53 for package in installer.packages() {
54 if !installer.list_context_files(package)?.is_empty() {
55 packages.push(package);
56 }
57 }
58 if packages.is_empty() {
59 return Ok("No dependency context files found".to_owned());
60 }
61
62 let mut output = String::from("Crates with context available:");
63 for package in packages {
64 output.push_str(&format!("\n {} ({})", package.selector(), package.version));
65 }
66 Ok(output)
67}
68
69#[bake::task(name = "agent:context:show")]
71pub fn show(
72 context: &mut Context,
73 #[bake(named)] package: String,
74 #[bake(named)] file: String,
75) -> Result<String> {
76 let installer = installer(context)?;
77 let Some(content) = installer.show_context_file(&package, &file)? else {
78 return Err(Error::new(format!(
79 "context file {file:?} was not found in crate {package:?}"
80 )));
81 };
82 Ok(content)
83}
84
85#[bake::task(name = "agent:context:install")]
87pub fn install(context: &mut Context, package: Option<String>) -> Result<String> {
88 let installer = installer(context)?;
89 let installed_context = if let Some(package) = package.as_deref() {
90 if installer.install_package(package)? {
91 vec![package.to_owned()]
92 } else {
93 Vec::new()
94 }
95 } else {
96 installer.install_all()?
97 };
98 let installed_skills = install_skills(&installer, package.as_deref(), None)?;
99
100 ContextIndex::new(context.root())
101 .with_packages(installer.packages())
102 .update_index()?;
103
104 let mut output = Vec::new();
105 if !installed_context.is_empty() {
106 output.push(format!(
107 "Installed context from: {}",
108 installed_context.join(", ")
109 ));
110 }
111 if !installed_skills.is_empty() {
112 output.push(format!("Installed skills: {}", installed_skills.join(", ")));
113 }
114
115 if output.is_empty() {
116 Ok("No dependency context or skills were installed".to_owned())
117 } else {
118 Ok(output.join("\n"))
119 }
120}
121
122#[bake::task(name = "agent:context:skill:list")]
124pub fn skill_list(context: &mut Context, package: Option<String>) -> Result<String> {
125 let installer = installer(context)?;
126 let skills = list_skills(&installer, package.as_deref())?;
127 if skills.is_empty() {
128 return Ok("No dependency skills found".to_owned());
129 }
130
131 let mut output = String::from("Available dependency skills:");
132 for skill in skills {
133 output.push_str(&format!(
134 "\n {} ({}) — {}",
135 skill.name,
136 skill.package_selector(),
137 skill.description
138 ));
139 }
140 Ok(output)
141}
142
143#[bake::task(name = "agent:context:skill:install")]
145pub fn skill_install(
146 context: &mut Context,
147 package: Option<String>,
148 skill: Option<String>,
149) -> Result<String> {
150 let installer = installer(context)?;
151 let installed = install_skills(&installer, package.as_deref(), skill.as_deref())?;
152 if installed.is_empty() {
153 Ok("No dependency skills were installed".to_owned())
154 } else {
155 Ok(format!("Installed skills: {}", installed.join(", ")))
156 }
157}
158
159#[bake::task(name = "agent:context:index")]
161pub fn index(context: &mut Context) -> Result<String> {
162 let installer = installer(context)?;
163 ContextIndex::new(context.root())
164 .with_packages(installer.packages())
165 .update_index()?;
166 Ok("Updated .agents/context/index.md".to_owned())
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use std::fs;
173 use std::path::Path;
174 use tempfile::tempdir;
175
176 fn make_context(root: &Path, packages: Vec<ContextPackage>) -> Context {
177 let mut context = bake::Registry::new().context(root);
178 context.insert(Installer::for_test(root, packages));
179 context
180 }
181
182 fn package(root: &Path, name: &str, version: &str) -> ContextPackage {
183 ContextPackage::for_test(name, version, root.join(name).join("context"))
184 }
185
186 fn write(path: &Path, contents: &str) {
187 fs::create_dir_all(path.parent().unwrap()).unwrap();
188 fs::write(path, contents).unwrap();
189 }
190
191 #[test]
192 fn task_wrappers_return_outputs_and_propagate_errors() {
193 let directory = tempdir().unwrap();
194 let root = directory.path();
195 let guide = package(root, "provider", "1.0.0");
196 write(&guide.context_path.join("guide.md"), "# Guide\n");
197 write(
198 &guide.context_path.join("broken.md"),
199 "---\ntype: guide\n---\n# Unsupported\n",
200 );
201 let mut context = make_context(root, vec![guide.clone()]);
202
203 assert!(
204 list(&mut context, Some("unknown".to_owned()))
205 .unwrap()
206 .contains("No context found")
207 );
208 assert!(list(&mut context, None).is_err());
209 assert!(list(&mut context, Some("provider".to_owned())).is_err());
210 assert!(install(&mut context, None).is_err());
211 assert!(show(&mut context, "provider".to_owned(), "missing.md".to_owned()).is_err());
212 assert!(install(&mut context, Some("provider".to_owned())).is_err());
213 assert!(skill_list(&mut context, Some("provider".to_owned())).is_err());
214 assert!(skill_install(&mut context, Some("provider".to_owned()), None).is_err());
215
216 let mut duplicates = make_context(
217 root,
218 vec![
219 package(root, "duplicate", "1.0.0"),
220 package(root, "duplicate", "2.0.0"),
221 ],
222 );
223 assert!(list(&mut duplicates, Some("duplicate".to_owned())).is_err());
224 assert!(
225 show(
226 &mut duplicates,
227 "duplicate".to_owned(),
228 "guide.md".to_owned()
229 )
230 .is_err()
231 );
232 assert!(
233 duplicates
234 .get::<Installer>()
235 .unwrap()
236 .install_package("duplicate")
237 .is_err()
238 );
239
240 let listing_package = package(root, "listing", "1.0.0");
241 write(
242 &listing_package.context_path.join("guide.md"),
243 "# Listing\n",
244 );
245 let mut listing_context = make_context(root, vec![listing_package]);
246 assert!(
247 list(&mut listing_context, None)
248 .unwrap()
249 .contains("listing@1.0.0 (1.0.0)")
250 );
251
252 let empty_package = package(root, "empty", "1.0.0");
253 fs::create_dir_all(&empty_package.context_path).unwrap();
254 let mut empty_context = make_context(root, vec![empty_package]);
255 assert!(
256 list(&mut empty_context, None)
257 .unwrap()
258 .contains("No dependency context files")
259 );
260 }
261
262 #[test]
263 fn task_wrappers_propagate_install_and_index_failures() {
264 let directory = tempdir().unwrap();
265 let root = directory.path();
266 let provider = package(root, "provider", "1.0.0");
267 write(&provider.context_path.join("guide.md"), "# Guide\n");
268
269 let registry = root.join(".agents/skills/.agent-context-skills.json");
270 write(®istry, r#"{"version":1,"skills":{}}"#);
271 let failure_path = registry.clone();
272 let _failure = test_filesystem::fail_once(test_filesystem::Operation::Read, move |path| {
273 path == failure_path
274 });
275 let mut context = make_context(root, vec![provider.clone()]);
276 assert!(install(&mut context, Some("provider".to_owned())).is_err());
277 drop(_failure);
278
279 let index_file = root.join(".agents/context/index.md");
280 fs::create_dir_all(&index_file).unwrap();
281 let mut context = make_context(root, Vec::new());
282 assert!(install(&mut context, None).is_err());
283 assert!(index(&mut context).is_err());
284
285 let mut empty = make_context(root, Vec::new());
286 assert!(
287 list(&mut empty, None)
288 .unwrap()
289 .contains("No dependency context files")
290 );
291 assert_eq!(
292 skill_list(&mut empty, None).unwrap(),
293 "No dependency skills found"
294 );
295 assert_eq!(
296 skill_install(&mut empty, None, None).unwrap(),
297 "No dependency skills were installed"
298 );
299 }
300
301 #[test]
302 fn installer_creation_errors_are_returned_by_tasks() {
303 let directory = tempdir().unwrap();
304 let mut context = bake::Registry::new().context(directory.path());
305 assert!(list(&mut context, None).is_err());
306 assert!(show(&mut context, "provider".to_owned(), "guide.md".to_owned()).is_err());
307 assert!(install(&mut context, None).is_err());
308 assert!(skill_list(&mut context, None).is_err());
309 assert!(skill_install(&mut context, None, None).is_err());
310 assert!(index(&mut context).is_err());
311 }
312}