@constructive-io/graphql-codegen 4.7.2 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
- import { buildSkillFile, formatArgType, getReadmeHeader, getReadmeFooter, gqlTypeToJsonSchemaType, } from './docs-utils';
1
+ import { toKebabCase } from 'komoji';
2
+ import { buildSkillFile, buildSkillReference, formatArgType, getReadmeHeader, getReadmeFooter, gqlTypeToJsonSchemaType, } from './docs-utils';
2
3
  import { getTableNames, getScalarFields, getPrimaryKeyInfo, getListQueryHookName, getSingleQueryHookName, getCreateMutationHookName, getUpdateMutationHookName, getDeleteMutationHookName, hasValidPrimaryKey, ucFirst, lcFirst, fieldTypeToTs, } from './utils';
3
4
  function getCustomHookName(op) {
4
5
  if (op.kind === 'query') {
@@ -371,8 +372,11 @@ export function getHooksMcpTools(tables, customOperations) {
371
372
  }
372
373
  return tools;
373
374
  }
374
- export function generateHooksSkills(tables, customOperations) {
375
+ export function generateHooksSkills(tables, customOperations, targetName) {
375
376
  const files = [];
377
+ const skillName = `hooks-${targetName}`;
378
+ const referenceNames = [];
379
+ // Generate reference files for each table
376
380
  for (const table of tables) {
377
381
  const { singularName, pluralName } = getTableNames(table);
378
382
  const pk = getPrimaryKeyInfo(table)[0];
@@ -380,10 +384,12 @@ export function generateHooksSkills(tables, customOperations) {
380
384
  const selectFields = scalarFields
381
385
  .map((f) => `${f.name}: true`)
382
386
  .join(', ');
387
+ const refName = toKebabCase(singularName);
388
+ referenceNames.push(refName);
383
389
  files.push({
384
- fileName: `skills/${lcFirst(singularName)}.md`,
385
- content: buildSkillFile({
386
- name: `hooks-${lcFirst(singularName)}`,
390
+ fileName: `${skillName}/references/${refName}.md`,
391
+ content: buildSkillReference({
392
+ title: singularName,
387
393
  description: table.description || `React Query hooks for ${table.name} data operations`,
388
394
  language: 'typescript',
389
395
  usage: [
@@ -423,15 +429,18 @@ export function generateHooksSkills(tables, customOperations) {
423
429
  }),
424
430
  });
425
431
  }
432
+ // Generate reference files for custom operations
426
433
  for (const op of customOperations) {
427
434
  const hookName = getCustomHookName(op);
428
435
  const callArgs = op.args.length > 0
429
436
  ? `{ ${op.args.map((a) => `${a.name}: '<value>'`).join(', ')} }`
430
437
  : '';
438
+ const refName = toKebabCase(op.name);
439
+ referenceNames.push(refName);
431
440
  files.push({
432
- fileName: `skills/${op.name}.md`,
433
- content: buildSkillFile({
434
- name: `hooks-${op.name}`,
441
+ fileName: `${skillName}/references/${refName}.md`,
442
+ content: buildSkillReference({
443
+ title: op.name,
435
444
  description: op.description ||
436
445
  `React Query ${op.kind} hook for ${op.name}`,
437
446
  language: 'typescript',
@@ -458,5 +467,36 @@ export function generateHooksSkills(tables, customOperations) {
458
467
  }),
459
468
  });
460
469
  }
470
+ // Generate the overview SKILL.md
471
+ const hookExamples = tables.slice(0, 3).map((t) => getListQueryHookName(t));
472
+ files.push({
473
+ fileName: `${skillName}/SKILL.md`,
474
+ content: buildSkillFile({
475
+ name: skillName,
476
+ description: `React Query hooks for the ${targetName} API — provides typed query and mutation hooks for ${tables.length} tables and ${customOperations.length} custom operations`,
477
+ language: 'typescript',
478
+ usage: [
479
+ `// Import hooks`,
480
+ `import { ${hookExamples[0] || 'useModelQuery'} } from './hooks';`,
481
+ '',
482
+ `// Query hooks: use<Model>Query, use<Model>sQuery`,
483
+ `// Mutation hooks: useCreate<Model>Mutation, useUpdate<Model>Mutation, useDelete<Model>Mutation`,
484
+ '',
485
+ `const { data, isLoading } = ${hookExamples[0] || 'useModelQuery'}({`,
486
+ ` selection: { fields: { id: true } },`,
487
+ `});`,
488
+ ],
489
+ examples: [
490
+ {
491
+ description: 'Query records',
492
+ code: [
493
+ `const { data, isLoading } = ${hookExamples[0] || 'useModelQuery'}({`,
494
+ ' selection: { fields: { id: true } },',
495
+ '});',
496
+ ],
497
+ },
498
+ ],
499
+ }, referenceNames),
500
+ });
461
501
  return files;
462
502
  }
@@ -3,4 +3,4 @@ import type { GeneratedDocFile, McpTool } from '../docs-utils';
3
3
  export declare function generateOrmReadme(tables: CleanTable[], customOperations: CleanOperation[]): GeneratedDocFile;
4
4
  export declare function generateOrmAgentsDocs(tables: CleanTable[], customOperations: CleanOperation[]): GeneratedDocFile;
5
5
  export declare function getOrmMcpTools(tables: CleanTable[], customOperations: CleanOperation[]): McpTool[];
6
- export declare function generateOrmSkills(tables: CleanTable[], customOperations: CleanOperation[]): GeneratedDocFile[];
6
+ export declare function generateOrmSkills(tables: CleanTable[], customOperations: CleanOperation[], targetName: string): GeneratedDocFile[];
@@ -1,4 +1,5 @@
1
- import { buildSkillFile, formatArgType, getEditableFields, getReadmeHeader, getReadmeFooter, gqlTypeToJsonSchemaType, } from '../docs-utils';
1
+ import { toKebabCase } from 'komoji';
2
+ import { buildSkillFile, buildSkillReference, formatArgType, getEditableFields, getReadmeHeader, getReadmeFooter, gqlTypeToJsonSchemaType, } from '../docs-utils';
2
3
  import { getScalarFields, getTableNames, getPrimaryKeyInfo, lcFirst, fieldTypeToTs, } from '../utils';
3
4
  export function generateOrmReadme(tables, customOperations) {
4
5
  const lines = [];
@@ -340,30 +341,36 @@ export function getOrmMcpTools(tables, customOperations) {
340
341
  }
341
342
  return tools;
342
343
  }
343
- export function generateOrmSkills(tables, customOperations) {
344
+ export function generateOrmSkills(tables, customOperations, targetName) {
344
345
  const files = [];
346
+ const skillName = `orm-${targetName}`;
347
+ const referenceNames = [];
348
+ // Generate reference files for each table
345
349
  for (const table of tables) {
346
350
  const { singularName } = getTableNames(table);
347
351
  const pk = getPrimaryKeyInfo(table)[0];
348
352
  const editableFields = getEditableFields(table);
353
+ const modelName = lcFirst(singularName);
354
+ const refName = toKebabCase(singularName);
355
+ referenceNames.push(refName);
349
356
  files.push({
350
- fileName: `skills/${lcFirst(singularName)}.md`,
351
- content: buildSkillFile({
352
- name: `orm-${lcFirst(singularName)}`,
357
+ fileName: `${skillName}/references/${refName}.md`,
358
+ content: buildSkillReference({
359
+ title: singularName,
353
360
  description: table.description || `ORM operations for ${table.name} records`,
354
361
  language: 'typescript',
355
362
  usage: [
356
- `db.${lcFirst(singularName)}.findMany({ select: { id: true } }).execute()`,
357
- `db.${lcFirst(singularName)}.findOne({ ${pk.name}: '<value>', select: { id: true } }).execute()`,
358
- `db.${lcFirst(singularName)}.create({ data: { ${editableFields.map((f) => `${f.name}: '<value>'`).join(', ')} }, select: { id: true } }).execute()`,
359
- `db.${lcFirst(singularName)}.update({ where: { ${pk.name}: '<value>' }, data: { ${editableFields[0]?.name || 'field'}: '<new>' }, select: { id: true } }).execute()`,
360
- `db.${lcFirst(singularName)}.delete({ where: { ${pk.name}: '<value>' } }).execute()`,
363
+ `db.${modelName}.findMany({ select: { id: true } }).execute()`,
364
+ `db.${modelName}.findOne({ ${pk.name}: '<value>', select: { id: true } }).execute()`,
365
+ `db.${modelName}.create({ data: { ${editableFields.map((f) => `${f.name}: '<value>'`).join(', ')} }, select: { id: true } }).execute()`,
366
+ `db.${modelName}.update({ where: { ${pk.name}: '<value>' }, data: { ${editableFields[0]?.name || 'field'}: '<new>' }, select: { id: true } }).execute()`,
367
+ `db.${modelName}.delete({ where: { ${pk.name}: '<value>' } }).execute()`,
361
368
  ],
362
369
  examples: [
363
370
  {
364
371
  description: `List all ${singularName} records`,
365
372
  code: [
366
- `const items = await db.${lcFirst(singularName)}.findMany({`,
373
+ `const items = await db.${modelName}.findMany({`,
367
374
  ` select: { ${pk.name}: true, ${editableFields[0]?.name || 'name'}: true }`,
368
375
  '}).execute();',
369
376
  ],
@@ -371,7 +378,7 @@ export function generateOrmSkills(tables, customOperations) {
371
378
  {
372
379
  description: `Create a ${singularName}`,
373
380
  code: [
374
- `const item = await db.${lcFirst(singularName)}.create({`,
381
+ `const item = await db.${modelName}.create({`,
375
382
  ` data: { ${editableFields.map((f) => `${f.name}: 'value'`).join(', ')} },`,
376
383
  ` select: { ${pk.name}: true }`,
377
384
  '}).execute();',
@@ -381,30 +388,60 @@ export function generateOrmSkills(tables, customOperations) {
381
388
  }),
382
389
  });
383
390
  }
391
+ // Generate reference files for custom operations
384
392
  for (const op of customOperations) {
385
393
  const accessor = op.kind === 'query' ? 'query' : 'mutation';
386
394
  const callArgs = op.args.length > 0
387
395
  ? `{ ${op.args.map((a) => `${a.name}: '<value>'`).join(', ')} }`
388
396
  : '';
397
+ const refName = toKebabCase(op.name);
398
+ referenceNames.push(refName);
389
399
  files.push({
390
- fileName: `skills/${op.name}.md`,
391
- content: buildSkillFile({
392
- name: `orm-${op.name}`,
400
+ fileName: `${skillName}/references/${refName}.md`,
401
+ content: buildSkillReference({
402
+ title: op.name,
393
403
  description: op.description || `Execute the ${op.name} ${op.kind}`,
394
404
  language: 'typescript',
395
- usage: [
396
- `db.${accessor}.${op.name}(${callArgs}).execute()`,
397
- ],
405
+ usage: [`db.${accessor}.${op.name}(${callArgs}).execute()`],
398
406
  examples: [
399
407
  {
400
408
  description: `Run ${op.name}`,
401
- code: [
402
- `const result = await db.${accessor}.${op.name}(${callArgs}).execute();`,
403
- ],
409
+ code: [`const result = await db.${accessor}.${op.name}(${callArgs}).execute();`],
404
410
  },
405
411
  ],
406
412
  }),
407
413
  });
408
414
  }
415
+ // Generate the overview SKILL.md
416
+ const tableNames = tables.map((t) => lcFirst(getTableNames(t).singularName));
417
+ files.push({
418
+ fileName: `${skillName}/SKILL.md`,
419
+ content: buildSkillFile({
420
+ name: skillName,
421
+ description: `ORM client for the ${targetName} API — provides typed CRUD operations for ${tables.length} tables and ${customOperations.length} custom operations`,
422
+ language: 'typescript',
423
+ usage: [
424
+ `// Import the ORM client`,
425
+ `import { db } from './orm';`,
426
+ '',
427
+ `// Available models: ${tableNames.slice(0, 8).join(', ')}${tableNames.length > 8 ? ', ...' : ''}`,
428
+ `db.<model>.findMany({ select: { id: true } }).execute()`,
429
+ `db.<model>.findOne({ id: '<value>', select: { id: true } }).execute()`,
430
+ `db.<model>.create({ data: { ... }, select: { id: true } }).execute()`,
431
+ `db.<model>.update({ where: { id: '<value>' }, data: { ... }, select: { id: true } }).execute()`,
432
+ `db.<model>.delete({ where: { id: '<value>' } }).execute()`,
433
+ ],
434
+ examples: [
435
+ {
436
+ description: 'Query records',
437
+ code: [
438
+ `const items = await db.${tableNames[0] || 'model'}.findMany({`,
439
+ ' select: { id: true }',
440
+ '}).execute();',
441
+ ],
442
+ },
443
+ ],
444
+ }, referenceNames),
445
+ });
409
446
  return files;
410
447
  }
@@ -33,6 +33,11 @@ export interface GenerateResult {
33
33
  */
34
34
  export interface GenerateInternalOptions {
35
35
  skipCli?: boolean;
36
+ /**
37
+ * Internal-only name for the target when generating skills.
38
+ * Used by generateMulti() so skill names are stable and composable.
39
+ */
40
+ targetName?: string;
36
41
  }
37
42
  export declare function generate(options?: GenerateOptions, internalOptions?: GenerateInternalOptions): Promise<GenerateResult>;
38
43
  export declare function expandApiNamesToMultiTarget(config: GraphQLSDKConfigTarget): Record<string, GraphQLSDKConfigTarget> | null;
@@ -24,6 +24,18 @@ import { generateTargetReadme, generateCombinedMcpConfig, generateRootRootReadme
24
24
  import { createSchemaSource, validateSourceOptions } from './introspect';
25
25
  import { writeGeneratedFiles } from './output';
26
26
  import { runCodegenPipeline, validateTablesFound } from './pipeline';
27
+ import { findWorkspaceRoot } from './workspace';
28
+ function resolveSkillsOutputDir(config, outputRoot) {
29
+ const workspaceRoot = findWorkspaceRoot(path.resolve(outputRoot)) ??
30
+ findWorkspaceRoot(process.cwd()) ??
31
+ process.cwd();
32
+ if (config.skillsPath) {
33
+ return path.isAbsolute(config.skillsPath)
34
+ ? config.skillsPath
35
+ : path.resolve(workspaceRoot, config.skillsPath);
36
+ }
37
+ return path.resolve(workspaceRoot, 'skills');
38
+ }
27
39
  export async function generate(options = {}, internalOptions) {
28
40
  // Apply defaults to get resolved config
29
41
  const config = getConfigOptions(options);
@@ -219,6 +231,8 @@ export async function generate(options = {}, internalOptions) {
219
231
  ...(customOperations.mutations ?? []),
220
232
  ];
221
233
  const allMcpTools = [];
234
+ const targetName = internalOptions?.targetName ?? 'default';
235
+ const skillsToWrite = [];
222
236
  if (runOrm) {
223
237
  if (docsConfig.readme) {
224
238
  const readme = generateOrmReadme(tables, allCustomOps);
@@ -232,8 +246,8 @@ export async function generate(options = {}, internalOptions) {
232
246
  allMcpTools.push(...getOrmMcpTools(tables, allCustomOps));
233
247
  }
234
248
  if (docsConfig.skills) {
235
- for (const skill of generateOrmSkills(tables, allCustomOps)) {
236
- filesToWrite.push({ path: path.posix.join('orm', skill.fileName), content: skill.content });
249
+ for (const skill of generateOrmSkills(tables, allCustomOps, targetName)) {
250
+ skillsToWrite.push({ path: skill.fileName, content: skill.content });
237
251
  }
238
252
  }
239
253
  }
@@ -250,8 +264,8 @@ export async function generate(options = {}, internalOptions) {
250
264
  allMcpTools.push(...getHooksMcpTools(tables, allCustomOps));
251
265
  }
252
266
  if (docsConfig.skills) {
253
- for (const skill of generateHooksSkills(tables, allCustomOps)) {
254
- filesToWrite.push({ path: path.posix.join('hooks', skill.fileName), content: skill.content });
267
+ for (const skill of generateHooksSkills(tables, allCustomOps, targetName)) {
268
+ skillsToWrite.push({ path: skill.fileName, content: skill.content });
255
269
  }
256
270
  }
257
271
  }
@@ -271,8 +285,8 @@ export async function generate(options = {}, internalOptions) {
271
285
  allMcpTools.push(...getCliMcpTools(tables, allCustomOps, toolName));
272
286
  }
273
287
  if (docsConfig.skills) {
274
- for (const skill of generateCliSkills(tables, allCustomOps, toolName)) {
275
- filesToWrite.push({ path: path.posix.join('cli', skill.fileName), content: skill.content });
288
+ for (const skill of generateCliSkills(tables, allCustomOps, toolName, targetName)) {
289
+ skillsToWrite.push({ path: skill.fileName, content: skill.content });
276
290
  }
277
291
  }
278
292
  }
@@ -310,6 +324,21 @@ export async function generate(options = {}, internalOptions) {
310
324
  };
311
325
  }
312
326
  allFilesWritten.push(...(writeResult.filesWritten ?? []));
327
+ if (skillsToWrite.length > 0) {
328
+ const skillsOutputDir = resolveSkillsOutputDir(config, outputRoot);
329
+ const skillsWriteResult = await writeGeneratedFiles(skillsToWrite, skillsOutputDir, [], {
330
+ pruneStaleFiles: false,
331
+ });
332
+ if (!skillsWriteResult.success) {
333
+ return {
334
+ success: false,
335
+ message: `Failed to write generated skill files: ${skillsWriteResult.errors?.join(', ')}`,
336
+ output: skillsOutputDir,
337
+ errors: skillsWriteResult.errors,
338
+ };
339
+ }
340
+ allFilesWritten.push(...(skillsWriteResult.filesWritten ?? []));
341
+ }
313
342
  }
314
343
  const generators = [
315
344
  runReactQuery && 'React Query',
@@ -488,7 +517,7 @@ export async function generateMulti(options) {
488
517
  dryRun,
489
518
  schemaOnly,
490
519
  schemaOnlyFilename: schemaOnly ? `${name}.graphql` : undefined,
491
- }, useUnifiedCli ? { skipCli: true } : undefined);
520
+ }, useUnifiedCli ? { skipCli: true, targetName: name } : { targetName: name });
492
521
  results.push({ name, result });
493
522
  if (!result.success) {
494
523
  hasError = true;
@@ -571,17 +600,24 @@ export async function generateMulti(options) {
571
600
  if (docsConfig.mcp) {
572
601
  allMcpTools.push(...getMultiTargetCliMcpTools(docsInput));
573
602
  }
574
- if (docsConfig.skills) {
575
- for (const skill of generateMultiTargetSkills(docsInput)) {
576
- cliFilesToWrite.push({ path: path.posix.join('cli', skill.fileName), content: skill.content });
577
- }
578
- }
579
603
  if (docsConfig.mcp && allMcpTools.length > 0) {
580
604
  const mcpFile = generateCombinedMcpConfig(allMcpTools, toolName);
581
605
  cliFilesToWrite.push({ path: path.posix.join('cli', mcpFile.fileName), content: mcpFile.content });
582
606
  }
583
607
  const { writeGeneratedFiles: writeFiles } = await import('./output');
584
608
  await writeFiles(cliFilesToWrite, '.', [], { pruneStaleFiles: false });
609
+ if (docsConfig.skills) {
610
+ const cliSkillsToWrite = generateMultiTargetSkills(docsInput).map((skill) => ({
611
+ path: skill.fileName,
612
+ content: skill.content,
613
+ }));
614
+ const firstTargetResolved = getConfigOptions({
615
+ ...(firstTargetConfig ?? {}),
616
+ ...(cliOverrides ?? {}),
617
+ });
618
+ const skillsOutputDir = resolveSkillsOutputDir(firstTargetResolved, firstTargetResolved.output);
619
+ await writeFiles(cliSkillsToWrite, skillsOutputDir, [], { pruneStaleFiles: false });
620
+ }
585
621
  }
586
622
  // Generate root-root README if multi-target
587
623
  if (names.length > 1 && targetInfos.length > 0 && !dryRun) {
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Find the workspace root directory.
3
+ *
4
+ * Search order:
5
+ * 1. pnpm-workspace.yaml (pnpm workspaces)
6
+ * 2. lerna.json (lerna workspaces)
7
+ * 3. package.json with "workspaces" field (npm/yarn workspaces)
8
+ * 4. Nearest package.json (fallback for non-workspace projects)
9
+ *
10
+ * @param cwd - Starting directory to search from (defaults to process.cwd())
11
+ * @returns The workspace root path, or null if nothing found
12
+ */
13
+ export declare function findWorkspaceRoot(cwd?: string): string | null;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Workspace detection utilities
3
+ *
4
+ * Finds the root of a workspace by walking up directories looking for
5
+ * workspace markers (pnpm-workspace.yaml, lerna.json, package.json with workspaces).
6
+ * Falls back to the nearest package.json directory.
7
+ *
8
+ * Inspired by @pgpmjs/env walkUp / resolvePnpmWorkspace patterns.
9
+ */
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { dirname, resolve, parse as parsePath } from 'node:path';
12
+ /**
13
+ * Walk up directories from startDir looking for a file.
14
+ * Returns the directory containing the file, or null if not found.
15
+ */
16
+ function walkUp(startDir, filename) {
17
+ let currentDir = resolve(startDir);
18
+ while (currentDir) {
19
+ const targetPath = resolve(currentDir, filename);
20
+ if (existsSync(targetPath)) {
21
+ return currentDir;
22
+ }
23
+ const parentDir = dirname(currentDir);
24
+ if (parentDir === currentDir) {
25
+ break;
26
+ }
27
+ currentDir = parentDir;
28
+ }
29
+ return null;
30
+ }
31
+ /**
32
+ * Find the first pnpm workspace root by looking for pnpm-workspace.yaml
33
+ */
34
+ function findPnpmWorkspace(cwd) {
35
+ return walkUp(cwd, 'pnpm-workspace.yaml');
36
+ }
37
+ /**
38
+ * Find the first lerna workspace root by looking for lerna.json
39
+ */
40
+ function findLernaWorkspace(cwd) {
41
+ return walkUp(cwd, 'lerna.json');
42
+ }
43
+ /**
44
+ * Find the first npm/yarn workspace root by looking for package.json with workspaces field
45
+ */
46
+ function findNpmWorkspace(cwd) {
47
+ let currentDir = resolve(cwd);
48
+ const root = parsePath(currentDir).root;
49
+ while (currentDir !== root) {
50
+ const packageJsonPath = resolve(currentDir, 'package.json');
51
+ if (existsSync(packageJsonPath)) {
52
+ try {
53
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
54
+ if (packageJson.workspaces) {
55
+ return currentDir;
56
+ }
57
+ }
58
+ catch {
59
+ // Ignore JSON parse errors
60
+ }
61
+ }
62
+ currentDir = dirname(currentDir);
63
+ }
64
+ return null;
65
+ }
66
+ /**
67
+ * Find the nearest package.json directory (fallback)
68
+ */
69
+ function findPackageRoot(cwd) {
70
+ return walkUp(cwd, 'package.json');
71
+ }
72
+ /**
73
+ * Find the workspace root directory.
74
+ *
75
+ * Search order:
76
+ * 1. pnpm-workspace.yaml (pnpm workspaces)
77
+ * 2. lerna.json (lerna workspaces)
78
+ * 3. package.json with "workspaces" field (npm/yarn workspaces)
79
+ * 4. Nearest package.json (fallback for non-workspace projects)
80
+ *
81
+ * @param cwd - Starting directory to search from (defaults to process.cwd())
82
+ * @returns The workspace root path, or null if nothing found
83
+ */
84
+ export function findWorkspaceRoot(cwd = process.cwd()) {
85
+ return (findPnpmWorkspace(cwd) ??
86
+ findLernaWorkspace(cwd) ??
87
+ findNpmWorkspace(cwd) ??
88
+ findPackageRoot(cwd));
89
+ }
@@ -138,9 +138,9 @@ export interface DocsConfig {
138
138
  */
139
139
  mcp?: boolean;
140
140
  /**
141
- * Generate skills/ directory — per-command .md skill files
142
- * Each command gets its own skill file with description, usage, and examples
143
- * Compatible with Devin and similar agent skill systems
141
+ * Generate skills/ directory — per-entity SKILL.md files with YAML frontmatter.
142
+ * Skills are written to the workspace root skills/ directory (not nested in output).
143
+ * Uses composable naming: orm-{target}-{entity}, hooks-{target}-{entity}, cli-{target}-{entity}.
144
144
  * @default false
145
145
  */
146
146
  skills?: boolean;
@@ -328,9 +328,17 @@ export interface GraphQLSDKConfigTarget {
328
328
  * Controls which doc formats are generated alongside code for each generator target.
329
329
  * Applied globally to all enabled generators (ORM, React Query, CLI).
330
330
  * Set to `true` to enable all formats, or configure individually.
331
- * @default { readme: true, agents: true, mcp: false, skills: false }
331
+ * @default { readme: true, agents: true, mcp: false }
332
332
  */
333
333
  docs?: DocsConfig | boolean;
334
+ /**
335
+ * Custom path for generated skill files.
336
+ * When set, skills are written to this directory.
337
+ * When undefined (default), skills are written to {workspaceRoot}/skills/
338
+ * where workspaceRoot is auto-detected by walking up from the output directory
339
+ * looking for pnpm-workspace.yaml, lerna.json, or package.json with workspaces.
340
+ */
341
+ skillsPath?: string;
334
342
  /**
335
343
  * Query key generation configuration
336
344
  * Controls how query keys are structured for cache management
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/graphql-codegen",
3
- "version": "4.7.2",
3
+ "version": "4.8.0",
4
4
  "description": "GraphQL SDK generator for Constructive databases with React Query hooks",
5
5
  "keywords": [
6
6
  "graphql",
@@ -54,28 +54,28 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@0no-co/graphql.web": "^1.1.2",
57
- "@babel/generator": "^7.28.6",
58
- "@babel/types": "^7.28.6",
59
- "@constructive-io/graphql-query": "^3.3.2",
57
+ "@babel/generator": "^7.29.1",
58
+ "@babel/types": "^7.29.0",
59
+ "@constructive-io/graphql-query": "^3.3.4",
60
60
  "@constructive-io/graphql-types": "^3.1.2",
61
61
  "@inquirerer/utils": "^3.3.1",
62
- "@pgpmjs/core": "^6.4.1",
63
- "ajv": "^8.17.1",
62
+ "@pgpmjs/core": "^6.4.2",
63
+ "ajv": "^8.18.0",
64
64
  "deepmerge": "^4.3.1",
65
- "find-and-require-package-json": "^0.9.0",
66
- "gql-ast": "^3.1.0",
67
- "graphile-schema": "^1.3.2",
68
- "graphql": "^16.9.0",
69
- "inflekt": "^0.3.1",
65
+ "find-and-require-package-json": "^0.9.1",
66
+ "gql-ast": "^3.1.1",
67
+ "graphile-schema": "^1.3.4",
68
+ "graphql": "^16.13.0",
69
+ "inflekt": "^0.3.3",
70
70
  "inquirerer": "^4.5.2",
71
71
  "jiti": "^2.6.1",
72
- "komoji": "^0.8.0",
73
- "oxfmt": "^0.26.0",
74
- "pg-cache": "^3.1.0",
72
+ "komoji": "^0.8.1",
73
+ "oxfmt": "^0.36.0",
74
+ "pg-cache": "^3.1.1",
75
75
  "pg-env": "^1.5.0",
76
- "pgsql-client": "^3.3.1",
77
- "pgsql-seed": "^2.3.1",
78
- "undici": "^7.19.0"
76
+ "pgsql-client": "^3.3.2",
77
+ "pgsql-seed": "^2.3.2",
78
+ "undici": "^7.22.0"
79
79
  },
80
80
  "peerDependencies": {
81
81
  "@tanstack/react-query": "^5.0.0",
@@ -90,16 +90,16 @@
90
90
  }
91
91
  },
92
92
  "devDependencies": {
93
- "@tanstack/react-query": "^5.90.19",
93
+ "@tanstack/react-query": "^5.90.21",
94
94
  "@types/babel__generator": "^7.27.0",
95
95
  "@types/jest": "^30.0.0",
96
- "@types/node": "^20.19.27",
97
- "@types/react": "^19.2.8",
96
+ "@types/node": "^22.19.11",
97
+ "@types/react": "^19.2.14",
98
98
  "jest": "^30.2.0",
99
- "react": "^19.2.3",
99
+ "react": "^19.2.4",
100
100
  "ts-jest": "^29.2.5",
101
101
  "tsx": "^4.21.0",
102
102
  "typescript": "^5.9.3"
103
103
  },
104
- "gitHead": "ff397a83e345bbefcd999079c7568c21de8dd3ea"
104
+ "gitHead": "d0d7d3916b70c8d960bc13e40ac85d73ea869224"
105
105
  }
package/types/config.d.ts CHANGED
@@ -138,9 +138,9 @@ export interface DocsConfig {
138
138
  */
139
139
  mcp?: boolean;
140
140
  /**
141
- * Generate skills/ directory — per-command .md skill files
142
- * Each command gets its own skill file with description, usage, and examples
143
- * Compatible with Devin and similar agent skill systems
141
+ * Generate skills/ directory — per-entity SKILL.md files with YAML frontmatter.
142
+ * Skills are written to the workspace root skills/ directory (not nested in output).
143
+ * Uses composable naming: orm-{target}-{entity}, hooks-{target}-{entity}, cli-{target}-{entity}.
144
144
  * @default false
145
145
  */
146
146
  skills?: boolean;
@@ -328,9 +328,17 @@ export interface GraphQLSDKConfigTarget {
328
328
  * Controls which doc formats are generated alongside code for each generator target.
329
329
  * Applied globally to all enabled generators (ORM, React Query, CLI).
330
330
  * Set to `true` to enable all formats, or configure individually.
331
- * @default { readme: true, agents: true, mcp: false, skills: false }
331
+ * @default { readme: true, agents: true, mcp: false }
332
332
  */
333
333
  docs?: DocsConfig | boolean;
334
+ /**
335
+ * Custom path for generated skill files.
336
+ * When set, skills are written to this directory.
337
+ * When undefined (default), skills are written to {workspaceRoot}/skills/
338
+ * where workspaceRoot is auto-detected by walking up from the output directory
339
+ * looking for pnpm-workspace.yaml, lerna.json, or package.json with workspaces.
340
+ */
341
+ skillsPath?: string;
334
342
  /**
335
343
  * Query key generation configuration
336
344
  * Controls how query keys are structured for cache management