Skip to content

Commit cb18072

Browse files
committed
fix: preserve skill install order when batching
1 parent c99215e commit cb18072

2 files changed

Lines changed: 122 additions & 9 deletions

File tree

src/index.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,22 @@ async function runSkillCommand(
461461
installationTaskLog.success(`Installed ${skillNoun} ${skillLabel}`);
462462
}
463463

464+
function groupContiguousSkillsBySource(skills: ExtraSkill[]) {
465+
const skillGroups: ExtraSkill[][] = [];
466+
467+
for (const skill of skills) {
468+
const lastGroup = skillGroups.at(-1);
469+
if (lastGroup?.[0]?.source === skill.source) {
470+
lastGroup.push(skill);
471+
continue;
472+
}
473+
474+
skillGroups.push([skill]);
475+
}
476+
477+
return skillGroups;
478+
}
479+
464480
export async function create({
465481
name,
466482
root,
@@ -603,15 +619,8 @@ export async function create({
603619
)
604620
.filter((skill): skill is ExtraSkill => Boolean(skill));
605621

606-
// Group selected skills by source so each repository is installed once.
607-
const skillGroups = new Map<string, ExtraSkill[]>();
608-
for (const skill of selectedExtraSkills) {
609-
const currentSkills = skillGroups.get(skill.source) ?? [];
610-
currentSkills.push(skill);
611-
skillGroups.set(skill.source, currentSkills);
612-
}
613-
614-
for (const groupedSkills of skillGroups.values()) {
622+
// Batch only contiguous skills from the same source to preserve install order.
623+
for (const groupedSkills of groupContiguousSkillsBySource(selectedExtraSkills)) {
615624
await runSkillCommand(groupedSkills, distFolder);
616625
}
617626

test/skills.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,110 @@ test('should install selected extra skills from repeated --skill flags', async (
483483
});
484484
});
485485

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+
486590
test('should skip the skills prompt when --skill is provided', async () => {
487591
const projectDir = path.join(testDir, 'skills-skip-prompt-with-cli-option');
488592
const calls = createExecCommand();

0 commit comments

Comments
 (0)