swallowkit 2.0.0-alpha.3 → 2.0.0-alpha.4

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.
Files changed (133) hide show
  1. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  2. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  3. package/dist/cli/commands/configure-mcp.js +42 -0
  4. package/dist/cli/commands/configure-mcp.js.map +1 -0
  5. package/dist/cli/commands/init.d.ts +4 -1
  6. package/dist/cli/commands/init.d.ts.map +1 -1
  7. package/dist/cli/commands/init.js +16 -4
  8. package/dist/cli/commands/init.js.map +1 -1
  9. package/dist/cli/commands/migrations.d.ts.map +1 -1
  10. package/dist/cli/commands/migrations.js +25 -0
  11. package/dist/cli/commands/migrations.js.map +1 -1
  12. package/dist/cli/commands/provision.d.ts +2 -0
  13. package/dist/cli/commands/provision.d.ts.map +1 -1
  14. package/dist/cli/commands/provision.js +45 -16
  15. package/dist/cli/commands/provision.js.map +1 -1
  16. package/dist/cli/commands/scaffold.d.ts +4 -0
  17. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  18. package/dist/cli/commands/scaffold.js +40 -2
  19. package/dist/cli/commands/scaffold.js.map +1 -1
  20. package/dist/cli/index.d.ts +2 -0
  21. package/dist/cli/index.d.ts.map +1 -1
  22. package/dist/cli/index.js +27 -2
  23. package/dist/cli/index.js.map +1 -1
  24. package/dist/core/operations/model-migration.d.ts +22 -0
  25. package/dist/core/operations/model-migration.d.ts.map +1 -0
  26. package/dist/core/operations/model-migration.js +36 -0
  27. package/dist/core/operations/model-migration.js.map +1 -0
  28. package/dist/core/operations/provision-operations.d.ts +19 -1
  29. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  30. package/dist/core/operations/provision-operations.js +62 -35
  31. package/dist/core/operations/provision-operations.js.map +1 -1
  32. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  33. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  34. package/dist/core/operations/scaffold-apply.js +1 -0
  35. package/dist/core/operations/scaffold-apply.js.map +1 -1
  36. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  37. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  38. package/dist/core/operations/scaffold-machine.js.map +1 -1
  39. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  40. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  41. package/dist/core/operations/scaffold-plan.js +3 -1
  42. package/dist/core/operations/scaffold-plan.js.map +1 -1
  43. package/dist/core/project/boundaries.d.ts +5 -2
  44. package/dist/core/project/boundaries.d.ts.map +1 -1
  45. package/dist/core/project/boundaries.js +49 -18
  46. package/dist/core/project/boundaries.js.map +1 -1
  47. package/dist/core/project/capabilities.d.ts +1 -0
  48. package/dist/core/project/capabilities.d.ts.map +1 -1
  49. package/dist/core/project/capabilities.js +20 -2
  50. package/dist/core/project/capabilities.js.map +1 -1
  51. package/dist/core/project/mcp-config.d.ts +26 -0
  52. package/dist/core/project/mcp-config.d.ts.map +1 -0
  53. package/dist/core/project/mcp-config.js +117 -0
  54. package/dist/core/project/mcp-config.js.map +1 -0
  55. package/dist/core/project/workflows.d.ts.map +1 -1
  56. package/dist/core/project/workflows.js +9 -1
  57. package/dist/core/project/workflows.js.map +1 -1
  58. package/dist/core/scaffold/model-parser.d.ts +23 -0
  59. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  60. package/dist/core/scaffold/model-parser.js +407 -137
  61. package/dist/core/scaffold/model-parser.js.map +1 -1
  62. package/dist/core/scaffold/native-schema-generator.d.ts +11 -0
  63. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  64. package/dist/core/scaffold/native-schema-generator.js +273 -75
  65. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  66. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  67. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  68. package/dist/core/scaffold/openapi-generator.js +53 -3
  69. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  70. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  71. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  72. package/dist/core/scaffold/ui-generator.js +68 -7
  73. package/dist/core/scaffold/ui-generator.js.map +1 -1
  74. package/dist/core/verify/index.d.ts.map +1 -1
  75. package/dist/core/verify/index.js +24 -1
  76. package/dist/core/verify/index.js.map +1 -1
  77. package/dist/machine/index.d.ts.map +1 -1
  78. package/dist/machine/index.js +39 -15
  79. package/dist/machine/index.js.map +1 -1
  80. package/dist/mcp/index.d.ts +3 -2
  81. package/dist/mcp/index.d.ts.map +1 -1
  82. package/dist/mcp/index.js +137 -13
  83. package/dist/mcp/index.js.map +1 -1
  84. package/dist/mcp/version-selection.d.ts +55 -0
  85. package/dist/mcp/version-selection.d.ts.map +1 -0
  86. package/dist/mcp/version-selection.js +152 -0
  87. package/dist/mcp/version-selection.js.map +1 -0
  88. package/dist/utils/azure-cli.d.ts +44 -0
  89. package/dist/utils/azure-cli.d.ts.map +1 -0
  90. package/dist/utils/azure-cli.js +164 -0
  91. package/dist/utils/azure-cli.js.map +1 -0
  92. package/dist/utils/semver.d.ts +14 -0
  93. package/dist/utils/semver.d.ts.map +1 -0
  94. package/dist/utils/semver.js +61 -0
  95. package/dist/utils/semver.js.map +1 -0
  96. package/package.json +1 -1
  97. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  98. package/src/__tests__/agent-loop-phase34.test.ts +29 -0
  99. package/src/__tests__/azure-cli.test.ts +199 -0
  100. package/src/__tests__/boundaries.test.ts +42 -0
  101. package/src/__tests__/csharp-schema.test.ts +261 -0
  102. package/src/__tests__/mcp-version.test.ts +262 -0
  103. package/src/__tests__/mcp.test.ts +49 -3
  104. package/src/__tests__/plan-apply.test.ts +59 -0
  105. package/src/__tests__/schema-analysis.test.ts +329 -0
  106. package/src/cli/commands/configure-mcp.ts +54 -0
  107. package/src/cli/commands/init.ts +19 -4
  108. package/src/cli/commands/migrations.ts +22 -0
  109. package/src/cli/commands/provision.ts +53 -17
  110. package/src/cli/commands/scaffold.ts +40 -2
  111. package/src/cli/index.ts +26 -3
  112. package/src/core/operations/model-migration.ts +52 -0
  113. package/src/core/operations/provision-operations.ts +86 -42
  114. package/src/core/operations/scaffold-apply.ts +2 -0
  115. package/src/core/operations/scaffold-machine.ts +1 -0
  116. package/src/core/operations/scaffold-plan.ts +5 -1
  117. package/src/core/project/agent-templates/AGENTS.md +39 -3
  118. package/src/core/project/agent-templates/CLAUDE.md +3 -1
  119. package/src/core/project/agent-templates/copilot-instructions.md +3 -1
  120. package/src/core/project/boundaries.ts +60 -18
  121. package/src/core/project/capabilities.ts +22 -2
  122. package/src/core/project/mcp-config.ts +84 -0
  123. package/src/core/project/workflows.ts +9 -1
  124. package/src/core/scaffold/model-parser.ts +413 -129
  125. package/src/core/scaffold/native-schema-generator.ts +297 -78
  126. package/src/core/scaffold/openapi-generator.ts +63 -5
  127. package/src/core/scaffold/ui-generator.ts +70 -8
  128. package/src/core/verify/index.ts +23 -1
  129. package/src/machine/index.ts +42 -18
  130. package/src/mcp/index.ts +156 -14
  131. package/src/mcp/version-selection.ts +163 -0
  132. package/src/utils/azure-cli.ts +154 -0
  133. package/src/utils/semver.ts +59 -0
