Skip to content

Commit 3224805

Browse files
committed
feat: generate skills to workspace root with composable naming and YAML frontmatter
- Add findWorkspaceRoot() utility (pnpm-workspace.yaml, lerna.json, package.json workspaces) - Add skillsPath config option for explicit override - Remove docs.skills from DocsConfig (skills now auto-generate) - Update buildSkillFile() to emit YAML frontmatter (name, description) - Update ORM skills: orm-{target}-{entity}/SKILL.md - Update hooks skills: hooks-{target}-{entity}/SKILL.md - Update CLI skills: cli-{target}-{entity}/SKILL.md + cli-context, cli-auth - Refactor generate()/generateMulti() to write skills to workspace root - Clean up old nested skills dirs after writing - Update SDK/React generation scripts to remove docs.skills - Fix test expectations for resolveDocsConfig
1 parent 16e3a68 commit 3224805

10 files changed

Lines changed: 271 additions & 73 deletions

File tree

graphql/codegen/src/__tests__/codegen/cli-generator.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,22 +397,22 @@ describe('target docs generator', () => {
397397
describe('resolveDocsConfig', () => {
398398
it('defaults to readme + agents', () => {
399399
const config = resolveDocsConfig(undefined);
400-
expect(config).toEqual({ readme: true, agents: true, mcp: false, skills: false });
400+
expect(config).toEqual({ readme: true, agents: true, mcp: false });
401401
});
402402

403403
it('docs: true enables all', () => {
404404
const config = resolveDocsConfig(true);
405-
expect(config).toEqual({ readme: true, agents: true, mcp: true, skills: true });
405+
expect(config).toEqual({ readme: true, agents: true, mcp: true });
406406
});
407407

408408
it('docs: false disables all', () => {
409409
const config = resolveDocsConfig(false);
410-
expect(config).toEqual({ readme: false, agents: false, mcp: false, skills: false });
410+
expect(config).toEqual({ readme: false, agents: false, mcp: false });
411411
});
412412

413413
it('partial config fills defaults', () => {
414414
const config = resolveDocsConfig({ mcp: true });
415-
expect(config).toEqual({ readme: true, agents: true, mcp: true, skills: false });
415+
expect(config).toEqual({ readme: true, agents: true, mcp: true });
416416
});
417417
});
418418

graphql/codegen/src/core/codegen/cli/docs-generator.ts

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -591,13 +591,16 @@ export function generateSkills(
591591
tables: CleanTable[],
592592
customOperations: CleanOperation[],
593593
toolName: string,
594+
targetName: string,
594595
): GeneratedDocFile[] {
595596
const files: GeneratedDocFile[] = [];
596597

598+
const contextSkillName = 'cli-context';
599+
597600
files.push({
598-
fileName: 'skills/context.md',
601+
fileName: `${contextSkillName}/SKILL.md`,
599602
content: buildSkillFile({
600-
name: `${toolName}-context`,
603+
name: contextSkillName,
601604
description: `Manage API endpoint contexts for ${toolName}`,
602605
usage: [
603606
`${toolName} context create <name> --endpoint <url>`,
@@ -622,10 +625,12 @@ export function generateSkills(
622625
}),
623626
});
624627

628+
const authSkillName = 'cli-auth';
629+
625630
files.push({
626-
fileName: 'skills/auth.md',
631+
fileName: `${authSkillName}/SKILL.md`,
627632
content: buildSkillFile({
628-
name: `${toolName}-auth`,
633+
name: authSkillName,
629634
description: `Manage authentication tokens for ${toolName}`,
630635
usage: [
631636
`${toolName} auth set-token <token>`,
@@ -651,10 +656,12 @@ export function generateSkills(
651656
const pk = getPrimaryKeyInfo(table)[0];
652657
const editableFields = getEditableFields(table);
653658

659+
const skillName = `cli-${targetName}-${kebab}`;
660+
654661
files.push({
655-
fileName: `skills/${kebab}.md`,
662+
fileName: `${skillName}/SKILL.md`,
656663
content: buildSkillFile({
657-
name: `${toolName}-${kebab}`,
664+
name: skillName,
658665
description: `CRUD operations for ${table.name} records via ${toolName} CLI`,
659666
usage: [
660667
`${toolName} ${kebab} list`,
@@ -700,10 +707,12 @@ export function generateSkills(
700707
? `${toolName} ${kebab} ${op.args.map((a) => `--${a.name} <value>`).join(' ')}`
701708
: `${toolName} ${kebab}`;
702709

710+
const skillName = `cli-${targetName}-${kebab}`;
711+
703712
files.push({
704-
fileName: `skills/${kebab}.md`,
713+
fileName: `${skillName}/SKILL.md`,
705714
content: buildSkillFile({
706-
name: `${toolName}-${kebab}`,
715+
name: skillName,
707716
description: op.description || `Execute the ${op.name} ${op.kind}`,
708717
usage: [usage],
709718
examples: [
@@ -1428,10 +1437,12 @@ export function generateMultiTargetSkills(
14281437
const contextCreateFlags = targets
14291438
.map((t) => `--${t.name}-endpoint <url>`)
14301439
.join(' ');
1440+
const contextSkillName = 'cli-context';
1441+
14311442
files.push({
1432-
fileName: `skills/${builtinNames.context}.md`,
1443+
fileName: `${contextSkillName}/SKILL.md`,
14331444
content: buildSkillFile({
1434-
name: `${toolName}-${builtinNames.context}`,
1445+
name: contextSkillName,
14351446
description: `Manage API endpoint contexts for ${toolName} (multi-target: ${targets.map((t) => t.name).join(', ')})`,
14361447
usage: contextUsage,
14371448
examples: [
@@ -1460,10 +1471,12 @@ export function generateMultiTargetSkills(
14601471
}),
14611472
});
14621473

1474+
const authSkillName = 'cli-auth';
1475+
14631476
files.push({
1464-
fileName: `skills/${builtinNames.auth}.md`,
1477+
fileName: `${authSkillName}/SKILL.md`,
14651478
content: buildSkillFile({
1466-
name: `${toolName}-${builtinNames.auth}`,
1479+
name: authSkillName,
14671480
description: `Manage authentication tokens for ${toolName} (shared across all targets)`,
14681481
usage: [
14691482
`${toolName} ${builtinNames.auth} set-token <token>`,
@@ -1491,10 +1504,12 @@ export function generateMultiTargetSkills(
14911504
const editableFields = getEditableFields(table);
14921505
const cmd = `${tgt.name}:${kebab}`;
14931506

1507+
const skillName = `cli-${tgt.name}-${kebab}`;
1508+
14941509
files.push({
1495-
fileName: `skills/${tgt.name}-${kebab}.md`,
1510+
fileName: `${skillName}/SKILL.md`,
14961511
content: buildSkillFile({
1497-
name: `${toolName}-${cmd}`,
1512+
name: skillName,
14981513
description: `CRUD operations for ${table.name} records via ${toolName} CLI (${tgt.name} target)`,
14991514
usage: [
15001515
`${toolName} ${cmd} list`,
@@ -1535,10 +1550,12 @@ export function generateMultiTargetSkills(
15351550
usageLines.push(`${baseUsage} --save-token`);
15361551
}
15371552

1553+
const skillName = `cli-${tgt.name}-${kebab}`;
1554+
15381555
files.push({
1539-
fileName: `skills/${tgt.name}-${kebab}.md`,
1556+
fileName: `${skillName}/SKILL.md`,
15401557
content: buildSkillFile({
1541-
name: `${toolName}-${cmd}`,
1558+
name: skillName,
15421559
description: `${op.description || `Execute the ${op.name} ${op.kind}`} (${tgt.name} target)`,
15431560
usage: usageLines,
15441561
examples: [

graphql/codegen/src/core/codegen/docs-utils.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,18 @@ export function resolveDocsConfig(
5959
docs: DocsConfig | boolean | undefined,
6060
): DocsConfig {
6161
if (docs === true) {
62-
return { readme: true, agents: true, mcp: true, skills: true };
62+
return { readme: true, agents: true, mcp: true };
6363
}
6464
if (docs === false) {
65-
return { readme: false, agents: false, mcp: false, skills: false };
65+
return { readme: false, agents: false, mcp: false };
6666
}
6767
if (!docs) {
68-
return { readme: true, agents: true, mcp: false, skills: false };
68+
return { readme: true, agents: true, mcp: false };
6969
}
7070
return {
7171
readme: docs.readme ?? true,
7272
agents: docs.agents ?? true,
7373
mcp: docs.mcp ?? false,
74-
skills: docs.skills ?? false,
7574
};
7675
}
7776

@@ -123,6 +122,13 @@ export function buildSkillFile(skill: SkillDefinition): string {
123122
const lang = skill.language ?? 'bash';
124123
const lines: string[] = [];
125124

125+
// YAML frontmatter (Agent Skills format)
126+
lines.push('---');
127+
lines.push(`name: ${skill.name}`);
128+
lines.push(`description: ${skill.description}`);
129+
lines.push('---');
130+
lines.push('');
131+
126132
lines.push(`# ${skill.name}`);
127133
lines.push('');
128134
lines.push('<!-- @constructive-io/graphql-codegen - DO NOT EDIT -->');

graphql/codegen/src/core/codegen/hooks-docs-generator.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { toKebabCase } from 'komoji';
2+
13
import type { CleanOperation, CleanTable } from '../../types/schema';
24
import {
35
buildSkillFile,
@@ -496,6 +498,7 @@ export function getHooksMcpTools(
496498
export function generateHooksSkills(
497499
tables: CleanTable[],
498500
customOperations: CleanOperation[],
501+
targetName: string,
499502
): GeneratedDocFile[] {
500503
const files: GeneratedDocFile[] = [];
501504

@@ -507,10 +510,13 @@ export function generateHooksSkills(
507510
.map((f) => `${f.name}: true`)
508511
.join(', ');
509512

513+
const tableKebab = toKebabCase(singularName);
514+
const skillName = `hooks-${targetName}-${tableKebab}`;
515+
510516
files.push({
511-
fileName: `skills/${lcFirst(singularName)}.md`,
517+
fileName: `${skillName}/SKILL.md`,
512518
content: buildSkillFile({
513-
name: `hooks-${lcFirst(singularName)}`,
519+
name: skillName,
514520
description: table.description || `React Query hooks for ${table.name} data operations`,
515521
language: 'typescript',
516522
usage: [
@@ -558,10 +564,13 @@ export function generateHooksSkills(
558564
? `{ ${op.args.map((a) => `${a.name}: '<value>'`).join(', ')} }`
559565
: '';
560566

567+
const opKebab = toKebabCase(op.name);
568+
const skillName = `hooks-${targetName}-${opKebab}`;
569+
561570
files.push({
562-
fileName: `skills/${op.name}.md`,
571+
fileName: `${skillName}/SKILL.md`,
563572
content: buildSkillFile({
564-
name: `hooks-${op.name}`,
573+
name: skillName,
565574
description:
566575
op.description ||
567576
`React Query ${op.kind} hook for ${op.name}`,

graphql/codegen/src/core/codegen/orm/docs-generator.ts

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { toKebabCase } from 'komoji';
2+
13
import type { CleanOperation, CleanTable } from '../../../types/schema';
24
import {
35
buildSkillFile,
@@ -449,6 +451,7 @@ export function getOrmMcpTools(
449451
export function generateOrmSkills(
450452
tables: CleanTable[],
451453
customOperations: CleanOperation[],
454+
targetName: string,
452455
): GeneratedDocFile[] {
453456
const files: GeneratedDocFile[] = [];
454457

@@ -457,32 +460,36 @@ export function generateOrmSkills(
457460
const pk = getPrimaryKeyInfo(table)[0];
458461
const editableFields = getEditableFields(table);
459462

463+
const modelName = lcFirst(singularName);
464+
const tableKebab = toKebabCase(singularName);
465+
const skillName = `orm-${targetName}-${tableKebab}`;
466+
460467
files.push({
461-
fileName: `skills/${lcFirst(singularName)}.md`,
468+
fileName: `${skillName}/SKILL.md`,
462469
content: buildSkillFile({
463-
name: `orm-${lcFirst(singularName)}`,
470+
name: skillName,
464471
description: table.description || `ORM operations for ${table.name} records`,
465472
language: 'typescript',
466473
usage: [
467-
`db.${lcFirst(singularName)}.findMany({ select: { id: true } }).execute()`,
468-
`db.${lcFirst(singularName)}.findOne({ ${pk.name}: '<value>', select: { id: true } }).execute()`,
469-
`db.${lcFirst(singularName)}.create({ data: { ${editableFields.map((f) => `${f.name}: '<value>'`).join(', ')} }, select: { id: true } }).execute()`,
470-
`db.${lcFirst(singularName)}.update({ where: { ${pk.name}: '<value>' }, data: { ${editableFields[0]?.name || 'field'}: '<new>' }, select: { id: true } }).execute()`,
471-
`db.${lcFirst(singularName)}.delete({ where: { ${pk.name}: '<value>' } }).execute()`,
474+
`db.${modelName}.findMany({ select: { id: true } }).execute()`,
475+
`db.${modelName}.findOne({ ${pk.name}: '<value>', select: { id: true } }).execute()`,
476+
`db.${modelName}.create({ data: { ${editableFields.map((f) => `${f.name}: '<value>'`).join(', ')} }, select: { id: true } }).execute()`,
477+
`db.${modelName}.update({ where: { ${pk.name}: '<value>' }, data: { ${editableFields[0]?.name || 'field'}: '<new>' }, select: { id: true } }).execute()`,
478+
`db.${modelName}.delete({ where: { ${pk.name}: '<value>' } }).execute()`,
472479
],
473480
examples: [
474481
{
475482
description: `List all ${singularName} records`,
476483
code: [
477-
`const items = await db.${lcFirst(singularName)}.findMany({`,
484+
`const items = await db.${modelName}.findMany({`,
478485
` select: { ${pk.name}: true, ${editableFields[0]?.name || 'name'}: true }`,
479486
'}).execute();',
480487
],
481488
},
482489
{
483490
description: `Create a ${singularName}`,
484491
code: [
485-
`const item = await db.${lcFirst(singularName)}.create({`,
492+
`const item = await db.${modelName}.create({`,
486493
` data: { ${editableFields.map((f) => `${f.name}: 'value'`).join(', ')} },`,
487494
` select: { ${pk.name}: true }`,
488495
'}).execute();',
@@ -500,21 +507,20 @@ export function generateOrmSkills(
500507
? `{ ${op.args.map((a) => `${a.name}: '<value>'`).join(', ')} }`
501508
: '';
502509

510+
const opKebab = toKebabCase(op.name);
511+
const skillName = `orm-${targetName}-${opKebab}`;
512+
503513
files.push({
504-
fileName: `skills/${op.name}.md`,
514+
fileName: `${skillName}/SKILL.md`,
505515
content: buildSkillFile({
506-
name: `orm-${op.name}`,
516+
name: skillName,
507517
description: op.description || `Execute the ${op.name} ${op.kind}`,
508518
language: 'typescript',
509-
usage: [
510-
`db.${accessor}.${op.name}(${callArgs}).execute()`,
511-
],
519+
usage: [`db.${accessor}.${op.name}(${callArgs}).execute()`],
512520
examples: [
513521
{
514522
description: `Run ${op.name}`,
515-
code: [
516-
`const result = await db.${accessor}.${op.name}(${callArgs}).execute();`,
517-
],
523+
code: [`const result = await db.${accessor}.${op.name}(${callArgs}).execute();`],
518524
},
519525
],
520526
}),

0 commit comments

Comments
 (0)