@@ -243,7 +243,75 @@ async function getCreateError(action: Promise<unknown>) {
243243 throw new Error ( 'Expected create() to throw' ) ;
244244}
245245
246- test ( 'should install selected extra skills from comma separated --skill option' , async ( ) => {
246+ test ( 'should batch selected same-source extra skills from comma separated --skill option into a single install' , async ( ) => {
247+ const projectDir = path . join ( testDir , 'skills-comma-separated-same-source' ) ;
248+ const calls = createExecCommand ( ) ;
249+ const taskLogEvents = mocks . state . taskLogEvents ;
250+ const commandLogs = mocks . state . commandLogs ;
251+
252+ await create ( {
253+ name : 'test' ,
254+ root : fixturesDir ,
255+ templates : [ 'vanilla' ] ,
256+ getTemplateName : async ( ) => 'vanilla' ,
257+ extraSkills : [
258+ {
259+ value : 'rstest-best-practices' ,
260+ label : 'Rstest Best Practices' ,
261+ source : 'rstackjs/agent-skills' ,
262+ } ,
263+ {
264+ value : 'rsbuild-best-practices' ,
265+ label : 'Rsbuild Best Practices' ,
266+ source : 'rstackjs/agent-skills' ,
267+ } ,
268+ ] ,
269+ argv : [
270+ 'node' ,
271+ 'test' ,
272+ '--dir' ,
273+ projectDir ,
274+ '--template' ,
275+ 'vanilla' ,
276+ '--skill' ,
277+ 'rstest-best-practices,rsbuild-best-practices' ,
278+ ] ,
279+ } ) ;
280+
281+ expect ( calls ) . toHaveLength ( 1 ) ;
282+ expect ( calls [ 0 ] ) . toEqual ( {
283+ args : [
284+ '-y' ,
285+ 'skills' ,
286+ 'add' ,
287+ 'rstackjs/agent-skills' ,
288+ '--agent' ,
289+ 'universal' ,
290+ '--yes' ,
291+ '--copy' ,
292+ '--skill' ,
293+ 'rstest-best-practices' ,
294+ '--skill' ,
295+ 'rsbuild-best-practices' ,
296+ ] ,
297+ command : 'npx' ,
298+ options : expect . objectContaining ( {
299+ nodeOptions : expect . objectContaining ( {
300+ cwd : projectDir ,
301+ stdio : 'pipe' ,
302+ } ) ,
303+ } ) ,
304+ } ) ;
305+ expect ( taskLogEvents ) . toEqual ( [
306+ 'create:Installing skills rstest-best-practices, rsbuild-best-practices' ,
307+ 'success:Installing skills rstest-best-practices, rsbuild-best-practices:Installed skills rstest-best-practices, rsbuild-best-practices' ,
308+ ] ) ;
309+ expect ( commandLogs ) . toContain (
310+ `Running skill install command: ${ color . dim ( 'npx -y skills add rstackjs/agent-skills --agent universal --yes --copy --skill rstest-best-practices --skill rsbuild-best-practices' ) } ` ,
311+ ) ;
312+ } ) ;
313+
314+ test ( 'should install selected extra skills from comma separated --skill option across different sources' , async ( ) => {
247315 const projectDir = path . join ( testDir , 'skills-comma-separated' ) ;
248316 const calls = createExecCommand ( ) ;
249317 const taskLogEvents = mocks . state . taskLogEvents ;
@@ -415,6 +483,110 @@ test('should install selected extra skills from repeated --skill flags', async (
415483 } ) ;
416484} ) ;
417485
486+ test ( 'should preserve skill install order when the same source appears non-contiguously' , async ( ) => {
487+ const projectDir = path . join ( testDir , 'skills-preserve-order' ) ;
488+ const calls = createExecCommand ( ) ;
489+
490+ await create ( {
491+ name : 'test' ,
492+ root : fixturesDir ,
493+ templates : [ 'vanilla' ] ,
494+ getTemplateName : async ( ) => 'vanilla' ,
495+ extraSkills : [
496+ {
497+ value : 'rstest-best-practices' ,
498+ label : 'Rstest Best Practices' ,
499+ source : 'rstackjs/agent-skills' ,
500+ } ,
501+ {
502+ value : 'docs-writer' ,
503+ label : 'Docs Writer' ,
504+ source : 'acme/skills' ,
505+ } ,
506+ {
507+ value : 'rsbuild-best-practices' ,
508+ label : 'Rsbuild Best Practices' ,
509+ source : 'rstackjs/agent-skills' ,
510+ } ,
511+ ] ,
512+ argv : [
513+ 'node' ,
514+ 'test' ,
515+ '--dir' ,
516+ projectDir ,
517+ '--template' ,
518+ 'vanilla' ,
519+ '--skill' ,
520+ 'rstest-best-practices,docs-writer,rsbuild-best-practices' ,
521+ ] ,
522+ } ) ;
523+
524+ expect ( calls ) . toHaveLength ( 3 ) ;
525+ expect ( calls [ 0 ] ) . toEqual ( {
526+ args : [
527+ '-y' ,
528+ 'skills' ,
529+ 'add' ,
530+ 'rstackjs/agent-skills' ,
531+ '--agent' ,
532+ 'universal' ,
533+ '--yes' ,
534+ '--copy' ,
535+ '--skill' ,
536+ 'rstest-best-practices' ,
537+ ] ,
538+ command : 'npx' ,
539+ options : expect . objectContaining ( {
540+ nodeOptions : expect . objectContaining ( {
541+ cwd : projectDir ,
542+ stdio : 'pipe' ,
543+ } ) ,
544+ } ) ,
545+ } ) ;
546+ expect ( calls [ 1 ] ) . toEqual ( {
547+ args : [
548+ '-y' ,
549+ 'skills' ,
550+ 'add' ,
551+ 'acme/skills' ,
552+ '--agent' ,
553+ 'universal' ,
554+ '--yes' ,
555+ '--copy' ,
556+ '--skill' ,
557+ 'docs-writer' ,
558+ ] ,
559+ command : 'npx' ,
560+ options : expect . objectContaining ( {
561+ nodeOptions : expect . objectContaining ( {
562+ cwd : projectDir ,
563+ stdio : 'pipe' ,
564+ } ) ,
565+ } ) ,
566+ } ) ;
567+ expect ( calls [ 2 ] ) . toEqual ( {
568+ args : [
569+ '-y' ,
570+ 'skills' ,
571+ 'add' ,
572+ 'rstackjs/agent-skills' ,
573+ '--agent' ,
574+ 'universal' ,
575+ '--yes' ,
576+ '--copy' ,
577+ '--skill' ,
578+ 'rsbuild-best-practices' ,
579+ ] ,
580+ command : 'npx' ,
581+ options : expect . objectContaining ( {
582+ nodeOptions : expect . objectContaining ( {
583+ cwd : projectDir ,
584+ stdio : 'pipe' ,
585+ } ) ,
586+ } ) ,
587+ } ) ;
588+ } ) ;
589+
418590test ( 'should skip the skills prompt when --skill is provided' , async ( ) => {
419591 const projectDir = path . join ( testDir , 'skills-skip-prompt-with-cli-option' ) ;
420592 const calls = createExecCommand ( ) ;
@@ -663,6 +835,53 @@ test('should throw the install command context when installation fails', async (
663835 ) ;
664836} ) ;
665837
838+ test ( 'should throw grouped install command context when batched installation fails' , async ( ) => {
839+ const projectDir = path . join ( testDir , 'skills-batched-install-failure' ) ;
840+ createExecCommand ( ( ) => {
841+ return {
842+ stdout : '' ,
843+ stderr : 'install failed' ,
844+ exitCode : 1 ,
845+ } ;
846+ } ) ;
847+
848+ const error = await getCreateError (
849+ create ( {
850+ name : 'test' ,
851+ root : fixturesDir ,
852+ templates : [ 'vanilla' ] ,
853+ getTemplateName : async ( ) => 'vanilla' ,
854+ extraSkills : [
855+ {
856+ value : 'rstest-best-practices' ,
857+ label : 'Rstest Best Practices' ,
858+ source : 'rstackjs/agent-skills' ,
859+ } ,
860+ {
861+ value : 'rsbuild-best-practices' ,
862+ label : 'Rsbuild Best Practices' ,
863+ source : 'rstackjs/agent-skills' ,
864+ } ,
865+ ] ,
866+ argv : [
867+ 'node' ,
868+ 'test' ,
869+ '--dir' ,
870+ projectDir ,
871+ '--template' ,
872+ 'vanilla' ,
873+ '--skill' ,
874+ 'rstest-best-practices,rsbuild-best-practices' ,
875+ ] ,
876+ } ) ,
877+ ) ;
878+
879+ expect ( error ) . toBeInstanceOf ( Error ) ;
880+ expect ( ( error as Error ) . message ) . toBe (
881+ 'Failed to install skills "rstest-best-practices", "rsbuild-best-practices" from "rstackjs/agent-skills" using command: npx -y skills add rstackjs/agent-skills --agent universal --yes --copy --skill rstest-best-practices --skill rsbuild-best-practices' ,
882+ ) ;
883+ } ) ;
884+
666885test ( 'should omit noisy skills cli output from install errors' , async ( ) => {
667886 const projectDir = path . join ( testDir , 'skills-install-noisy-error' ) ;
668887 const rawStdout = `███████╗██╗ ██╗██╗██╗ ██╗ ███████╗
0 commit comments