@@ -3,7 +3,7 @@
3
3
  * CRUD 画面(一覧、詳細、新規作成、編集)を生成する
4
4
  */
5
5
 
6
- import { ModelInfo, toCamelCase, toKebabCase } from "./model-parser";
6
+ import { FieldInfo, ModelInfo, toCamelCase, toKebabCase } from "./model-parser";
7
7
  import { ModelAuthPolicy } from "../../types";
8
8
 
9
9
  /** auth が有効かつ authPolicy が存在する場合に渡されるオプション */
@@ -18,6 +18,21 @@ export interface UIAuthOptions {
18
18
  /**
19
19
  * 認証付きCRUDルートを、ページ側のuseAuthと同じAuthProviderで囲むレイアウトを生成する。
20
20
  */
21
+ /** Lookup state and fetches are per referenced model; several fields may reference the same model. */
22
+ function uniqueByReferencedModel(fields: FieldInfo[]): FieldInfo[] {
23
+ const seen = new Set<string>();
24
+ return fields.filter((field) => {
25
+ if (!field.referencedModel || seen.has(field.referencedModel)) return false;
26
+ seen.add(field.referencedModel);
27
+ return true;
28
+ });
29
+ }
30
+
31
+ /** Untyped or inline z.object fields (not references to another model) are edited and shown as JSON. */
32
+ function isJsonField(f: FieldInfo): boolean {
33
+ return f.type === "object" && !f.isNestedSchema;
34
+ }
35
+
21
36
  export function generateAuthLayout(model: ModelInfo, authOptions?: UIAuthOptions): string | undefined {
22
37
  if (!authOptions) return undefined;
23
38
 
@@ -59,14 +74,14 @@ export function generateListPage(model: ModelInfo, sharedPackageName: string, au
59
74
  const hasForeignKeys = foreignKeyFields.length > 0;
60
75
 
61
76
  // 外部キー用のstate定義を生成
62
- const foreignKeyStates = foreignKeyFields.map(f => {
77
+ const foreignKeyStates = uniqueByReferencedModel(foreignKeyFields).map(f => {
63
78
  const refModel = f.referencedModel!;
64
79
  const refModelCamel = toCamelCase(refModel);
65
80
  return ` const [${refModelCamel}Map, set${refModel}Map] = useState<Record<string, string>>({});`;
66
81
  }).join('\n');
67
82
 
68
83
  // 外部キーデータのフェッチロジックを生成
69
- const foreignKeyFetches = foreignKeyFields.map(f => {
84
+ const foreignKeyFetches = uniqueByReferencedModel(foreignKeyFields).map(f => {
70
85
  const refModel = f.referencedModel!;
71
86
  const refModelCamel = toCamelCase(refModel);
72
87
  return ` fetch('/api/${refModelCamel}')
@@ -234,6 +249,10 @@ ${displayFields.map(f => {
234
249
  return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
235
250
  {${refModelCamel}Map[item.${f.name}] || item.${f.name}}
236
251
  </td>`;
252
+ } else if (isJsonField(f)) {
253
+ return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
254
+ {item.${f.name} === undefined ? '-' : JSON.stringify(item.${f.name})}
255
+ </td>`;
237
256
  } else {
238
257
  return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
239
258
  {String(item.${f.name})}
@@ -305,14 +324,14 @@ export function generateDetailPage(model: ModelInfo, sharedPackageName: string,
305
324
  const hasForeignKeys = foreignKeyFields.length > 0;
306
325
 
307
326
  // 外部キー用のstate定義を生成
308
- const foreignKeyStates = foreignKeyFields.map(f => {
327
+ const foreignKeyStates = uniqueByReferencedModel(foreignKeyFields).map(f => {
309
328
  const refModel = f.referencedModel!;
310
329
  const refModelCamel = toCamelCase(refModel);
311
330
  return ` const [${refModelCamel}Map, set${refModel}Map] = useState<Record<string, string>>({});`;
312
331
  }).join('\n');
313
332
 
314
333
  // 外部キーデータのフェッチロジックを生成
315
- const foreignKeyFetches = foreignKeyFields.map(f => {
334
+ const foreignKeyFetches = uniqueByReferencedModel(foreignKeyFields).map(f => {
316
335
  const refModel = f.referencedModel!;
317
336
  const refModelCamel = toCamelCase(refModel);
318
337
  return ` fetch('/api/${refModelCamel}')
@@ -476,6 +495,11 @@ ${model.fields.map(f => {
476
495
  <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${refModel}</dt>
477
496
  <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{${refModelCamel}Map[${modelCamel}.${f.name}] || ${modelCamel}.${f.name}}</dd>
478
497
  </div>`;
498
+ } else if (isJsonField(f)) {
499
+ return ` <div>
500
+ <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${f.name}</dt>
501
+ <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100"><pre className="whitespace-pre-wrap">{${modelCamel}.${f.name} === undefined ? '-' : JSON.stringify(${modelCamel}.${f.name}, null, 2)}</pre></dd>
502
+ </div>`;
479
503
  } else {
480
504
  return ` <div>
481
505
  <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${f.name}</dt>
@@ -573,7 +597,7 @@ export default function ${modelName}Form({ initialData, isEdit = false }: ${mode
573
597
  const router = useRouter();
574
598
  const [loading, setLoading] = useState(false);
575
599
  const [errors, setErrors] = useState<Record<string, string>>({});
576
- ${hasForignKeys ? foreignKeyFields.map(f => ` const [${toCamelCase(f.referencedModel!)}Options, set${f.referencedModel}Options] = useState<Array<{ id: string; name: string }>>([]);`).join('\n') : ''}
600
+ ${hasForignKeys ? uniqueByReferencedModel(foreignKeyFields).map(f => ` const [${toCamelCase(f.referencedModel!)}Options, set${f.referencedModel}Options] = useState<Array<{ id: string; name: string }>>([]);`).join('\n') : ''}
577
601
  ${hasNestedSchemas ? nestedSchemaStates : ''}
578
602
 
579
603
  const [formData, setFormData] = useState({
@@ -587,6 +611,11 @@ ${formFields.map(f => {
587
611
  }
588
612
  }
589
613
 
614
+ // インラインオブジェクトは JSON テキストとして編集する
615
+ if (isJsonField(f)) {
616
+ return ` ${f.name}: initialData?.${f.name} !== undefined ? JSON.stringify(initialData.${f.name}, null, 2) : '',`;
617
+ }
618
+
590
619
  let defaultValue = "''";
591
620
  if (f.isArray) {
592
621
  // Array の場合は空文字列(カンマ区切りで入力するため)
@@ -612,7 +641,7 @@ ${formFields.map(f => {
612
641
  ${needsUseEffect ? `
613
642
  // 参照データの選択肢を取得
614
643
  useEffect(() => {
615
- ${hasForignKeys ? foreignKeyFields.map(f => ` // ${f.referencedModel} の一覧を取得
644
+ ${hasForignKeys ? uniqueByReferencedModel(foreignKeyFields).map(f => ` // ${f.referencedModel} の一覧を取得
616
645
  fetch('/api/${toKebabCase(f.referencedModel!)}')
617
646
  .then(res => res.json())
618
647
  .then(data => set${f.referencedModel}Options(data.map((item: Record<string, string>) => ({ id: item.id, name: item.name || item.title || item.id }))))
@@ -628,7 +657,19 @@ ${hasNestedSchemas ? nestedSchemaFetches : ''}
628
657
  try {
629
658
  // Array フィールドをカンマ区切りから配列に変換
630
659
  const submitData: Record<string, unknown> = { ...formData };
631
- ${formFields.filter(f => f.isArray && !f.isNestedSchema).map(f => ` if (typeof submitData.${f.name} === 'string') {
660
+ ${formFields.filter(isJsonField).length > 0 ? ` // JSON フィールド(インラインオブジェクト)を解析
661
+ ${formFields.filter(isJsonField).map(f => ` if (submitData.${f.name} === '') {
662
+ submitData.${f.name} = undefined;
663
+ } else if (typeof submitData.${f.name} === 'string') {
664
+ try {
665
+ submitData.${f.name} = JSON.parse(submitData.${f.name});
666
+ } catch {
667
+ setErrors({ ${f.name}: 'Invalid JSON' });
668
+ setLoading(false);
669
+ return;
670
+ }
671
+ }`).join('\n')}
672
+ ` : ''}${formFields.filter(f => f.isArray && !f.isNestedSchema && !isJsonField(f)).map(f => ` if (typeof submitData.${f.name} === 'string') {
632
673
  submitData.${f.name} = submitData.${f.name}.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0);
633
674
  }`).join('\n')}
634
675
  ${formFields.filter(f => f.enumValues && f.enumValues.length > 0).length > 0 ? `
@@ -833,6 +874,27 @@ ${f.enumValues.map(v => ` <option value="${v}">${v}</option>`).join('\n
833
874
  </div>`;
834
875
  }
835
876
 
877
+ // インラインオブジェクトは JSON テキストエリア
878
+ if (isJsonField(f)) {
879
+ return ` <div>
880
+ <label htmlFor="${f.name}" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
881
+ ${f.name}${!f.isOptional ? ' *' : ''} <span className="text-xs text-gray-500">(JSON)</span>
882
+ </label>
883
+ <textarea
884
+ id="${f.name}"
885
+ name="${f.name}"
886
+ rows={4}
887
+ value={formData.${f.name}}
888
+ onChange={(e) => setFormData({ ...formData, ${f.name}: e.target.value })}
889
+ className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 font-mono focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
890
+ ${!f.isOptional ? 'required' : ''}
891
+ />
892
+ {errors.${f.name} && (
893
+ <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.${f.name}}</p>
894
+ )}
895
+ </div>`;
896
+ }
897
+
836
898
  // Array の場合はカンマ区切りテキスト
837
899
  if (f.isArray) {
838
900
  return ` <div>
@@ -253,6 +253,8 @@ function runTypecheckCheck(projectRoot: string): VerifyCheckResult {
253
253
 
254
254
  const scripts = readPackageScripts(projectRoot);
255
255
  const pm = detectFromProject(projectRoot);
256
+ // Next.js App Router templates use generated route types (e.g. LayoutProps); create them before tsc.
257
+ const typegenNotes = runNextTypegenIfPresent(projectRoot, pm);
256
258
  let command: string;
257
259
  let args: string[];
258
260
  if (typeof scripts.typecheck === "string") {
@@ -263,13 +265,33 @@ function runTypecheckCheck(projectRoot: string): VerifyCheckResult {
263
265
  args = pm === "pnpm" ? ["exec", "tsc", "--noEmit"] : ["exec", "tsc", "--", "--noEmit"];
264
266
  }
265
267
 
266
- return runCommandCheck(projectRoot, {
268
+ const result = runCommandCheck(projectRoot, {
267
269
  id: "typecheck",
268
270
  title: "TypeScript type check",
269
271
  command,
270
272
  args,
271
273
  failAction: "Fix the TypeScript errors listed in evidence.logTail, then re-run verify.",
272
274
  });
275
+ if (result.status !== "pass" && typegenNotes.length > 0) {
276
+ result.evidence.logTail = [...typegenNotes, ...(result.evidence.logTail ?? [])];
277
+ }
278
+ return result;
279
+ }
280
+
281
+ const NEXT_CONFIG_FILES = ["next.config.ts", "next.config.js", "next.config.mjs", "next.config.cjs"];
282
+
283
+ function runNextTypegenIfPresent(projectRoot: string, pm: ReturnType<typeof detectFromProject>): string[] {
284
+ if (!NEXT_CONFIG_FILES.some((file) => fs.existsSync(path.join(projectRoot, file)))) return [];
285
+ if (!fs.existsSync(path.join(projectRoot, "node_modules", "next", "package.json"))) return [];
286
+ const args = pm === "pnpm" ? ["exec", "next", "typegen"] : ["exec", "--", "next", "typegen"];
287
+ const result = spawnSync(getCommands(pm).name, args, {
288
+ cwd: projectRoot,
289
+ shell: true,
290
+ encoding: "utf-8",
291
+ timeout: 2 * 60 * 1000,
292
+ });
293
+ if (!result.error && result.status === 0) return [];
294
+ return [`next typegen did not complete (${result.error?.message ?? `exit ${result.status}`}); route types such as LayoutProps may be missing.`];
273
295
  }
274
296
 
275
297
  const SCRIPT_CHECK_TITLES: Record<string, string> = {
@@ -274,7 +274,8 @@ function createMachineProgram(): Command {
274
274
  .option("--functions-dir <dir>", "Functions directory", "functions")
275
275
  .option("--api-dir <dir>", "BFF routes directory (Next.js: app/api; React: bff)")
276
276
  .option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets", false)
277
- .action(async (model: string, options: { functionsDir?: string; apiDir?: string; apiOnly?: boolean }) => {
277
+ .option("--skip-infra", "Do not create or update anything under infra/ (container migration is reported as pending)", false)
278
+ .action(async (model: string, options: { functionsDir?: string; apiDir?: string; apiOnly?: boolean; skipInfra?: boolean }) => {
278
279
  await handleMachineAction("generate-scaffold", async () => {
279
280
  ensureProjectForMachine();
280
281
  return runMachineScaffoldOperation({
@@ -282,6 +283,7 @@ function createMachineProgram(): Command {
282
283
  functionsDir: options.functionsDir,
283
284
  apiDir: options.apiDir,
284
285
  apiOnly: options.apiOnly,
286
+ skipInfra: options.skipInfra,
285
287
  });
286
288
  });
287
289
  });
@@ -294,7 +296,8 @@ function createMachineProgram(): Command {
294
296
  .option("--functions-dir <dir>", "Functions directory", "functions")
295
297
  .option("--api-dir <dir>", "BFF routes directory (Next.js: app/api; React: bff)")
296
298
  .option("--api-only", "Skip UI components in the plan", false)
297
- .action(async (models: string[], options: { functionsDir?: string; apiDir?: string; apiOnly?: boolean }) => {
299
+ .option("--skip-infra", "Exclude all infra/ changes (container migration) from the plan", false)
300
+ .action(async (models: string[], options: { functionsDir?: string; apiDir?: string; apiOnly?: boolean; skipInfra?: boolean }) => {
298
301
  await handleMachineAction(
299
302
  "plan-scaffold",
300
303
  async () => {
@@ -307,6 +310,7 @@ function createMachineProgram(): Command {
307
310
  functionsDir: options.functionsDir,
308
311
  apiDir: options.apiDir,
309
312
  apiOnly: options.apiOnly,
313
+ skipInfra: options.skipInfra,
310
314
  })
311
315
  );
312
316
  }
@@ -316,12 +320,20 @@ function createMachineProgram(): Command {
316
320
  const plans = "plans" in data ? data.plans : [data];
317
321
  return {
318
322
  status: plans.some((planData) => planData.requiresApproval) ? "requires-human" : "complete",
319
- nextActions: plans.map((planData) => ({
320
- command: `swallowkit machine apply scaffold --plan ${planData.planId}${planData.requiresApproval ? " --approve" : ""}`,
321
- description: planData.requiresApproval
322
- ? `Apply the plan for ${planData.model} after reviewing the listed conflicts (requires --approve).`
323
- : `Apply the plan for ${planData.model}.`,
324
- })),
323
+ nextActions: [
324
+ ...plans.map((planData) => ({
325
+ command: `swallowkit machine apply scaffold --plan ${planData.planId}${planData.requiresApproval ? " --approve" : ""}`,
326
+ description: planData.requiresApproval
327
+ ? `Apply the plan for ${planData.model} after reviewing the listed conflicts (requires --approve).`
328
+ : `Apply the plan for ${planData.model}.`,
329
+ })),
330
+ ...plans
331
+ .filter((planData) => planData.warnings.some((warning) => warning.startsWith("infra-skipped:")))
332
+ .map((planData) => ({
333
+ command: `swallowkit migrations add-model ${planData.model}`,
334
+ description: `Later, when infrastructure changes are allowed, generate only the Cosmos DB container migration for ${planData.model}.`,
335
+ })),
336
+ ],
325
337
  };
326
338
  }
327
339
  );
@@ -408,10 +420,11 @@ function createMachineProgram(): Command {
408
420
  .option("--functions-dir <dir>", "Functions directory")
409
421
  .option("--api-dir <dir>", "API routes directory")
410
422
  .option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets")
423
+ .option("--skip-infra", "Do not create or update anything under infra/ (container migration is reported as pending)")
411
424
  .action(
412
425
  async (
413
426
  models: string[],
414
- options: { plan?: string; approve?: boolean; functionsDir?: string; apiDir?: string; apiOnly?: boolean }
427
+ options: { plan?: string; approve?: boolean; functionsDir?: string; apiDir?: string; apiOnly?: boolean; skipInfra?: boolean }
415
428
  ) => {
416
429
  await handleMachineAction(
417
430
  "apply-scaffold",
@@ -433,6 +446,7 @@ function createMachineProgram(): Command {
433
446
  functionsDir: options.functionsDir,
434
447
  apiDir: options.apiDir,
435
448
  apiOnly: options.apiOnly,
449
+ skipInfra: options.skipInfra,
436
450
  });
437
451
  }
438
452
  const results = [];
@@ -444,20 +458,30 @@ function createMachineProgram(): Command {
444
458
  functionsDir: options.functionsDir,
445
459
  apiDir: options.apiDir,
446
460
  apiOnly: options.apiOnly,
461
+ skipInfra: options.skipInfra,
447
462
  })
448
463
  );
449
464
  }
450
465
  return { applyType: "scaffold-batch" as const, results };
451
466
  },
452
- () => ({
453
- status: "complete",
454
- nextActions: [
455
- {
456
- command: "swallowkit machine verify project",
457
- description: "Verify the project after applying scaffold changes.",
458
- },
459
- ],
460
- })
467
+ (data) => {
468
+ const results = "results" in data ? data.results : [data];
469
+ return {
470
+ status: "complete",
471
+ nextActions: [
472
+ {
473
+ command: "swallowkit machine verify project",
474
+ description: "Verify the project after applying scaffold changes.",
475
+ },
476
+ ...results
477
+ .filter((result) => result.warnings.some((warning) => warning.startsWith("infra-skipped:")))
478
+ .map((result) => ({
479
+ command: `swallowkit migrations add-model ${result.model}`,
480
+ description: `The Cosmos DB container migration for ${result.model} is still pending; generate it when infrastructure changes are allowed.`,
481
+ })),
482
+ ],
483
+ };
484
+ }
461
485
  );
462
486
  }
463
487
  );
package/src/mcp/index.ts CHANGED
@@ -1,11 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { spawn } from "child_process";
3
4
  import * as path from "path";
4
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
7
  import * as z from "zod/v4";
7
8
  import type { MachineNextAction, MachineOperationStatus, MachineResponse } from "../machine/contracts";
8
9
  import { getSwallowKitVersion } from "../version";
10
+ import { detectFromProject, getCommands } from "../utils/package-manager";
11
+ import {
12
+ MCP_DELEGATED_FROM_ENV,
13
+ McpVersionContext,
14
+ buildPinnedUpdateNotice,
15
+ decideMcpLaunch,
16
+ resolveProjectSwallowKit,
17
+ } from "./version-selection";
9
18
 
10
19
  interface MachineSuccessPayload<TData> {
11
20
  ok: true;
@@ -70,6 +79,29 @@ async function executeMachineCommand<TData>(
70
79
  return parsed;
71
80
  }
72
81
 
82
+ function defaultVersionContext(): McpVersionContext {
83
+ return {
84
+ mcpVersion: getSwallowKitVersion(),
85
+ requestedVersion: "latest",
86
+ projectVersion: null,
87
+ selection: "running-version",
88
+ delegated: false,
89
+ warnings: [],
90
+ nextActions: [],
91
+ };
92
+ }
93
+
94
+ function mergeNextActions(
95
+ first: MachineNextAction[] | undefined,
96
+ second: MachineNextAction[]
97
+ ): MachineNextAction[] {
98
+ const merged = [...(first ?? [])];
99
+ for (const action of second) {
100
+ if (!merged.some((existing) => existing.command === action.command)) merged.push(action);
101
+ }
102
+ return merged;
103
+ }
104
+
73
105
  function withOperationStatus<TData>(response: MachineSuccessPayload<TData>): Record<string, unknown> {
74
106
  const data = response.data && typeof response.data === "object"
75
107
  ? (response.data as Record<string, unknown>)
@@ -92,25 +124,41 @@ function jsonTextContent(value: unknown): { content: Array<{ type: "text"; text:
92
124
  };
93
125
  }
94
126
 
95
- function withSwallowKitMetadata<TData>(data: TData): TData & { metadata: Record<string, unknown> } {
96
- const value = data && typeof data === "object" ? data as Record<string, unknown> : { data };
127
+ function withVersionMetadata(data: unknown, context: McpVersionContext): Record<string, unknown> {
128
+ const value = data && typeof data === "object" && !Array.isArray(data)
129
+ ? data as Record<string, unknown>
130
+ : { data };
97
131
  const metadata = value.metadata && typeof value.metadata === "object"
98
132
  ? value.metadata as Record<string, unknown>
99
133
  : {};
134
+ const existingNextActions = Array.isArray(value.nextActions) ? value.nextActions as MachineNextAction[] : undefined;
135
+ const nextActions = mergeNextActions(existingNextActions, context.nextActions);
100
136
 
101
137
  return {
102
138
  ...value,
139
+ ...(nextActions.length > 0 ? { nextActions } : {}),
103
140
  metadata: {
104
141
  ...metadata,
105
- swallowkitVersion: getSwallowKitVersion(),
142
+ swallowkitVersion: context.mcpVersion,
143
+ mcp: {
144
+ version: context.mcpVersion,
145
+ requestedVersion: context.requestedVersion,
146
+ projectVersion: context.projectVersion,
147
+ selection: context.selection,
148
+ delegated: context.delegated,
149
+ ...(context.delegatedFrom ? { delegatedFrom: context.delegatedFrom } : {}),
150
+ },
151
+ ...(context.warnings.length > 0 ? { warnings: [...context.warnings] } : {}),
106
152
  },
107
- } as TData & { metadata: Record<string, unknown> };
153
+ };
108
154
  }
109
155
 
110
156
  export function buildSwallowKitToolDefinitions(
111
- runMachineCli: MachineCliRunner = defaultMachineCliRunner
157
+ runMachineCli: MachineCliRunner = defaultMachineCliRunner,
158
+ versionContext: McpVersionContext = defaultVersionContext()
112
159
  ): ToolDefinition[] {
113
- return [
160
+ const withSwallowKitMetadata = (data: unknown) => withVersionMetadata(data, versionContext);
161
+ const definitions: ToolDefinition[] = [
114
162
  {
115
163
  name: "swallowkit_inspect_project",
116
164
  description: "Return framework-owned SwallowKit project metadata.",
@@ -169,8 +217,9 @@ export function buildSwallowKitToolDefinitions(
169
217
  functionsDir: z.string().optional(),
170
218
  apiDir: z.string().optional(),
171
219
  apiOnly: z.boolean().optional(),
220
+ skipInfra: z.boolean().optional().describe("Do not create or update anything under infra/; the container migration stays pending."),
172
221
  }),
173
- handler: async ({ model, functionsDir, apiDir, apiOnly }: { model: string; functionsDir?: string; apiDir?: string; apiOnly?: boolean }) => {
222
+ handler: async ({ model, functionsDir, apiDir, apiOnly, skipInfra }: { model: string; functionsDir?: string; apiDir?: string; apiOnly?: boolean; skipInfra?: boolean }) => {
174
223
  const args = ["generate", "scaffold", model];
175
224
  if (functionsDir) {
176
225
  args.push("--functions-dir", functionsDir);
@@ -181,6 +230,9 @@ export function buildSwallowKitToolDefinitions(
181
230
  if (apiOnly) {
182
231
  args.push("--api-only");
183
232
  }
233
+ if (skipInfra) {
234
+ args.push("--skip-infra");
235
+ }
184
236
 
185
237
  const response = await executeMachineCommand(args, runMachineCli);
186
238
  return jsonTextContent(response.data);
@@ -195,14 +247,16 @@ export function buildSwallowKitToolDefinitions(
195
247
  functionsDir: z.string().optional(),
196
248
  apiDir: z.string().optional(),
197
249
  apiOnly: z.boolean().optional(),
250
+ skipInfra: z.boolean().optional().describe("Exclude all infra/ changes (container migration) from the plan."),
198
251
  }),
199
- handler: async ({ model, models, functionsDir, apiDir, apiOnly }: { model?: string; models?: string[]; functionsDir?: string; apiDir?: string; apiOnly?: boolean }) => {
252
+ handler: async ({ model, models, functionsDir, apiDir, apiOnly, skipInfra }: { model?: string; models?: string[]; functionsDir?: string; apiDir?: string; apiOnly?: boolean; skipInfra?: boolean }) => {
200
253
  const targets = models && models.length > 0 ? models : model ? [model] : [];
201
254
  if (targets.length === 0) throw new Error("Provide model or models.");
202
255
  const args = ["plan", "scaffold", ...targets];
203
256
  if (functionsDir) args.push("--functions-dir", functionsDir);
204
257
  if (apiDir) args.push("--api-dir", apiDir);
205
258
  if (apiOnly) args.push("--api-only");
259
+ if (skipInfra) args.push("--skip-infra");
206
260
 
207
261
  const response = await executeMachineCommand(args, runMachineCli);
208
262
  return jsonTextContent(withOperationStatus(response));
@@ -218,8 +272,9 @@ export function buildSwallowKitToolDefinitions(
218
272
  functionsDir: z.string().optional(),
219
273
  apiDir: z.string().optional(),
220
274
  apiOnly: z.boolean().optional(),
275
+ skipInfra: z.boolean().optional().describe("Do not create or update anything under infra/ (defaults to the plan's setting)."),
221
276
  }),
222
- handler: async ({ model, planId, approve, functionsDir, apiDir, apiOnly }: { model?: string; planId?: string; approve?: boolean; functionsDir?: string; apiDir?: string; apiOnly?: boolean }) => {
277
+ handler: async ({ model, planId, approve, functionsDir, apiDir, apiOnly, skipInfra }: { model?: string; planId?: string; approve?: boolean; functionsDir?: string; apiDir?: string; apiOnly?: boolean; skipInfra?: boolean }) => {
223
278
  const args = ["apply", "scaffold"];
224
279
  if (model) args.push(model);
225
280
  if (planId) args.push("--plan", planId);
@@ -227,6 +282,7 @@ export function buildSwallowKitToolDefinitions(
227
282
  if (functionsDir) args.push("--functions-dir", functionsDir);
228
283
  if (apiDir) args.push("--api-dir", apiDir);
229
284
  if (apiOnly) args.push("--api-only");
285
+ if (skipInfra) args.push("--skip-infra");
230
286
 
231
287
  const response = await executeMachineCommand(args, runMachineCli);
232
288
  return jsonTextContent(withOperationStatus(response));
@@ -391,16 +447,28 @@ export function buildSwallowKitToolDefinitions(
391
447
  },
392
448
  },
393
449
  ];
450
+
451
+ // Every response reports which SwallowKit version answered and why it was selected.
452
+ return definitions.map((definition) => ({
453
+ ...definition,
454
+ handler: async (input: unknown) => {
455
+ const result = await definition.handler(input);
456
+ return jsonTextContent(withSwallowKitMetadata(JSON.parse(result.content[0].text)));
457
+ },
458
+ }));
394
459
  }
395
460
 
396
- export function createSwallowKitMcpServer(runMachineCli: MachineCliRunner = defaultMachineCliRunner): McpServer {
397
- const version = getSwallowKitVersion();
461
+ export function createSwallowKitMcpServer(
462
+ runMachineCli: MachineCliRunner = defaultMachineCliRunner,
463
+ versionContext: McpVersionContext = defaultVersionContext()
464
+ ): McpServer {
465
+ const version = versionContext.mcpVersion;
398
466
  const server = new McpServer({
399
467
  name: "swallowkit-mcp",
400
468
  version,
401
469
  });
402
470
 
403
- for (const tool of buildSwallowKitToolDefinitions(runMachineCli)) {
471
+ for (const tool of buildSwallowKitToolDefinitions(runMachineCli, versionContext)) {
404
472
  server.registerTool(
405
473
  tool.name,
406
474
  {
@@ -414,9 +482,83 @@ export function createSwallowKitMcpServer(runMachineCli: MachineCliRunner = defa
414
482
  return server;
415
483
  }
416
484
 
485
+ function resolveExecCommand(cwd: string): string {
486
+ try {
487
+ return getCommands(detectFromProject(cwd)).exec;
488
+ } catch {
489
+ return "npx";
490
+ }
491
+ }
492
+
493
+ async function fetchPublishedLatest(): Promise<string | null> {
494
+ if (process.env.SWALLOWKIT_MCP_UPDATE_CHECK === "0") return null;
495
+ const registry = (process.env.npm_config_registry || "https://registry.npmjs.org").replace(/\/+$/, "");
496
+ try {
497
+ const response = await fetch(`${registry}/swallowkit/latest`, { signal: AbortSignal.timeout(3000) });
498
+ if (!response.ok) return null;
499
+ const body = await response.json() as { version?: unknown };
500
+ return typeof body.version === "string" ? body.version : null;
501
+ } catch {
502
+ return null;
503
+ }
504
+ }
505
+
506
+ function delegateToProjectMcp(entrypoint: string, fromVersion: string): void {
507
+ const child = spawn(process.execPath, [entrypoint], {
508
+ stdio: "inherit",
509
+ env: { ...process.env, [MCP_DELEGATED_FROM_ENV]: fromVersion },
510
+ });
511
+ for (const signal of ["SIGINT", "SIGTERM"] as const) {
512
+ process.on(signal, () => child.kill(signal));
513
+ }
514
+ child.on("error", (error) => {
515
+ console.error(`[swallowkit-mcp] failed to start the project-installed MCP: ${error.message}`);
516
+ process.exitCode = 1;
517
+ });
518
+ child.on("exit", (code, signal) => {
519
+ process.exitCode = code ?? (signal ? 1 : 0);
520
+ });
521
+ }
522
+
417
523
  export async function runMcpServer(): Promise<void> {
418
- console.error(`[swallowkit-mcp] version: ${getSwallowKitVersion()}`);
419
- const server = createSwallowKitMcpServer();
524
+ const selfVersion = getSwallowKitVersion();
525
+ const cwd = process.cwd();
526
+ const execCommand = resolveExecCommand(cwd);
527
+ const decision = decideMcpLaunch({
528
+ selfVersion,
529
+ selfPackageRoot: path.resolve(__dirname, "..", ".."),
530
+ env: process.env,
531
+ project: resolveProjectSwallowKit(cwd),
532
+ execCommand,
533
+ });
534
+
535
+ if (decision.action === "delegate") {
536
+ console.error(
537
+ `[swallowkit-mcp] version: ${selfVersion}; delegating to the project-installed swallowkit-mcp ${decision.toVersion} (newer than the launcher).`
538
+ );
539
+ delegateToProjectMcp(decision.entrypoint, decision.fromVersion);
540
+ return;
541
+ }
542
+
543
+ const context = decision.context;
544
+ // One write keeps the version line and its warnings together for log readers.
545
+ console.error([
546
+ `[swallowkit-mcp] version: ${selfVersion} (selection: ${context.selection}${context.delegatedFrom ? `, delegated from ${context.delegatedFrom}` : ""})`,
547
+ ...context.warnings.map((warning) => `[swallowkit-mcp] warning: ${warning}`),
548
+ ].join("\n"));
549
+
550
+ if (context.selection === "pinned") {
551
+ void fetchPublishedLatest().then((latest) => {
552
+ const notice = buildPinnedUpdateNotice(context, latest, execCommand);
553
+ if (notice) {
554
+ context.warnings.push(notice.warning);
555
+ context.nextActions.push(notice.nextAction);
556
+ console.error(`[swallowkit-mcp] warning: ${notice.warning}`);
557
+ }
558
+ });
559
+ }
560
+
561
+ const server = createSwallowKitMcpServer(defaultMachineCliRunner, context);
420
562
  const transport = new StdioServerTransport();
421
563
  await server.connect(transport);
422
564
  }