1use crate::*;
10
11const VERSION: &str = env!("CARGO_PKG_VERSION");
13
14const SKILL_MD: &str = include_str!("../templates/skills/oneiros/SKILL.md");
15const PLUGIN_JSON: &str = include_str!("../templates/skills/oneiros/plugin.json");
16const HOOKS_JSON: &str = include_str!("../templates/skills/oneiros/hooks.json");
17const MARKETPLACE_JSON: &str = include_str!("../templates/skills/oneiros/marketplace.json");
18const AGENTS_MD: &str = include_str!("../templates/skills/oneiros/agents-md.md");
19const MORNING_PAGES_MD: &str = include_str!("../templates/skills/oneiros-morning-pages/SKILL.md");
20const EVENING_PAGES_MD: &str = include_str!("../templates/skills/oneiros-evening-pages/SKILL.md");
21
22pub struct SkillAsset {
24 pub path: &'static str,
26 pub content: String,
28}
29
30pub(crate) struct SkillInventory;
32
33impl SkillInventory {
34 pub(crate) fn all() -> Vec<Skill> {
36 let mut skills = Vec::new();
37
38 skills.extend(ActorSkills::all());
39 skills.extend(AgentSkills::all());
40 skills.extend(BookmarkSkills::all());
41 skills.extend(BrainSkills::all());
42 skills.extend(CognitionSkills::all());
43 skills.extend(ConnectionSkills::all());
44 skills.extend(ContinuitySkills::all());
45 skills.extend(DoctorSkills::all());
46 skills.extend(ExperienceSkills::all());
47 skills.extend(FollowSkills::all());
48 skills.extend(LevelSkills::all());
49 skills.extend(McpConfigSkills::all());
50 skills.extend(MemorySkills::all());
51 skills.extend(NatureSkills::all());
52 skills.extend(PeerSkills::all());
53 skills.extend(PersonaSkills::all());
54 skills.extend(PressureSkills::all());
55 skills.extend(ProjectSkills::all());
56 skills.extend(SearchSkills::all());
57 skills.extend(SeedSkills::all());
58 skills.extend(SensationSkills::all());
59 skills.extend(ServiceSkills::all());
60 skills.extend(SetupSkills::all());
61 skills.extend(StorageSkills::all());
62 skills.extend(SystemSkills::all());
63 skills.extend(TenantSkills::all());
64 skills.extend(TextureSkills::all());
65 skills.extend(TicketSkills::all());
66 skills.extend(UrgeSkills::all());
67
68 skills
69 }
70}
71
72pub struct SkillPackage;
74
75impl SkillPackage {
76 pub fn assets() -> Vec<SkillAsset> {
80 let mut assets = vec![
81 SkillAsset {
82 path: "skills/oneiros/SKILL.md",
83 content: Self::stamp(SKILL_MD),
84 },
85 SkillAsset {
86 path: ".claude-plugin/plugin.json",
87 content: Self::stamp(PLUGIN_JSON),
88 },
89 SkillAsset {
90 path: ".claude-plugin/marketplace.json",
91 content: Self::stamp(MARKETPLACE_JSON),
92 },
93 SkillAsset {
94 path: "hooks/hooks.json",
95 content: HOOKS_JSON.to_string(),
96 },
97 SkillAsset {
98 path: "agents-md.md",
99 content: AGENTS_MD.to_string(),
100 },
101 SkillAsset {
102 path: "skills/oneiros-morning-pages/SKILL.md",
103 content: Self::stamp(MORNING_PAGES_MD),
104 },
105 SkillAsset {
106 path: "skills/oneiros-evening-pages/SKILL.md",
107 content: Self::stamp(EVENING_PAGES_MD),
108 },
109 ];
110
111 for (name, content) in Self::agents() {
113 assets.push(SkillAsset {
114 path: name,
115 content: content.to_string(),
116 });
117 }
118
119 for (name, content) in Self::resources() {
121 assets.push(SkillAsset {
122 path: name,
123 content: content.to_string(),
124 });
125 }
126
127 for skill in SkillInventory::all() {
129 assets.push(SkillAsset {
130 path: leak_path(&format!("commands/{}.md", skill.name)),
131 content: skill.content.to_string(),
132 });
133 }
134
135 assets
136 }
137
138 fn agents() -> Vec<(&'static str, &'static str)> {
140 vec![
141 (
142 "agents/activity.scribe.md",
143 include_str!("../templates/skills/oneiros/agents/activity.scribe.md"),
144 ),
145 (
146 "agents/oneiroi.process.md",
147 include_str!("../templates/skills/oneiros/agents/oneiroi.process.md"),
148 ),
149 ]
150 }
151
152 fn resources() -> Vec<(&'static str, &'static str)> {
154 vec![
155 (
156 "skills/oneiros/resources/cognitive-model.md",
157 include_str!("../templates/skills/oneiros/resources/cognitive-model.md"),
158 ),
159 (
160 "skills/oneiros/resources/getting-started.md",
161 include_str!("../templates/skills/oneiros/resources/getting-started.md"),
162 ),
163 ]
164 }
165
166 fn stamp(content: &str) -> String {
168 content.replace("{{VERSION}}", VERSION)
169 }
170}
171
172fn leak_path(s: &str) -> &'static str {
177 Box::leak(s.to_string().into_boxed_str())
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn inventory_is_not_empty() {
186 let skills = SkillInventory::all();
187 assert!(!skills.is_empty(), "skill inventory should not be empty");
188 }
189
190 #[test]
191 fn all_skills_have_content() {
192 for skill in SkillInventory::all() {
193 assert!(
194 !skill.content.trim().is_empty(),
195 "skill '{}' has empty content",
196 skill.name
197 );
198 }
199 }
200
201 #[test]
202 fn all_skill_names_are_unique() {
203 let skills = SkillInventory::all();
204 let mut names: Vec<&str> = skills.iter().map(|s| s.name).collect();
205 names.sort();
206 names.dedup();
207 assert_eq!(names.len(), skills.len(), "duplicate skill names found");
208 }
209
210 #[test]
211 fn level_skills_are_present() {
212 let skills = SkillInventory::all();
213 let level_skills: Vec<_> = skills
214 .iter()
215 .filter(|s| s.name.starts_with("level-"))
216 .collect();
217 assert_eq!(
218 level_skills.len(),
219 4,
220 "expected 4 level skills (set, show, list, remove), got {}",
221 level_skills.len()
222 );
223 }
224
225 #[test]
226 fn continuity_skills_are_present() {
227 let skills = SkillInventory::all();
228 let continuity_skills: Vec<_> = skills
229 .iter()
230 .filter(|s| {
231 matches!(
232 s.name,
233 "wake"
234 | "dream"
235 | "introspect"
236 | "reflect"
237 | "sense"
238 | "sleep"
239 | "guidebook"
240 | "emerge"
241 | "recede"
242 | "status"
243 )
244 })
245 .collect();
246 assert_eq!(
247 continuity_skills.len(),
248 10,
249 "expected 10 continuity skills, got {}",
250 continuity_skills.len()
251 );
252 }
253
254 #[test]
255 fn vocabulary_domains_are_complete() {
256 let skills = SkillInventory::all();
257 for domain in &["texture", "sensation", "nature", "persona", "urge"] {
258 let domain_skills: Vec<_> = skills
259 .iter()
260 .filter(|s| s.name.starts_with(domain))
261 .collect();
262 assert_eq!(
263 domain_skills.len(),
264 4,
265 "expected 4 skills for domain '{domain}', got {}",
266 domain_skills.len()
267 );
268 }
269 }
270
271 #[test]
272 fn package_assets_include_skill_md() {
273 let assets = SkillPackage::assets();
274 let skill_md = assets.iter().find(|a| a.path == "skills/oneiros/SKILL.md");
275 assert!(skill_md.is_some(), "package should include SKILL.md");
276 assert!(
277 !skill_md.unwrap().content.contains("{{VERSION}}"),
278 "SKILL.md should have version stamped"
279 );
280 }
281
282 #[test]
283 fn package_assets_include_commands() {
284 let assets = SkillPackage::assets();
285 let commands: Vec<_> = assets
286 .iter()
287 .filter(|a| a.path.starts_with("commands/"))
288 .collect();
289 assert!(
290 !commands.is_empty(),
291 "package should include command documentation"
292 );
293 assert_eq!(
295 commands.len(),
296 SkillInventory::all().len(),
297 "command count should match skill inventory"
298 );
299 }
300
301 #[test]
302 fn package_version_is_stamped() {
303 let assets = SkillPackage::assets();
304 let plugin = assets
305 .iter()
306 .find(|a| a.path == ".claude-plugin/plugin.json")
307 .expect("package should include plugin.json");
308 assert!(
309 plugin.content.contains(VERSION),
310 "plugin.json should contain the current version"
311 );
312 }
313}