@@ -183,3 +183,248 @@ test("returns empty array when no skills exist", async () => {
183183 } ,
184184 } )
185185} )
186+
187+ test ( "discovers skills from custom paths in config" , async ( ) => {
188+ await using tmp = await tmpdir ( {
189+ git : true ,
190+ init : async ( dir ) => {
191+ const customDir = path . join ( dir , "my-custom-skills" , "skill" , "custom-skill" )
192+ await Bun . write (
193+ path . join ( customDir , "SKILL.md" ) ,
194+ `---
195+ name: custom-skill
196+ description: A skill from a custom directory.
197+ ---
198+
199+ # Custom Skill
200+ ` ,
201+ )
202+ await Bun . write (
203+ path . join ( dir , "opencode.json" ) ,
204+ JSON . stringify ( {
205+ $schema : "https://opencode.ai/config.json" ,
206+ skill : {
207+ paths : [ "my-custom-skills" ] ,
208+ } ,
209+ } ) ,
210+ )
211+ } ,
212+ } )
213+
214+ await Instance . provide ( {
215+ directory : tmp . path ,
216+ fn : async ( ) => {
217+ const skills = await Skill . all ( )
218+ expect ( skills . length ) . toBe ( 1 )
219+ const skill = skills . find ( ( s ) => s . name === "custom-skill" )
220+ expect ( skill ) . toBeDefined ( )
221+ expect ( skill ! . description ) . toBe ( "A skill from a custom directory." )
222+ } ,
223+ } )
224+ } )
225+
226+ test ( "resolves relative custom skill paths from project directory" , async ( ) => {
227+ await using tmp = await tmpdir ( {
228+ git : true ,
229+ init : async ( dir ) => {
230+ const customDir = path . join ( dir , "custom-skills" , "skills" , "relative-skill" )
231+ await Bun . write (
232+ path . join ( customDir , "SKILL.md" ) ,
233+ `---
234+ name: relative-skill
235+ description: A skill from a relative path.
236+ ---
237+
238+ # Relative Skill
239+ ` ,
240+ )
241+ await Bun . write (
242+ path . join ( dir , "opencode.json" ) ,
243+ JSON . stringify ( {
244+ $schema : "https://opencode.ai/config.json" ,
245+ skill : {
246+ paths : [ "./custom-skills" ] ,
247+ } ,
248+ } ) ,
249+ )
250+ } ,
251+ } )
252+
253+ await Instance . provide ( {
254+ directory : tmp . path ,
255+ fn : async ( ) => {
256+ const skills = await Skill . all ( )
257+ expect ( skills . length ) . toBe ( 1 )
258+ const skill = skills . find ( ( s ) => s . name === "relative-skill" )
259+ expect ( skill ) . toBeDefined ( )
260+ expect ( skill ! . location ) . toContain ( "custom-skills/skills/relative-skill/SKILL.md" )
261+ } ,
262+ } )
263+ } )
264+
265+ test ( "expands ~ in custom skill paths" , async ( ) => {
266+ await using tmp = await tmpdir ( { git : true } )
267+
268+ const original = process . env . OPENCODE_TEST_HOME
269+ process . env . OPENCODE_TEST_HOME = tmp . path
270+
271+ try {
272+ const homeSkillDir = path . join ( tmp . path , "test-skills" , "skill" , "home-skill" )
273+ await fs . mkdir ( homeSkillDir , { recursive : true } )
274+ await Bun . write (
275+ path . join ( homeSkillDir , "SKILL.md" ) ,
276+ `---
277+ name: home-skill
278+ description: A skill from home directory expansion.
279+ ---
280+
281+ # Home Skill
282+ ` ,
283+ )
284+ await Bun . write (
285+ path . join ( tmp . path , "opencode.json" ) ,
286+ JSON . stringify ( {
287+ $schema : "https://opencode.ai/config.json" ,
288+ skill : {
289+ paths : [ "~/test-skills" ] ,
290+ } ,
291+ } ) ,
292+ )
293+
294+ await Instance . provide ( {
295+ directory : tmp . path ,
296+ fn : async ( ) => {
297+ const skills = await Skill . all ( )
298+ expect ( skills . length ) . toBe ( 1 )
299+ const skill = skills . find ( ( s ) => s . name === "home-skill" )
300+ expect ( skill ) . toBeDefined ( )
301+ expect ( skill ! . description ) . toBe ( "A skill from home directory expansion." )
302+ } ,
303+ } )
304+ } finally {
305+ process . env . OPENCODE_TEST_HOME = original
306+ }
307+ } )
308+
309+ test ( "handles absolute custom skill paths" , async ( ) => {
310+ await using tmp = await tmpdir ( { git : true } )
311+
312+ const absoluteDir = path . join ( tmp . path , "absolute-skills" , "skill" , "abs-skill" )
313+ await fs . mkdir ( absoluteDir , { recursive : true } )
314+ await Bun . write (
315+ path . join ( absoluteDir , "SKILL.md" ) ,
316+ `---
317+ name: abs-skill
318+ description: A skill from an absolute path.
319+ ---
320+
321+ # Absolute Skill
322+ ` ,
323+ )
324+ await Bun . write (
325+ path . join ( tmp . path , "opencode.json" ) ,
326+ JSON . stringify ( {
327+ $schema : "https://opencode.ai/config.json" ,
328+ skill : {
329+ paths : [ path . join ( tmp . path , "absolute-skills" ) ] ,
330+ } ,
331+ } ) ,
332+ )
333+
334+ await Instance . provide ( {
335+ directory : tmp . path ,
336+ fn : async ( ) => {
337+ const skills = await Skill . all ( )
338+ expect ( skills . length ) . toBe ( 1 )
339+ const skill = skills . find ( ( s ) => s . name === "abs-skill" )
340+ expect ( skill ) . toBeDefined ( )
341+ expect ( skill ! . location ) . toContain ( "absolute-skills/skill/abs-skill/SKILL.md" )
342+ } ,
343+ } )
344+ } )
345+
346+ test ( "gracefully skips non-existent custom paths" , async ( ) => {
347+ await using tmp = await tmpdir ( {
348+ git : true ,
349+ init : async ( dir ) => {
350+ const skillDir = path . join ( dir , ".opencode" , "skill" , "existing-skill" )
351+ await Bun . write (
352+ path . join ( skillDir , "SKILL.md" ) ,
353+ `---
354+ name: existing-skill
355+ description: An existing skill.
356+ ---
357+
358+ # Existing Skill
359+ ` ,
360+ )
361+ await Bun . write (
362+ path . join ( dir , "opencode.json" ) ,
363+ JSON . stringify ( {
364+ $schema : "https://opencode.ai/config.json" ,
365+ skill : {
366+ paths : [ "non-existent-path" , "another-missing-dir" ] ,
367+ } ,
368+ } ) ,
369+ )
370+ } ,
371+ } )
372+
373+ await Instance . provide ( {
374+ directory : tmp . path ,
375+ fn : async ( ) => {
376+ const skills = await Skill . all ( )
377+ expect ( skills . length ) . toBe ( 1 )
378+ expect ( skills [ 0 ] . name ) . toBe ( "existing-skill" )
379+ } ,
380+ } )
381+ } )
382+
383+ test ( "merges custom paths with default paths" , async ( ) => {
384+ await using tmp = await tmpdir ( {
385+ git : true ,
386+ init : async ( dir ) => {
387+ const defaultDir = path . join ( dir , ".opencode" , "skill" , "default-skill" )
388+ await Bun . write (
389+ path . join ( defaultDir , "SKILL.md" ) ,
390+ `---
391+ name: default-skill
392+ description: A skill from default path.
393+ ---
394+
395+ # Default Skill
396+ ` ,
397+ )
398+ const customDir = path . join ( dir , "custom-skills" , "skill" , "custom-skill" )
399+ await Bun . write (
400+ path . join ( customDir , "SKILL.md" ) ,
401+ `---
402+ name: custom-skill
403+ description: A skill from custom path.
404+ ---
405+
406+ # Custom Skill
407+ ` ,
408+ )
409+ await Bun . write (
410+ path . join ( dir , "opencode.json" ) ,
411+ JSON . stringify ( {
412+ $schema : "https://opencode.ai/config.json" ,
413+ skill : {
414+ paths : [ "custom-skills" ] ,
415+ } ,
416+ } ) ,
417+ )
418+ } ,
419+ } )
420+
421+ await Instance . provide ( {
422+ directory : tmp . path ,
423+ fn : async ( ) => {
424+ const skills = await Skill . all ( )
425+ expect ( skills . length ) . toBe ( 2 )
426+ expect ( skills . find ( ( s ) => s . name === "default-skill" ) ) . toBeDefined ( )
427+ expect ( skills . find ( ( s ) => s . name === "custom-skill" ) ) . toBeDefined ( )
428+ } ,
429+ } )
430+ } )
0 commit comments