Skip to content

Commit b6ca9d8

Browse files
committed
refine skill when behavior
1 parent d182350 commit b6ca9d8

3 files changed

Lines changed: 195 additions & 13 deletions

File tree

src/index.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -234,10 +234,11 @@ async function getTools(
234234
function filterExtraSkills(
235235
extraSkills: ExtraSkill[] | undefined,
236236
templateName?: string,
237+
tools: string[] = [],
237238
) {
238239
return extraSkills?.filter((extraSkill) => {
239240
const when = extraSkill.when ?? (() => true);
240-
return templateName ? when(templateName) : true;
241+
return templateName ? when(templateName, tools) : true;
241242
});
242243
}
243244

@@ -257,14 +258,15 @@ async function getSkills(
257258
{ skill, dir, template }: Argv,
258259
extraSkills?: ExtraSkill[],
259260
templateName?: string,
261+
tools: string[] = [],
260262
promptMultiselect: typeof multiselect = multiselect,
261263
) {
262264
const parsedSkills = parseSkillsOption(skill);
263-
const filteredExtraSkills = filterExtraSkills(extraSkills, templateName);
265+
const filteredExtraSkills = filterExtraSkills(extraSkills, templateName, tools);
264266

265267
if (parsedSkills !== null) {
266268
return parsedSkills.filter((value: string) =>
267-
filteredExtraSkills?.some((extraSkill) => extraSkill.value === value),
269+
extraSkills?.some((extraSkill) => extraSkill.value === value),
268270
);
269271
}
270272

@@ -367,7 +369,7 @@ type ExtraSkill = {
367369
label: string;
368370
source: string;
369371
skill?: string;
370-
when?: (templateName: string) => boolean;
372+
when?: (templateName: string, tools: string[]) => boolean;
371373
order?: 'pre' | 'post';
372374
};
373375

@@ -566,13 +568,7 @@ export async function create({
566568

567569
const templateName = await getTemplateName(argv);
568570
const tools = await getTools(argv, extraTools, templateName);
569-
const filteredExtraSkills = filterExtraSkills(extraSkills, templateName);
570-
const skills = await getSkills(
571-
argv,
572-
filteredExtraSkills,
573-
templateName,
574-
multiselect,
575-
);
571+
const skills = await getSkills(argv, extraSkills, templateName, tools, multiselect);
576572

577573
const srcFolder = path.join(root, `template-${templateName}`);
578574
const commonFolder = path.join(root, 'template-common');
@@ -598,7 +594,7 @@ export async function create({
598594
});
599595

600596
const skillsByValue = new Map(
601-
(filteredExtraSkills ?? []).map((extraSkill) => [extraSkill.value, extraSkill]),
597+
(extraSkills ?? []).map((extraSkill) => [extraSkill.value, extraSkill]),
602598
);
603599
let currentSkillBatch: ExtraSkill[] = [];
604600

test/help.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,55 @@ test('help message includes optional skills', async () => {
9696
expect(logOutput).toContain('Optional skills:');
9797
expect(logOutput).toContain('git-url');
9898
});
99+
100+
test('help message lists all optional skills even when template and tools are provided', async () => {
101+
const logs: string[] = [];
102+
const originalLog = logger.log;
103+
104+
logger.override({
105+
log: (message?: unknown) => {
106+
logs.push(String(message ?? ''));
107+
},
108+
});
109+
110+
try {
111+
await create({
112+
name: 'test',
113+
root: '.',
114+
templates: ['vanilla', 'react'],
115+
getTemplateName: async () => 'vanilla',
116+
extraSkills: [
117+
{
118+
value: 'shared-docs',
119+
label: 'Shared Docs',
120+
source: 'acme/skills',
121+
when: (templateName) => templateName === 'vanilla',
122+
},
123+
{
124+
value: 'react-docs',
125+
label: 'React Docs',
126+
source: 'acme/skills',
127+
when: (templateName) => templateName === 'react',
128+
},
129+
{
130+
value: 'rstest-best-practices',
131+
label: 'Rstest Best Practices',
132+
source: 'rstackjs/agent-skills',
133+
when: (_templateName, selectedTools) => selectedTools.includes('rstest'),
134+
},
135+
],
136+
argv: ['node', 'test', '--help', '--template', 'vanilla', '--tools', 'biome'],
137+
});
138+
} finally {
139+
logger.override({
140+
log: originalLog,
141+
});
142+
}
143+
144+
const logOutput = logs.join('\n');
145+
expect(logOutput).toContain('--skill <skill>');
146+
expect(logOutput).toContain('Optional skills:');
147+
expect(logOutput).toContain('shared-docs');
148+
expect(logOutput).toContain('react-docs');
149+
expect(logOutput).toContain('rstest-best-practices');
150+
});

test/skills.test.ts

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ test('should prove --skill skips the skills prompt even without --dir and --temp
731731
});
732732
});
733733

734-
test('should filter extra skills by template and install using skill override', async () => {
734+
test('should honor explicit --skill values even when they are hidden by template gating', async () => {
735735
const projectDir = path.join(testDir, 'skills-template-filtering');
736736
const calls = createExecCommand();
737737

@@ -770,6 +770,140 @@ test('should filter extra skills by template and install using skill override',
770770

771771
expect(calls).toHaveLength(1);
772772
expect(calls[0]).toEqual({
773+
args: [
774+
'-y',
775+
'skills',
776+
'add',
777+
'acme/skills',
778+
'--agent',
779+
'universal',
780+
'--yes',
781+
'--copy',
782+
'--skill',
783+
'docs/react',
784+
'--skill',
785+
'docs/shared',
786+
],
787+
command: 'npx',
788+
options: expect.objectContaining({
789+
nodeOptions: expect.objectContaining({
790+
cwd: projectDir,
791+
stdio: 'pipe',
792+
}),
793+
}),
794+
});
795+
});
796+
797+
test('should show tool-gated skills in the prompt when the required tool is selected', async () => {
798+
const projectDir = path.join(testDir, 'skills-tools-filtering-prompt');
799+
800+
await create({
801+
name: 'test',
802+
root: fixturesDir,
803+
templates: ['vanilla'],
804+
getTemplateName: async () => 'vanilla',
805+
extraTools: [
806+
{
807+
value: 'rstest',
808+
label: 'Rstest',
809+
},
810+
],
811+
extraSkills: [
812+
{
813+
value: 'shared-docs',
814+
label: 'Shared Docs',
815+
source: 'acme/skills',
816+
},
817+
{
818+
value: 'rstest-best-practices',
819+
label: 'Rstest Best Practices',
820+
source: 'rstackjs/agent-skills',
821+
when: (_templateName, selectedTools) => selectedTools.includes('rstest'),
822+
},
823+
],
824+
argv: ['node', 'test', projectDir, '--tools', 'rstest'],
825+
});
826+
827+
expect(mocks.state.promptOptions).toEqual([
828+
{
829+
value: 'shared-docs',
830+
label: 'Shared Docs',
831+
hint: 'acme/skills',
832+
},
833+
{
834+
value: 'rstest-best-practices',
835+
label: 'Rstest Best Practices',
836+
hint: 'rstackjs/agent-skills',
837+
},
838+
]);
839+
});
840+
841+
test('should honor explicit --skill values even when the required tool is not selected', async () => {
842+
const projectDir = path.join(testDir, 'skills-tools-filtering-cli');
843+
const calls = createExecCommand();
844+
845+
await create({
846+
name: 'test',
847+
root: fixturesDir,
848+
templates: ['vanilla'],
849+
getTemplateName: async () => 'vanilla',
850+
extraTools: [
851+
{
852+
value: 'rstest',
853+
label: 'Rstest',
854+
},
855+
],
856+
extraSkills: [
857+
{
858+
value: 'shared-docs',
859+
label: 'Shared Docs',
860+
source: 'acme/skills',
861+
skill: 'docs/shared',
862+
},
863+
{
864+
value: 'rstest-best-practices',
865+
label: 'Rstest Best Practices',
866+
source: 'rstackjs/agent-skills',
867+
when: (_templateName, selectedTools) => selectedTools.includes('rstest'),
868+
},
869+
],
870+
argv: [
871+
'node',
872+
'test',
873+
'--dir',
874+
projectDir,
875+
'--template',
876+
'vanilla',
877+
'--tools',
878+
'biome',
879+
'--skill',
880+
'rstest-best-practices,shared-docs',
881+
],
882+
});
883+
884+
expect(calls).toHaveLength(2);
885+
expect(calls[0]).toEqual({
886+
args: [
887+
'-y',
888+
'skills',
889+
'add',
890+
'rstackjs/agent-skills',
891+
'--agent',
892+
'universal',
893+
'--yes',
894+
'--copy',
895+
'--skill',
896+
'rstest-best-practices',
897+
],
898+
command: 'npx',
899+
options: expect.objectContaining({
900+
nodeOptions: expect.objectContaining({
901+
cwd: projectDir,
902+
stdio: 'pipe',
903+
}),
904+
}),
905+
});
906+
expect(calls[1]).toEqual({
773907
args: [
774908
'-y',
775909
'skills',

0 commit comments

Comments
 (0)