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.
- package/dist/cli/commands/configure-mcp.d.ts +12 -0
- package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
- package/dist/cli/commands/configure-mcp.js +42 -0
- package/dist/cli/commands/configure-mcp.js.map +1 -0
- package/dist/cli/commands/init.d.ts +4 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +16 -4
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrations.d.ts.map +1 -1
- package/dist/cli/commands/migrations.js +25 -0
- package/dist/cli/commands/migrations.js.map +1 -1
- package/dist/cli/commands/provision.d.ts +2 -0
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +45 -16
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +4 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +40 -2
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +27 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/core/operations/model-migration.d.ts +22 -0
- package/dist/core/operations/model-migration.d.ts.map +1 -0
- package/dist/core/operations/model-migration.js +36 -0
- package/dist/core/operations/model-migration.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +19 -1
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +62 -35
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts +1 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
- package/dist/core/operations/scaffold-apply.js +1 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -1
- package/dist/core/operations/scaffold-machine.d.ts +1 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
- package/dist/core/operations/scaffold-machine.js.map +1 -1
- package/dist/core/operations/scaffold-plan.d.ts +2 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
- package/dist/core/operations/scaffold-plan.js +3 -1
- package/dist/core/operations/scaffold-plan.js.map +1 -1
- package/dist/core/project/boundaries.d.ts +5 -2
- package/dist/core/project/boundaries.d.ts.map +1 -1
- package/dist/core/project/boundaries.js +49 -18
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +1 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +20 -2
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/mcp-config.d.ts +26 -0
- package/dist/core/project/mcp-config.d.ts.map +1 -0
- package/dist/core/project/mcp-config.js +117 -0
- package/dist/core/project/mcp-config.js.map +1 -0
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +9 -1
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +23 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +407 -137
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +11 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +273 -75
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +5 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
- package/dist/core/scaffold/openapi-generator.js +53 -3
- package/dist/core/scaffold/openapi-generator.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +0 -3
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +68 -7
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +24 -1
- package/dist/core/verify/index.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +39 -15
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +3 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +137 -13
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/version-selection.d.ts +55 -0
- package/dist/mcp/version-selection.d.ts.map +1 -0
- package/dist/mcp/version-selection.js +152 -0
- package/dist/mcp/version-selection.js.map +1 -0
- package/dist/utils/azure-cli.d.ts +44 -0
- package/dist/utils/azure-cli.d.ts.map +1 -0
- package/dist/utils/azure-cli.js +164 -0
- package/dist/utils/azure-cli.js.map +1 -0
- package/dist/utils/semver.d.ts +14 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +61 -0
- package/dist/utils/semver.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-loop-phase34.test.ts +29 -0
- package/src/__tests__/azure-cli.test.ts +199 -0
- package/src/__tests__/boundaries.test.ts +42 -0
- package/src/__tests__/csharp-schema.test.ts +261 -0
- package/src/__tests__/mcp-version.test.ts +262 -0
- package/src/__tests__/mcp.test.ts +49 -3
- package/src/__tests__/plan-apply.test.ts +59 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/init.ts +19 -4
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +53 -17
- package/src/cli/commands/scaffold.ts +40 -2
- package/src/cli/index.ts +26 -3
- package/src/core/operations/model-migration.ts +52 -0
- package/src/core/operations/provision-operations.ts +86 -42
- package/src/core/operations/scaffold-apply.ts +2 -0
- package/src/core/operations/scaffold-machine.ts +1 -0
- package/src/core/operations/scaffold-plan.ts +5 -1
- package/src/core/project/agent-templates/AGENTS.md +39 -3
- package/src/core/project/agent-templates/CLAUDE.md +3 -1
- package/src/core/project/agent-templates/copilot-instructions.md +3 -1
- package/src/core/project/boundaries.ts +60 -18
- package/src/core/project/capabilities.ts +22 -2
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/workflows.ts +9 -1
- package/src/core/scaffold/model-parser.ts +413 -129
- package/src/core/scaffold/native-schema-generator.ts +297 -78
- package/src/core/scaffold/openapi-generator.ts +63 -5
- package/src/core/scaffold/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +23 -1
- package/src/machine/index.ts +42 -18
- package/src/mcp/index.ts +156 -14
- package/src/mcp/version-selection.ts +163 -0
- package/src/utils/azure-cli.ts +154 -0
- 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(
|
|
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>
|
package/src/core/verify/index.ts
CHANGED
|
@@ -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
|
-
|
|
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> = {
|
package/src/machine/index.ts
CHANGED
|
@@ -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
|
-
.
|
|
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
|
-
.
|
|
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:
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
|
96
|
-
const value = data && typeof data === "object"
|
|
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:
|
|
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
|
-
}
|
|
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
|
-
|
|
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(
|
|
397
|
-
|
|
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
|
-
|
|
419
|
-
const
|
|
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
|
}
|