@@ -109,6 +109,20 @@ component extends="coldbox-cli.models.BaseAICommand" aliases="coldbox ai skills
109109 var resolvedItems = _resolveSlugs ( slugs , language )
110110
111111 if ( resolvedItems .isEmpty () ) {
112+ // Try direct GitHub folder fallback (for skills not yet indexed in the registry)
113+ var githubInstalled = _tryGitHubFolderInstall (
114+ slugs = slugs ,
115+ directory = arguments .directory ,
116+ manifest = manifest ,
117+ force = arguments .force
118+ )
119+ if ( githubInstalled ) {
120+ saveManifest ( arguments .directory , manifest )
121+ _regenerateAgents ( arguments .directory , manifest )
122+ print .line ()
123+ printTip ( " Run 'coldbox ai skills list' to see all installed skills." )
124+ return
125+ }
112126 printError ( " No matching skills found for the given slug(s) '#arguments .slug #'." )
113127 return
114128 } else {
@@ -409,20 +423,28 @@ component extends="coldbox-cli.models.BaseAICommand" aliases="coldbox ai skills
409423 } else {
410424 // Fall back to category filter
411425 var categoryMatches = repoSkills .filter ( ( s ) = > s ? .category == thirdPart )
412- if ( categoryMatches .len () ) {
413- for ( var cs in categoryMatches ) {
414- resolved .append ( {
415- owner : slugOwner ,
416- repo : slugRepo ,
417- slug : cs .slug ,
418- name : cs .name ,
419- description : cs .description ?: " " ,
420- type : " core" ,
421- source : " "
422- } )
423- }
426+
427+ // Fallback: slug prefix match (e.g. folder~skill-name when thirdPart is a folder)
428+ if ( categoryMatches .isEmpty () ) {
429+ categoryMatches = repoSkills .filter ( ( s ) = > s .slug .listFirst ( " ~" ) == thirdPart )
430+ }
431+
432+ // Fallback: path prefix match (e.g. folder/skill-name/SKILL.md)
433+ if ( categoryMatches .isEmpty () ) {
434+ categoryMatches = repoSkills .filter ( ( s ) = > s .path .startsWith ( thirdPart & " /" ) )
435+ }
436+
437+ for ( var cs in categoryMatches ) {
438+ resolved .append ( {
439+ owner : slugOwner ,
440+ repo : slugRepo ,
441+ slug : cs .slug ,
442+ name : cs .name ,
443+ description : cs .description ?: " " ,
444+ type : " core" ,
445+ source : " "
446+ } )
424447 }
425- // If neither a direct skill nor a category matched, resolved stays empty for this slug
426448 }
427449 } else {
428450 // Explicit 4+ part slug: owner/repo/category/skill-name
@@ -474,4 +496,121 @@ component extends="coldbox-cli.models.BaseAICommand" aliases="coldbox ai skills
474496 }
475497 }
476498
499+ /**
500+ * Fallback: install skills by fetching a GitHub folder directly via the Contents API.
501+ * Used when the registry has no results for a 3-part slug (owner/repo/folder).
502+ *
503+ * @slugs Array of slug strings (only 3-part ones are processed)
504+ * @directory Target project directory
505+ * @manifest Manifest struct to update (mutated in place)
506+ * @force Overwrite existing skills if true
507+ *
508+ * @return true if at least one skill was installed
509+ */
510+ private boolean function _tryGitHubFolderInstall (
511+ required array slugs ,
512+ required string directory ,
513+ required struct manifest ,
514+ required boolean force
515+ ){
516+ var installed = false
517+
518+ for ( var slug in arguments .slugs ) {
519+ var parts = slug .listToArray ( " /" )
520+ if ( parts .len () == 3 ) {
521+ var owner = parts [ 1 ]
522+ var repo = parts [ 2 ]
523+ var folder = parts [ 3 ]
524+
525+ // Fetch folder listing from GitHub Contents API
526+ var listResult = " "
527+ cfhttp (
528+ method = " GET" ,
529+ url = " https://api.github.com/repos/#owner #/#repo #/contents/#folder #" ,
530+ result = " listResult" ,
531+ timeout = 15
532+ ) {
533+ cfhttpparam ( type = " header" , name = " User-Agent" , value = " coldbox-cli" );
534+ cfhttpparam ( type = " header" , name = " Accept" , value = " application/vnd.github.v3+json" );
535+ };
536+
537+ if ( val ( listResult .statusCode ) > 0 && val ( listResult .statusCode ) < 400 ) {
538+ try {
539+ var items = deserializeJSON ( listResult .fileContent )
540+ if ( isArray ( items ) ) {
541+ var dirs = items .filter ( ( i ) = > i .type == " dir" )
542+
543+ if ( dirs .len () > 0 ) {
544+ printInfo ( " Fetching #dirs .len () # skill(s) directly from GitHub: #owner #/#repo #/#folder #" )
545+ print .line ().toConsole ()
546+
547+ var successCount = 0
548+ for ( var item in dirs ) {
549+ var skillResult = " "
550+ cfhttp (
551+ method = " GET" ,
552+ url = " https://api.github.com/repos/#owner #/#repo #/contents/#item .path #/SKILL.md" ,
553+ result = " skillResult" ,
554+ timeout = 15
555+ ) {
556+ cfhttpparam ( type = " header" , name = " User-Agent" , value = " coldbox-cli" );
557+ cfhttpparam ( type = " header" , name = " Accept" , value = " application/vnd.github.v3+json" );
558+ };
559+
560+ if ( val ( skillResult .statusCode ) > 0 && val ( skillResult .statusCode ) < 400 ) {
561+ var fileData = deserializeJSON ( skillResult .fileContent )
562+
563+ if ( fileData .keyExists ( " content" ) ) {
564+ var rawContent = fileData .content .replace ( chr ( 10 ), " " ).replace ( chr ( 13 ), " " )
565+ var content = toString ( binaryDecode ( rawContent , " base64" ) )
566+ var sha = fileData .sha ?: " "
567+ var skillName = item .name
568+ var canInstall = true
569+
570+ if ( ! force ) {
571+ var existing = variables .skillManager .getSkillFilePath ( directory , skillName )
572+ if ( ! isNull ( existing ) ) {
573+ printInfo ( " → #skillName # already installed (use --force to overwrite)" )
574+ canInstall = false
575+ }
576+ }
577+
578+ if ( canInstall ) {
579+ variables .skillManager .installRemoteSkill (
580+ directory = directory ,
581+ name = skillName ,
582+ content = content ,
583+ owner = owner ,
584+ repo = repo ,
585+ path = item .path ,
586+ sha = sha ,
587+ description = " " ,
588+ auditStatus = " skipped" ,
589+ skillType = " core" ,
590+ source = " " ,
591+ manifest = manifest
592+ )
593+ print .greenLine ( " + #skillName #" )
594+ successCount ++
595+ installed = true
596+ }
597+ }
598+ }
599+ }
600+
601+ if ( successCount ) {
602+ printSuccess ( " Installed #successCount # skill(s) from GitHub." )
603+ }
604+ }
605+ }
606+ } catch ( any e ) {
607+ printWarn ( " GitHub folder fetch failed for '#slug #': #e .message #" )
608+ }
609+ }
610+ }
611+ }
612+
613+ return installed
614+ }
615+
477616}
0 commit comments