bake_agent_context/
lib.rs1mod document;
6mod exclude;
7mod installer;
8mod ownership;
9mod package;
10mod skill;
11#[cfg(test)]
12mod test_filesystem;
13
14pub use installer::{Definition, FileDefinition, Installation, Installer, Specification};
15pub use package::Package;
16pub use skill::SkillDefinition;
17
18use bake::{Context, Error, Result};
19
20fn installer(context: &mut Context) -> Result<Installer> {
21 if let Some(installer) = context.get::<Installer>() {
22 return Ok(installer.clone());
23 }
24
25 let installer = Installer::discover(context.root())?;
26 context.insert(installer.clone());
27 Ok(installer)
28}
29
30#[bake::task(name = "agent:context:list")]
32pub fn list(context: &mut Context, package: Option<String>) -> Result<String> {
33 let installer = installer(context)?;
34
35 if let Some(selector) = package {
36 let Some(provider) = installer.find_provider(&selector)? else {
37 return Ok(format!("No context found for crate '{selector}'"));
38 };
39 if provider.definitions.is_empty() {
40 return Ok(format!(
41 "No context definitions found for crate '{selector}'."
42 ));
43 }
44 let mut output = format!("Context files for crate '{selector}':");
45 for definition in provider.definitions {
46 output.push_str(&format!("\n {}", definition.display_name()));
47 }
48 return Ok(output);
49 }
50
51 let providers = installer.providers()?;
52 let providers: Vec<_> = providers
53 .iter()
54 .filter(|provider| !provider.definitions.is_empty())
55 .collect();
56 if providers.is_empty() {
57 return Ok("No dependency context files found".to_owned());
58 }
59
60 let mut output = String::from("Crates with context available:");
61 for provider in providers {
62 output.push_str(&format!(
63 "\n {} ({})",
64 provider.specification.selector(),
65 provider.specification.version
66 ));
67 }
68 Ok(output)
69}
70
71#[bake::task(name = "agent:context:show")]
73pub fn show(
74 context: &mut Context,
75 #[bake(named)] package: String,
76 #[bake(named)] file: String,
77) -> Result<String> {
78 let installer = installer(context)?;
79 let Some(content) = installer.show_context_file(&package, &file)? else {
80 return Err(Error::new(format!(
81 "context file {file:?} was not found in crate {package:?}"
82 )));
83 };
84 Ok(content)
85}
86
87#[bake::task(name = "agent:context:install")]
89pub fn install(context: &mut Context, package: Option<String>) -> Result<String> {
90 let installer = installer(context)?;
91 let installed = installer.install(package.as_deref())?;
92 let installed_context = installed.context;
93 let installed_skills = installed.skills;
94
95 let mut output = Vec::new();
96 if !installed_context.is_empty() {
97 output.push(format!(
98 "Installed context from: {}",
99 installed_context.join(", ")
100 ));
101 }
102 if !installed_skills.is_empty() {
103 output.push(format!("Installed skills: {}", installed_skills.join(", ")));
104 }
105
106 if output.is_empty() {
107 Ok("No dependency context or skills were installed".to_owned())
108 } else {
109 Ok(output.join("\n"))
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use std::fs;
117 use std::path::Path;
118 use tempfile::tempdir;
119
120 fn make_context(root: &Path, specifications: Vec<Specification>) -> Context {
121 let mut context = bake::Registry::new().context(root);
122 context.insert(Installer::for_test(root, specifications));
123 context
124 }
125
126 fn specification(root: &Path, name: &str, version: &str) -> Specification {
127 Specification::for_test(name, version, root.join(name).join("context"))
128 }
129
130 fn write(path: &Path, contents: &str) {
131 fs::create_dir_all(path.parent().unwrap()).unwrap();
132 fs::write(path, contents).unwrap();
133 }
134
135 #[test]
136 fn task_wrappers_return_outputs_and_propagate_errors() {
137 let directory = tempdir().unwrap();
138 let root = directory.path();
139 let guide = specification(root, "provider", "1.0.0");
140 write(&guide.context_path.join("guide.md"), "# Guide\n");
141 write(
142 &guide.context_path.join("broken.md"),
143 "---\nmetadata: [\n---\n# Invalid YAML\n",
144 );
145 let mut context = make_context(root, vec![guide.clone()]);
146
147 assert!(
148 list(&mut context, Some("unknown".to_owned()))
149 .unwrap()
150 .contains("No context found")
151 );
152 assert!(list(&mut context, None).is_err());
153 assert!(list(&mut context, Some("provider".to_owned())).is_err());
154 assert!(install(&mut context, None).is_err());
155 assert!(show(&mut context, "provider".to_owned(), "missing.md".to_owned()).is_err());
156 assert!(install(&mut context, Some("provider".to_owned())).is_err());
157
158 let mut duplicates = make_context(
159 root,
160 vec![
161 specification(root, "duplicate", "1.0.0"),
162 specification(root, "duplicate", "2.0.0"),
163 ],
164 );
165 assert!(list(&mut duplicates, Some("duplicate".to_owned())).is_err());
166 assert!(
167 show(
168 &mut duplicates,
169 "duplicate".to_owned(),
170 "guide.md".to_owned()
171 )
172 .is_err()
173 );
174 assert!(
175 duplicates
176 .get::<Installer>()
177 .unwrap()
178 .install(Some("duplicate"))
179 .is_err()
180 );
181
182 let listing_package = specification(root, "listing", "1.0.0");
183 write(
184 &listing_package.context_path.join("guide.md"),
185 "# Listing\n",
186 );
187 let mut listing_context = make_context(root, vec![listing_package]);
188 assert!(
189 list(&mut listing_context, None)
190 .unwrap()
191 .contains("listing@1.0.0 (1.0.0)")
192 );
193
194 let empty_package = specification(root, "empty", "1.0.0");
195 fs::create_dir_all(&empty_package.context_path).unwrap();
196 let mut empty_context = make_context(root, vec![empty_package]);
197 assert_eq!(
198 list(&mut empty_context, Some("empty".to_owned())).unwrap(),
199 "No context definitions found for crate 'empty'."
200 );
201 assert!(
202 list(&mut empty_context, None)
203 .unwrap()
204 .contains("No dependency context files")
205 );
206 }
207
208 #[test]
209 fn task_wrappers_propagate_install_failures() {
210 let directory = tempdir().unwrap();
211 let root = directory.path();
212 let specification = specification(root, "provider", "1.0.0");
213 write(&specification.context_path.join("guide.md"), "# Guide\n");
214
215 let ownership = root.join(".agents/skills/provider-workflow/SKILL.md");
216 write(
217 &ownership,
218 "---\nmetadata:\n type: skill\n ecosystem: cargo\n package: provider\n version: 1.0.0\n---\n",
219 );
220 let failure_path = ownership.clone();
221 let _failure = test_filesystem::fail_once(test_filesystem::Operation::Read, move |path| {
222 path == failure_path
223 });
224 let mut context = make_context(root, vec![specification.clone()]);
225 assert!(install(&mut context, Some("provider".to_owned())).is_err());
226 drop(_failure);
227
228 let mut empty = make_context(root, Vec::new());
229 assert!(
230 list(&mut empty, None)
231 .unwrap()
232 .contains("No dependency context files")
233 );
234 }
235
236 #[test]
237 fn installer_creation_errors_are_returned_by_tasks() {
238 let directory = tempdir().unwrap();
239 let mut context = bake::Registry::new().context(directory.path());
240 assert!(list(&mut context, None).is_err());
241 assert!(show(&mut context, "provider".to_owned(), "guide.md".to_owned()).is_err());
242 assert!(install(&mut context, None).is_err());
243 }
244}