swallowkit 1.1.2 → 2.0.0-alpha.10

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 (262) hide show
  1. package/README.ja.md +61 -7
  2. package/README.md +61 -7
  3. package/dist/cli/commands/add-auth.d.ts.map +1 -1
  4. package/dist/cli/commands/add-auth.js +103 -27
  5. package/dist/cli/commands/add-auth.js.map +1 -1
  6. package/dist/cli/commands/configure-mcp.d.ts +12 -0
  7. package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
  8. package/dist/cli/commands/configure-mcp.js +42 -0
  9. package/dist/cli/commands/configure-mcp.js.map +1 -0
  10. package/dist/cli/commands/dev.d.ts +5 -2
  11. package/dist/cli/commands/dev.d.ts.map +1 -1
  12. package/dist/cli/commands/dev.js +143 -67
  13. package/dist/cli/commands/dev.js.map +1 -1
  14. package/dist/cli/commands/init.d.ts +14 -7
  15. package/dist/cli/commands/init.d.ts.map +1 -1
  16. package/dist/cli/commands/init.js +149 -170
  17. package/dist/cli/commands/init.js.map +1 -1
  18. package/dist/cli/commands/migrations.d.ts.map +1 -1
  19. package/dist/cli/commands/migrations.js +25 -0
  20. package/dist/cli/commands/migrations.js.map +1 -1
  21. package/dist/cli/commands/provision.d.ts +5 -0
  22. package/dist/cli/commands/provision.d.ts.map +1 -1
  23. package/dist/cli/commands/provision.js +278 -55
  24. package/dist/cli/commands/provision.js.map +1 -1
  25. package/dist/cli/commands/scaffold.d.ts +5 -1
  26. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  27. package/dist/cli/commands/scaffold.js +123 -18
  28. package/dist/cli/commands/scaffold.js.map +1 -1
  29. package/dist/cli/index.d.ts +2 -0
  30. package/dist/cli/index.d.ts.map +1 -1
  31. package/dist/cli/index.js +49 -8
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/core/config.d.ts +2 -1
  34. package/dist/core/config.d.ts.map +1 -1
  35. package/dist/core/config.js +19 -0
  36. package/dist/core/config.js.map +1 -1
  37. package/dist/core/operations/create-model.d.ts +2 -0
  38. package/dist/core/operations/create-model.d.ts.map +1 -1
  39. package/dist/core/operations/create-model.js +60 -8
  40. package/dist/core/operations/create-model.js.map +1 -1
  41. package/dist/core/operations/migration-generator.d.ts.map +1 -1
  42. package/dist/core/operations/migration-generator.js +12 -1
  43. package/dist/core/operations/migration-generator.js.map +1 -1
  44. package/dist/core/operations/model-migration.d.ts +22 -0
  45. package/dist/core/operations/model-migration.d.ts.map +1 -0
  46. package/dist/core/operations/model-migration.js +36 -0
  47. package/dist/core/operations/model-migration.js.map +1 -0
  48. package/dist/core/operations/provision-deployment.d.ts +52 -0
  49. package/dist/core/operations/provision-deployment.d.ts.map +1 -0
  50. package/dist/core/operations/provision-deployment.js +164 -0
  51. package/dist/core/operations/provision-deployment.js.map +1 -0
  52. package/dist/core/operations/provision-operations.d.ts +62 -6
  53. package/dist/core/operations/provision-operations.d.ts.map +1 -1
  54. package/dist/core/operations/provision-operations.js +653 -115
  55. package/dist/core/operations/provision-operations.js.map +1 -1
  56. package/dist/core/operations/scaffold-apply.d.ts +1 -0
  57. package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
  58. package/dist/core/operations/scaffold-apply.js +1 -0
  59. package/dist/core/operations/scaffold-apply.js.map +1 -1
  60. package/dist/core/operations/scaffold-machine.d.ts +1 -0
  61. package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
  62. package/dist/core/operations/scaffold-machine.js.map +1 -1
  63. package/dist/core/operations/scaffold-plan.d.ts +2 -0
  64. package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
  65. package/dist/core/operations/scaffold-plan.js +3 -1
  66. package/dist/core/operations/scaffold-plan.js.map +1 -1
  67. package/dist/core/project/agent-instructions.d.ts +2 -1
  68. package/dist/core/project/agent-instructions.d.ts.map +1 -1
  69. package/dist/core/project/agent-instructions.js +15 -0
  70. package/dist/core/project/agent-instructions.js.map +1 -1
  71. package/dist/core/project/boundaries.d.ts +5 -2
  72. package/dist/core/project/boundaries.d.ts.map +1 -1
  73. package/dist/core/project/boundaries.js +51 -20
  74. package/dist/core/project/boundaries.js.map +1 -1
  75. package/dist/core/project/capabilities.d.ts +17 -0
  76. package/dist/core/project/capabilities.d.ts.map +1 -1
  77. package/dist/core/project/capabilities.js +45 -6
  78. package/dist/core/project/capabilities.js.map +1 -1
  79. package/dist/core/project/home-page.d.ts +4 -0
  80. package/dist/core/project/home-page.d.ts.map +1 -0
  81. package/dist/core/project/home-page.js +113 -0
  82. package/dist/core/project/home-page.js.map +1 -0
  83. package/dist/core/project/manifest.d.ts +3 -1
  84. package/dist/core/project/manifest.d.ts.map +1 -1
  85. package/dist/core/project/manifest.js +47 -25
  86. package/dist/core/project/manifest.js.map +1 -1
  87. package/dist/core/project/mcp-config.d.ts +26 -0
  88. package/dist/core/project/mcp-config.d.ts.map +1 -0
  89. package/dist/core/project/mcp-config.js +117 -0
  90. package/dist/core/project/mcp-config.js.map +1 -0
  91. package/dist/core/project/react-project.d.ts +26 -0
  92. package/dist/core/project/react-project.d.ts.map +1 -0
  93. package/dist/core/project/react-project.js +443 -0
  94. package/dist/core/project/react-project.js.map +1 -0
  95. package/dist/core/project/state.d.ts +7 -0
  96. package/dist/core/project/state.d.ts.map +1 -1
  97. package/dist/core/project/state.js +86 -0
  98. package/dist/core/project/state.js.map +1 -1
  99. package/dist/core/project/validation.d.ts.map +1 -1
  100. package/dist/core/project/validation.js +32 -10
  101. package/dist/core/project/validation.js.map +1 -1
  102. package/dist/core/project/workflows.d.ts.map +1 -1
  103. package/dist/core/project/workflows.js +53 -7
  104. package/dist/core/project/workflows.js.map +1 -1
  105. package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
  106. package/dist/core/scaffold/auth-generator.js +26 -27
  107. package/dist/core/scaffold/auth-generator.js.map +1 -1
  108. package/dist/core/scaffold/model-parser.d.ts +23 -0
  109. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  110. package/dist/core/scaffold/model-parser.js +407 -137
  111. package/dist/core/scaffold/model-parser.js.map +1 -1
  112. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  113. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
  114. package/dist/core/scaffold/native-schema-generator.js +294 -91
  115. package/dist/core/scaffold/native-schema-generator.js.map +1 -1
  116. package/dist/core/scaffold/openapi-generator.d.ts +5 -0
  117. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
  118. package/dist/core/scaffold/openapi-generator.js +53 -3
  119. package/dist/core/scaffold/openapi-generator.js.map +1 -1
  120. package/dist/core/scaffold/react-bff-generator.d.ts +10 -0
  121. package/dist/core/scaffold/react-bff-generator.d.ts.map +1 -0
  122. package/dist/core/scaffold/react-bff-generator.js +252 -0
  123. package/dist/core/scaffold/react-bff-generator.js.map +1 -0
  124. package/dist/core/scaffold/react-ui-generator.d.ts +6 -0
  125. package/dist/core/scaffold/react-ui-generator.d.ts.map +1 -0
  126. package/dist/core/scaffold/react-ui-generator.js +41 -0
  127. package/dist/core/scaffold/react-ui-generator.js.map +1 -0
  128. package/dist/core/scaffold/ui-generator.d.ts +0 -3
  129. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  130. package/dist/core/scaffold/ui-generator.js +68 -7
  131. package/dist/core/scaffold/ui-generator.js.map +1 -1
  132. package/dist/core/verify/index.d.ts +4 -3
  133. package/dist/core/verify/index.d.ts.map +1 -1
  134. package/dist/core/verify/index.js +122 -5
  135. package/dist/core/verify/index.js.map +1 -1
  136. package/dist/index.d.ts +3 -3
  137. package/dist/index.d.ts.map +1 -1
  138. package/dist/index.js +3 -2
  139. package/dist/index.js.map +1 -1
  140. package/dist/machine/contracts.d.ts +1 -0
  141. package/dist/machine/contracts.d.ts.map +1 -1
  142. package/dist/machine/errors.d.ts +3 -2
  143. package/dist/machine/errors.d.ts.map +1 -1
  144. package/dist/machine/errors.js +2 -1
  145. package/dist/machine/errors.js.map +1 -1
  146. package/dist/machine/index.d.ts.map +1 -1
  147. package/dist/machine/index.js +61 -30
  148. package/dist/machine/index.js.map +1 -1
  149. package/dist/mcp/index.d.ts +6 -4
  150. package/dist/mcp/index.d.ts.map +1 -1
  151. package/dist/mcp/index.js +210 -25
  152. package/dist/mcp/index.js.map +1 -1
  153. package/dist/mcp/version-selection.d.ts +55 -0
  154. package/dist/mcp/version-selection.d.ts.map +1 -0
  155. package/dist/mcp/version-selection.js +152 -0
  156. package/dist/mcp/version-selection.js.map +1 -0
  157. package/dist/types/index.d.ts +4 -0
  158. package/dist/types/index.d.ts.map +1 -1
  159. package/dist/utils/azure-cli.d.ts +56 -0
  160. package/dist/utils/azure-cli.d.ts.map +1 -0
  161. package/dist/utils/azure-cli.js +326 -0
  162. package/dist/utils/azure-cli.js.map +1 -0
  163. package/dist/utils/azure-diagnostics.d.ts +16 -0
  164. package/dist/utils/azure-diagnostics.d.ts.map +1 -0
  165. package/dist/utils/azure-diagnostics.js +68 -0
  166. package/dist/utils/azure-diagnostics.js.map +1 -0
  167. package/dist/utils/package-manager.d.ts +2 -2
  168. package/dist/utils/package-manager.d.ts.map +1 -1
  169. package/dist/utils/package-manager.js +6 -3
  170. package/dist/utils/package-manager.js.map +1 -1
  171. package/dist/utils/python-uv.d.ts +5 -7
  172. package/dist/utils/python-uv.d.ts.map +1 -1
  173. package/dist/utils/python-uv.js +50 -24
  174. package/dist/utils/python-uv.js.map +1 -1
  175. package/dist/utils/react-dev.d.ts +4 -0
  176. package/dist/utils/react-dev.d.ts.map +1 -0
  177. package/dist/utils/react-dev.js +35 -0
  178. package/dist/utils/react-dev.js.map +1 -0
  179. package/dist/utils/semver.d.ts +14 -0
  180. package/dist/utils/semver.d.ts.map +1 -0
  181. package/dist/utils/semver.js +61 -0
  182. package/dist/utils/semver.js.map +1 -0
  183. package/package.json +7 -4
  184. package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
  185. package/src/__tests__/agent-instructions.test.ts +6 -1
  186. package/src/__tests__/agent-loop-phase34.test.ts +143 -8
  187. package/src/__tests__/auth.test.ts +9 -0
  188. package/src/__tests__/azure-cli.test.ts +202 -0
  189. package/src/__tests__/boundaries.test.ts +42 -0
  190. package/src/__tests__/csharp-schema.test.ts +261 -0
  191. package/src/__tests__/dev.test.ts +13 -6
  192. package/src/__tests__/fixtures/az wrapper.cmd +2 -0
  193. package/src/__tests__/fixtures/azure-cli.cjs +153 -0
  194. package/src/__tests__/fixtures/provision-runner.cjs +28 -0
  195. package/src/__tests__/generated-project-matrix.test.ts +789 -0
  196. package/src/__tests__/home-page.test.ts +50 -0
  197. package/src/__tests__/init.test.ts +145 -5
  198. package/src/__tests__/mcp-version.test.ts +262 -0
  199. package/src/__tests__/mcp.test.ts +120 -3
  200. package/src/__tests__/multi-auth.test.ts +115 -4
  201. package/src/__tests__/package-manager.test.ts +17 -2
  202. package/src/__tests__/plan-apply.test.ts +179 -0
  203. package/src/__tests__/provision-runtime.test.ts +722 -0
  204. package/src/__tests__/python-uv.test.ts +50 -14
  205. package/src/__tests__/python-venv.test.ts +48 -0
  206. package/src/__tests__/react-dev.test.ts +72 -0
  207. package/src/__tests__/react.test.ts +838 -0
  208. package/src/__tests__/release-metadata.test.ts +121 -0
  209. package/src/__tests__/schema-analysis.test.ts +329 -0
  210. package/src/cli/commands/add-auth.ts +103 -29
  211. package/src/cli/commands/configure-mcp.ts +54 -0
  212. package/src/cli/commands/dev.ts +145 -73
  213. package/src/cli/commands/init.ts +147 -172
  214. package/src/cli/commands/migrations.ts +22 -0
  215. package/src/cli/commands/provision.ts +294 -56
  216. package/src/cli/commands/scaffold.ts +145 -22
  217. package/src/cli/index.ts +46 -10
  218. package/src/core/config.ts +21 -1
  219. package/src/core/operations/create-model.ts +51 -13
  220. package/src/core/operations/migration-generator.ts +14 -1
  221. package/src/core/operations/model-migration.ts +52 -0
  222. package/src/core/operations/provision-deployment.ts +183 -0
  223. package/src/core/operations/provision-operations.ts +662 -154
  224. package/src/core/operations/scaffold-apply.ts +2 -0
  225. package/src/core/operations/scaffold-machine.ts +1 -0
  226. package/src/core/operations/scaffold-plan.ts +5 -1
  227. package/src/core/project/agent-instructions.ts +17 -1
  228. package/src/core/project/agent-templates/AGENTS.md +113 -15
  229. package/src/core/project/agent-templates/CLAUDE.md +14 -1
  230. package/src/core/project/agent-templates/bff-routes.instructions.md +5 -3
  231. package/src/core/project/agent-templates/copilot-instructions.md +15 -1
  232. package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
  233. package/src/core/project/boundaries.ts +62 -20
  234. package/src/core/project/capabilities.ts +63 -6
  235. package/src/core/project/home-page.ts +114 -0
  236. package/src/core/project/manifest.ts +52 -27
  237. package/src/core/project/mcp-config.ts +84 -0
  238. package/src/core/project/react-project.ts +529 -0
  239. package/src/core/project/state.ts +70 -0
  240. package/src/core/project/validation.ts +32 -10
  241. package/src/core/project/workflows.ts +53 -7
  242. package/src/core/scaffold/auth-generator.ts +26 -27
  243. package/src/core/scaffold/model-parser.ts +413 -129
  244. package/src/core/scaffold/native-schema-generator.ts +319 -103
  245. package/src/core/scaffold/openapi-generator.ts +63 -5
  246. package/src/core/scaffold/react-bff-generator.ts +292 -0
  247. package/src/core/scaffold/react-ui-generator.ts +51 -0
  248. package/src/core/scaffold/ui-generator.ts +70 -8
  249. package/src/core/verify/index.ts +118 -6
  250. package/src/index.ts +3 -1
  251. package/src/machine/contracts.ts +1 -0
  252. package/src/machine/errors.ts +4 -2
  253. package/src/machine/index.ts +67 -36
  254. package/src/mcp/index.ts +221 -31
  255. package/src/mcp/version-selection.ts +163 -0
  256. package/src/types/index.ts +4 -0
  257. package/src/utils/azure-cli.ts +315 -0
  258. package/src/utils/azure-diagnostics.ts +75 -0
  259. package/src/utils/package-manager.ts +7 -4
  260. package/src/utils/python-uv.ts +53 -33
  261. package/src/utils/react-dev.ts +31 -0
  262. package/src/utils/semver.ts +59 -0
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * SwallowKit Scaffold コマンド
3
- * Zod モデルから Azure Functions と Next.js BFF の CRUD コードを生成
3
+ * Zod モデルから Azure Functions、BFF、選択したフロントエンドの CRUD を生成
4
4
  */
5
5
 
6
6
  import * as fs from "fs";
7
7
  import * as path from "path";
8
8
  import { spawnSync } from "child_process";
9
- import { getBackendLanguage, getConnectorDefinition, getAuthConfig, getFullConfig, getNormalizedAuthConfig, ensureSwallowKitProject, normalizeAuthConfig, validateConfig } from "../../core/config";
10
- import { ModelInfo, parseModelFile, toKebabCase, toPascalCase, toCamelCase } from "../../core/scaffold/model-parser";
9
+ import { getBackendLanguage, getFrontendFramework, getConnectorDefinition, getAuthConfig, getFullConfig, getNormalizedAuthConfig, ensureSwallowKitProject, normalizeAuthConfig, validateConfig } from "../../core/config";
10
+ import { generateManagedFunctionConfig, generateReactBffHelper, generateReactBffRoute } from "../../core/scaffold/react-bff-generator";
11
+ import { adaptReactPage, generateReactModelRoutes } from "../../core/scaffold/react-ui-generator";
12
+ import { ModelInfo, parseModelFile, toKebabCase, toCamelCase } from "../../core/scaffold/model-parser";
11
13
  import { MachineCommandError } from "../../machine/errors";
12
14
  import {
13
15
  generateCSharpAzureFunctionsCRUD,
@@ -52,17 +54,44 @@ import {
52
54
  } from "../../core/operations/file-session";
53
55
  import { generateModelContainerMigration } from "../../core/operations/migration-generator";
54
56
  import { recordSessionOperations } from "../../core/project/artifacts";
57
+ import { updateSharedIndex } from "../../core/operations/create-model";
55
58
 
56
59
  const SCAFFOLD_GENERATOR = "scaffold";
57
60
 
58
61
  interface ScaffoldOptions {
59
62
  model: string; // モデルファイルのパス(例: "lib/models/todo.ts" or "todo")
60
63
  functionsDir?: string; // Azure Functions のディレクトリ(デフォルト: "functions")
61
- apiDir?: string; // Next.js API routes のディレクトリ(デフォルト: "app/api")
64
+ apiDir?: string; // BFF directory (Next.js: app/api, React: bff)
62
65
  apiOnly?: boolean; // true の場合、UI を生成しない(デフォルト: false)
66
+ skipInfra?: boolean; // true の場合、infra/ 配下(コンテナー migration)を変更しない
63
67
  expectedSchemaAnalysis?: SchemaAnalysisSnapshot;
64
68
  }
65
69
 
70
+ export function buildInfraSkippedWarning(modelName: string, modelArgument: string): string {
71
+ return `infra-skipped: The Cosmos DB container migration for ${modelName} was not generated (--skip-infra); infra/ was left unchanged. ` +
72
+ `Generate it later with "swallowkit migrations add-model ${modelArgument}", or re-plan scaffold without --skip-infra.`;
73
+ }
74
+
75
+ const USER_SCOPE_FIELD = /^(user|owner|learner|member|account|customer|student|author|creator|profile|person|employee|patient)Id$/i;
76
+
77
+ function isAuthenticationConfigured(authConfig: AuthConfig | undefined): boolean {
78
+ if (!authConfig || authConfig.provider === "none") return false;
79
+ return Boolean(authConfig.provider) || Object.keys(authConfig.schemes ?? {}).length > 0;
80
+ }
81
+
82
+ /** Warn when a model looks per-user (partition key or owner-like id field) but no authentication is configured. */
83
+ export function buildUserScopedDataWarning(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): string | undefined {
84
+ if (isAuthenticationConfigured(authConfig)) return undefined;
85
+ const partitionField = modelInfo.partitionKey.replace(/^\//, "");
86
+ const fieldNames = modelInfo.fields.filter((field) => field.type === "string" && !field.isArray).map((field) => field.name);
87
+ const ownerField = [partitionField, ...fieldNames].find((name) => USER_SCOPE_FIELD.test(name) && fieldNames.includes(name));
88
+ if (!ownerField) return undefined;
89
+ const reason = ownerField === partitionField ? `is the partition key and looks like a per-user owner id` : `looks like a per-user owner id`;
90
+ return `user-scoped-data-without-auth: ${modelInfo.name}.${ownerField} ${reason}, but authentication is not configured. ` +
91
+ `Generated CRUD does not scope data to a principal, so any caller can read and write every ${modelInfo.name} document. ` +
92
+ `Add authentication with "plan auth", restrict or remove the generated write endpoints, and add ai-authored endpoints that derive ${ownerField} from the verified principal instead of the request body.`;
93
+ }
94
+
66
95
  function getMachineAwareStdio(): "inherit" | "pipe" {
67
96
  return process.env.SWALLOWKIT_MACHINE_OUTPUT === "1" ? "pipe" : "inherit";
68
97
  }
@@ -130,7 +159,20 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
130
159
  session.setSchemaAnalysis(schemaAnalysis);
131
160
  console.log(`✅ Model parsed: ${modelInfo.name} (${modelInfo.schemaName})`);
132
161
 
162
+ // Validate policy prerequisites before any generated artifacts are written.
163
+ resolveAuthPolicy(modelInfo, getAuthConfig());
164
+ for (const configName of ["swallowkit.config.js", "swallowkit.config.json", ".swallowkitrc.json"]) {
165
+ session.registerInput(path.join(process.cwd(), configName));
166
+ }
167
+ const userScopeWarning = buildUserScopedDataWarning(modelInfo, getAuthConfig());
168
+ if (userScopeWarning) {
169
+ session.addWarning(userScopeWarning);
170
+ console.warn(`⚠️ ${userScopeWarning}`);
171
+ }
172
+
133
173
  const backendLanguage = getBackendLanguage();
174
+ const frontend = getFrontendFramework();
175
+ const apiDir = options.apiDir || (frontend === "react" ? "bff" : "app/api");
134
176
  console.log(`🧠 Backend language: ${backendLanguage}`);
135
177
 
136
178
  // コネクタモデルかどうかを判定
@@ -181,8 +223,10 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
181
223
  console.log("ℹ️ --api-only skips UI generation only; Functions, BFF routes, OpenAPI, and native schema assets are still updated.");
182
224
  }
183
225
 
226
+ updateSharedIndex(modelPath, modelInfo.schemaName, session);
227
+
184
228
  // 5. Generate BFF callFunction helper
185
- await generateCallFunctionHelper(session);
229
+ await generateCallFunctionHelper(session, apiDir);
186
230
 
187
231
  // 6. Generate Azure Functions code
188
232
  if (isConnectorModel) {
@@ -197,17 +241,24 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
197
241
  }
198
242
  }
199
243
 
200
- // 7. Generate Next.js BFF API Routes
201
- const apiDir = options.apiDir || "app/api";
202
- if (isConnectorModel) {
244
+ // 7. Generate framework-specific BFF routes (the standalone backend is unchanged).
245
+ if (frontend === "react") {
246
+ generateManagedBffRoutes(modelInfo, apiDir, sharedPackageName, session);
247
+ } else if (isConnectorModel) {
203
248
  await generateConnectorBFFRoutesFiles(modelInfo, apiDir, sharedPackageName, session);
204
249
  } else {
205
250
  await generateBFFRoutes(modelInfo, apiDir, sharedPackageName, session);
206
251
  }
207
252
 
208
- // 8. Generate Cosmos DB container Bicep file (skip for connector models)
253
+ // 8. Generate Cosmos DB container Bicep file (skip for connector models or when infra changes are excluded)
209
254
  if (!isConnectorModel) {
210
- await generateCosmosContainer(modelInfo, session);
255
+ if (options.skipInfra) {
256
+ const warning = buildInfraSkippedWarning(modelInfo.name, toKebabCase(modelInfo.name));
257
+ session.addWarning(warning);
258
+ console.warn(`\n⚠️ ${warning}`);
259
+ } else {
260
+ await generateCosmosContainer(modelInfo, session);
261
+ }
211
262
  }
212
263
 
213
264
  // 9. Generate UI components (unless --api-only)
@@ -237,14 +288,14 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
237
288
  console.log("\n📝 Next steps:");
238
289
  console.log(` 1. Review generated files in ${describeFunctionsOutputPath(functionsDir, backendLanguage)} and ${apiDir}/`);
239
290
  if (!options.apiOnly) {
240
- console.log(` 2. Check the generated UI pages in app/${toKebabCase(modelInfo.name)}/`);
291
+ console.log(` 2. Check the generated UI pages in ${frontend === "react" ? "src/pages" : "app"}/${toKebabCase(modelInfo.name)}/`);
241
292
  console.log(" 3. Navigate to the model from the homepage menu");
242
293
  }
243
294
  if (backendLanguage !== "typescript") {
244
295
  console.log(` ${options.apiOnly ? "2" : "4"}. Review generated OpenAPI export and native schema assets in ${functionsDir}/openapi/ and ${functionsDir}/generated/`);
245
296
  }
246
297
  console.log(
247
- ` ${options.apiOnly ? (backendLanguage === "typescript" ? "2" : "3") : (backendLanguage === "typescript" ? "4" : "5")}. Ensure BACKEND_FUNCTIONS_BASE_URL is set in your .env.local file`
298
+ ` ${options.apiOnly ? (backendLanguage === "typescript" ? "2" : "3") : (backendLanguage === "typescript" ? "4" : "5")}. Ensure BACKEND_FUNCTIONS_BASE_URL is set in ${frontend === "react" ? `${apiDir}/local.settings.json` : "your .env.local file"}`
248
299
  );
249
300
  console.log(
250
301
  ` ${options.apiOnly ? (backendLanguage === "typescript" ? "3" : "4") : (backendLanguage === "typescript" ? "5" : "6")}. Configure CosmosDBConnection in functions/local.settings.json`
@@ -258,7 +309,7 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
258
309
  }
259
310
 
260
311
  function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo, session: FileOperationSession, importPath = '@/lib/auth/authenticated-fetch'): void {
261
- const uiPrefix = `app/${toKebabCase(modelInfo.name)}/`;
312
+ const uiPrefix = `${getFrontendFramework() === "react" ? "src/pages" : "app"}/${toKebabCase(modelInfo.name)}/`;
262
313
  for (const operation of session.operations) {
263
314
  if (!operation.path.startsWith(uiPrefix) || !operation.path.endsWith('.tsx') || operation.action === 'delete') continue;
264
315
  let content = session.readFile(operation.absolutePath);
@@ -289,10 +340,43 @@ function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo, session: Fil
289
340
  export function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): ModelAuthPolicy | undefined {
290
341
  // モデルに明示的な authPolicy がある場合はそれを使用
291
342
  if (modelInfo.authPolicy) {
343
+ if (!isAuthenticationConfigured(authConfig)) {
344
+ throw new MachineCommandError(
345
+ "auth-policy-without-auth",
346
+ `Model '${modelInfo.name}' declares authPolicy, but authentication is not configured. No artifacts were written. Run plan auth and apply auth before scaffolding this model.`,
347
+ { model: modelInfo.name },
348
+ "blocked",
349
+ [{
350
+ command: "swallowkit machine plan auth --provider <custom-jwt|swa|external-token>",
351
+ description: "Choose an authentication provider, review and apply its plan, then re-plan scaffold.",
352
+ }]
353
+ );
354
+ }
292
355
  const normalized = normalizeAuthConfig(authConfig);
356
+ const usesNamedPolicy = Boolean(modelInfo.authPolicy.policy ||
357
+ typeof modelInfo.authPolicy.read === "string" || typeof modelInfo.authPolicy.write === "string");
358
+ // Guards resolve each operation independently: a string selects the named router, an array (or
359
+ // the roles fallback) selects the single-provider helper. Named schemes only have the router.
360
+ const operationIsNamed = (selected: string | string[] | undefined) =>
361
+ typeof selected === "string" || (selected === undefined && Boolean(modelInfo.authPolicy!.policy));
362
+ const everyOperationNamed = operationIsNamed(modelInfo.authPolicy.read) && operationIsNamed(modelInfo.authPolicy.write) &&
363
+ !modelInfo.authPolicy.roles;
364
+ if (authConfig?.schemes && !authConfig.provider && !everyOperationNamed) {
365
+ throw new MachineCommandError(
366
+ "auth-policy-requires-named-policy",
367
+ `Model '${modelInfo.name}' declares an authPolicy whose read or write operation does not resolve to a named policy, but authentication uses named schemes. Use authPolicy = { policy: '<name>' } or name both read and write policies (role arrays are not supported with named schemes); no artifacts were written.`,
368
+ { model: modelInfo.name },
369
+ "blocked",
370
+ [{
371
+ command: "swallowkit machine inspect capabilities",
372
+ description: "Define a named authorization policy selecting the required schemes/roles, reference it from the model, then re-plan scaffold.",
373
+ }]
374
+ );
375
+ }
293
376
  for (const policyName of [modelInfo.authPolicy.policy, typeof modelInfo.authPolicy.read === 'string' ? modelInfo.authPolicy.read : undefined, typeof modelInfo.authPolicy.write === 'string' ? modelInfo.authPolicy.write : undefined].filter((value): value is string => Boolean(value))) {
294
377
  if (!normalized?.authorization.policies[policyName]) throw new Error(`Model '${modelInfo.name}' references undefined auth policy '${policyName}'. Define auth.authorization.policies.${policyName}.`);
295
378
  }
379
+ if (usesNamedPolicy) assertNamedSchemesConfigured(modelInfo.name, "authPolicy", authConfig);
296
380
  return modelInfo.authPolicy;
297
381
  }
298
382
 
@@ -307,12 +391,35 @@ export function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig |
307
391
  return {}; // 空のポリシー = 認証のみ、ロール制限なし
308
392
  }
309
393
 
310
- if (defaultPolicy !== 'anonymous') return { policy: defaultPolicy };
394
+ if (defaultPolicy !== 'anonymous') {
395
+ assertNamedSchemesConfigured(modelInfo.name, `auth.authorization.defaultPolicy '${defaultPolicy}'`, authConfig);
396
+ return { policy: defaultPolicy };
397
+ }
311
398
 
312
399
  // defaultPolicy が 'anonymous' なら認証不要
313
400
  return undefined;
314
401
  }
315
402
 
403
+ /**
404
+ * Named authorization policies are enforced by the generated multi-scheme router and scheme-specific
405
+ * client contexts, which exist only for named auth.schemes. A single-provider configuration
406
+ * (auth.provider) has no router, so referencing a named policy would generate code that cannot build.
407
+ */
408
+ function assertNamedSchemesConfigured(modelName: string, source: string, authConfig: AuthConfig | undefined): void {
409
+ if (Object.keys(authConfig?.schemes ?? {}).some((name) => name !== "default")) return;
410
+ throw new MachineCommandError(
411
+ "auth-named-policy-requires-named-scheme",
412
+ `Model '${modelName}' uses a named authorization policy via ${source}, but authentication is configured with a single auth.provider. ` +
413
+ `Named policies require named auth.schemes; no artifacts were written. Use role lists (authPolicy = { roles: [...] } or read/write role arrays) with auth.provider, or add a named scheme.`,
414
+ { model: modelName, source },
415
+ "blocked",
416
+ [{
417
+ command: "swallowkit machine plan auth --provider <swa|external-token|custom-jwt> --scheme <name>",
418
+ description: "Add a named scheme and reference it from auth.authorization.policies, or switch the model to role lists; then re-plan scaffold.",
419
+ }]
420
+ );
421
+ }
422
+
316
423
  /**
317
424
  * モデルファイルのパスを解決
318
425
  */
@@ -384,10 +491,17 @@ function readSharedPackageName(): string {
384
491
  /**
385
492
  * Generate BFF callFunction helper (lib/api/call-function.ts)
386
493
  */
387
- async function generateCallFunctionHelper(session: FileOperationSession): Promise<void> {
494
+ async function generateCallFunctionHelper(session: FileOperationSession, apiDir: string): Promise<void> {
388
495
  console.log("\n📦 Generating BFF callFunction helper...");
389
496
 
390
497
  const cwd = process.cwd();
498
+ if (getFrontendFramework() === "react") {
499
+ const projectName = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8")).name;
500
+ session.writeFile(path.join(cwd, apiDir, "src", "lib", "call-function.ts"), generateReactBffHelper(projectName, getAuthConfig()), {
501
+ ownership: "managed", generator: SCAFFOLD_GENERATOR,
502
+ });
503
+ return;
504
+ }
391
505
 
392
506
  const helperDir = path.join(cwd, "lib", "api");
393
507
  const helperPath = path.join(helperDir, "call-function.ts");
@@ -800,6 +914,13 @@ function updatePythonFunctionRegistrations(functionAppPath: string, registration
800
914
  /**
801
915
  * Next.js BFF API Routes を生成
802
916
  */
917
+ function generateManagedBffRoutes(modelInfo: ModelInfo, apiDir: string, sharedPackageName: string, session: FileOperationSession): void {
918
+ const name = toCamelCase(modelInfo.name);
919
+ const meta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
920
+ session.writeFile(path.join(process.cwd(), apiDir, "src", name, "index.ts"), generateReactBffRoute(modelInfo, sharedPackageName), meta);
921
+ session.writeFile(path.join(process.cwd(), apiDir, name, "function.json"), generateManagedFunctionConfig(`${name}/{id?}`, ["GET", "POST", "PUT", "DELETE"], name), meta);
922
+ }
923
+
803
924
  async function generateBFFRoutes(
804
925
  modelInfo: any,
805
926
  apiDir: string,
@@ -867,9 +988,10 @@ export async function generateUIComponents(modelInfo: ModelInfo, sharedPackageNa
867
988
  const modelKebab = toKebabCase(modelInfo.name);
868
989
  const modelName = modelInfo.name;
869
990
  const cwd = process.cwd();
991
+ const react = getFrontendFramework() === "react";
870
992
 
871
993
  // Create directory structure: app/[model]/
872
- const modelDir = path.join(cwd, "app", modelKebab);
994
+ const modelDir = path.join(cwd, react ? path.join("src", "pages") : "app", modelKebab);
873
995
  const componentsDir = path.join(modelDir, "_components");
874
996
  const newDir = path.join(modelDir, "new");
875
997
  const idDir = path.join(modelDir, "[id]");
@@ -884,14 +1006,16 @@ export async function generateUIComponents(modelInfo: ModelInfo, sharedPackageNa
884
1006
  const authLayout = generateAuthLayout(modelInfo, authOptions);
885
1007
 
886
1008
  const meta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
887
- session.writeFile(path.join(modelDir, "page.tsx"), listPage, meta);
888
- session.writeFile(path.join(idDir, "page.tsx"), detailPage, meta);
889
- session.writeFile(path.join(componentsDir, `${modelName}Form.tsx`), formComponent, meta);
890
- session.writeFile(path.join(newDir, "page.tsx"), newPage, meta);
891
- session.writeFile(path.join(editDir, "page.tsx"), editPage, meta);
1009
+ const adapt = react ? adaptReactPage : (source: string) => source;
1010
+ session.writeFile(path.join(modelDir, "page.tsx"), adapt(listPage), meta);
1011
+ session.writeFile(path.join(idDir, "page.tsx"), adapt(detailPage), meta);
1012
+ session.writeFile(path.join(componentsDir, `${modelName}Form.tsx`), adapt(formComponent), meta);
1013
+ session.writeFile(path.join(newDir, "page.tsx"), adapt(newPage), meta);
1014
+ session.writeFile(path.join(editDir, "page.tsx"), adapt(editPage), meta);
892
1015
  if (authLayout) {
893
1016
  session.writeFile(path.join(modelDir, "layout.tsx"), authLayout, meta);
894
1017
  }
1018
+ if (react) session.writeFile(path.join(modelDir, "routes.tsx"), generateReactModelRoutes(modelInfo, authOptions), meta);
895
1019
 
896
1020
  console.log(`✅ Created: ${path.join(modelDir, "page.tsx")}`);
897
1021
  console.log(`✅ Created: ${path.join(idDir, "page.tsx")}`);
@@ -971,4 +1095,3 @@ ${models.map(m => ` { name: '${m.name}', path: '${m.path}', label: '${m.label
971
1095
  session.writeFile(configPath, newConfigContent, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
972
1096
  console.log(`✅ Added ${modelInfo.name} to navigation menu`);
973
1097
  }
974
-
package/src/cli/index.ts CHANGED
@@ -5,9 +5,11 @@ import { readFileSync } from "fs";
5
5
  import * as path from "path";
6
6
  import { Command } from "commander";
7
7
  import { initCommand, devCommand, devSeedsCommand, scaffoldCommand, createModelCommand } from "./commands";
8
- import { provisionCommand } from "./commands/provision";
8
+ import { createProvisionCommand } from "./commands/provision";
9
+ import { AzureCliDiagnostic, formatAzureDiagnostic } from "../utils/azure-diagnostics";
9
10
  import { createMigrationCommand } from "./commands/create-migration";
10
11
  import { migrationsCommand } from "./commands/migrations";
12
+ import { configureMcpCommand } from "./commands/configure-mcp";
11
13
  import { addConnectorCommand } from "./commands/add-connector";
12
14
  import { addAuthCommand } from "./commands/add-auth";
13
15
  import { planScaffoldOperation } from "../core/operations/scaffold-plan";
@@ -15,6 +17,7 @@ import { inspectArtifacts } from "../core/project/artifacts";
15
17
  import { detectDrift } from "../core/project/drift";
16
18
  import { runVerify } from "../core/verify";
17
19
  import { isMachineCommand, runMachineCli } from "../machine";
20
+ import { MachineCommandError } from "../machine/errors";
18
21
 
19
22
  const packageJsonPath = path.resolve(__dirname, "../../package.json");
20
23
  export const CLI_VERSION = JSON.parse(readFileSync(packageJsonPath, "utf8")).version as string;
@@ -24,6 +27,7 @@ const DEV_OPTION_ARITY = new Map<string, 0 | 1>([
24
27
  ["--port", 1],
25
28
  ["-f", 1],
26
29
  ["--functions-port", 1],
30
+ ["--bff-port", 1],
27
31
  ["--host", 1],
28
32
  ["--seed-env", 1],
29
33
  ["-o", 0],
@@ -102,7 +106,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
102
106
 
103
107
  program
104
108
  .name("swallowkit")
105
- .description("Next.js framework optimized for Azure deployment - Automatically splits SSR into individual Azure Functions")
109
+ .description("Schema-driven React and Next.js applications on Azure Static Web Apps and Functions")
106
110
  .version(CLI_VERSION);
107
111
 
108
112
  // Register commands
@@ -110,6 +114,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
110
114
  .command("init [project-name]")
111
115
  .description("Initialize a new SwallowKit project")
112
116
  .option("--template <template>", "Template to use", "default")
117
+ .option("--frontend <framework>", "Frontend framework: nextjs | react (defaults to nextjs in non-interactive mode)")
113
118
  .option("--next-version <version>", "Next.js version to install (e.g., 16.0.7, latest)", "latest")
114
119
  .option("--package-manager <manager>", "Package manager: npm | pnpm (defaults to invocation context)")
115
120
  .option("--cicd <provider>", "CI/CD provider: github | azure | skip")
@@ -121,6 +126,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
121
126
  initCommand({
122
127
  name: projectName || "swallowkit-app",
123
128
  template: options.template,
129
+ frontend: options.frontend,
124
130
  nextVersion: options.nextVersion,
125
131
  packageManager: options.packageManager,
126
132
  cicd: options.cicd,
@@ -134,9 +140,10 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
134
140
  program.addCommand(devCommandOverride);
135
141
  program.addCommand(devSeedsCommand);
136
142
 
137
- program.addCommand(provisionCommand);
143
+ program.addCommand(createProvisionCommand());
138
144
  program.addCommand(migrationsCommand);
139
145
  program.addCommand(createMigrationCommand);
146
+ program.addCommand(configureMcpCommand);
140
147
 
141
148
  program
142
149
  .command("create-model <names...>")
@@ -153,10 +160,11 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
153
160
 
154
161
  program
155
162
  .command("scaffold <model>")
156
- .description("Generate CRUD code for Azure Functions and Next.js BFF from Zod models")
163
+ .description("Generate CRUD code for Azure Functions, BFF, and the selected frontend from Zod models")
157
164
  .option("--functions-dir <dir>", "Azure Functions directory", "functions")
158
- .option("--api-dir <dir>", "Next.js API routes directory", "app/api")
165
+ .option("--api-dir <dir>", "BFF routes directory (Next.js: app/api; React: bff)")
159
166
  .option("--api-only", "Skip UI components; still update Functions, BFF routes, OpenAPI, and native schema assets", false)
167
+ .option("--skip-infra", "Do not create or update anything under infra/ (generate the container migration later with migrations add-model)", false)
160
168
  .option("--dry-run", "Show planned file changes without writing anything", false)
161
169
  .action(async (model, options) => {
162
170
  if (options.dryRun) {
@@ -165,6 +173,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
165
173
  functionsDir: options.functionsDir,
166
174
  apiDir: options.apiDir,
167
175
  apiOnly: options.apiOnly,
176
+ skipInfra: options.skipInfra,
168
177
  });
169
178
  return;
170
179
  }
@@ -174,6 +183,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
174
183
  functionsDir: options.functionsDir,
175
184
  apiDir: options.apiDir,
176
185
  apiOnly: options.apiOnly,
186
+ skipInfra: options.skipInfra,
177
187
  });
178
188
  });
179
189
 
@@ -214,8 +224,8 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
214
224
 
215
225
  program
216
226
  .command("verify")
217
- .description("Run verification checks (structure, drift, typecheck)")
218
- .option("--checks <ids>", "Comma-separated check ids to run (structure,drift,typecheck)")
227
+ .description("Run verification checks (structure, drift, typecheck, backend-build)")
228
+ .option("--checks <ids>", "Comma-separated check ids (structure,drift,typecheck,backend-build,build,lint,test); quote CSV values in PowerShell")
219
229
  .action(async (options) => {
220
230
  await runVerifyCommand({ checks: options.checks });
221
231
  });
@@ -223,7 +233,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
223
233
  return program;
224
234
  }
225
235
 
226
- async function runScaffoldDryRun(options: { model: string; functionsDir?: string; apiDir?: string; apiOnly?: boolean }): Promise<void> {
236
+ async function runScaffoldDryRun(options: { model: string; functionsDir?: string; apiDir?: string; apiOnly?: boolean; skipInfra?: boolean }): Promise<void> {
227
237
  console.log("🔍 Scaffold dry-run — no files will be written.\n");
228
238
 
229
239
  const plan = await planScaffoldOperation(options);
@@ -261,7 +271,7 @@ async function runScaffoldDryRun(options: { model: string; functionsDir?: string
261
271
  console.log(`⚠️ ${plan.conflicts.length} file(s) were modified after generation and would be overwritten.`);
262
272
  console.log(` Review the conflicts, then run: swallowkit scaffold ${options.model} (or machine apply scaffold --plan ${plan.planId} --approve)`);
263
273
  } else {
264
- console.log(` Run without --dry-run to apply: swallowkit scaffold ${options.model}`);
274
+ console.log(` Run without --dry-run to apply: swallowkit scaffold ${options.model}${options.skipInfra ? " --skip-infra" : ""}`);
265
275
  }
266
276
  }
267
277
 
@@ -348,9 +358,35 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
348
358
  await createProgram().parseAsync(normalizeDevCommandArgv(argv));
349
359
  }
350
360
 
361
+ /** Human-readable error output for regular CLI commands (machine commands print JSON). */
362
+ export function formatCliError(error: unknown): string {
363
+ if (error instanceof MachineCommandError) {
364
+ const lines = [`❌ ${error.message}`, ` (code: ${error.code})`];
365
+ const details = error.details as Record<string, unknown> | undefined;
366
+ if (details && typeof details === "object") {
367
+ if (typeof details.nextAction === "string") lines.push(` Next: ${details.nextAction}`);
368
+ if (Array.isArray(details.changedFiles)) lines.push(` Changed files: ${details.changedFiles.join(", ")}`);
369
+ if (typeof details.runId === "string") lines.push(` Run: ${details.runId}`);
370
+ if (details.deployment && typeof details.deployment === "object") {
371
+ const deployment = details.deployment as { state?: string; reference?: { name?: string; id?: string; resourceGroup?: string; subscription?: string } };
372
+ lines.push(` Deployment: ${deployment.reference?.id ?? deployment.reference?.name ?? "unknown"}; state: ${deployment.state ?? "unknown"}`);
373
+ }
374
+ if (Array.isArray(details.diagnostics)) {
375
+ for (const diagnostic of details.diagnostics as AzureCliDiagnostic[]) lines.push(formatAzureDiagnostic(diagnostic));
376
+ }
377
+ }
378
+ for (const action of error.nextActions ?? []) lines.push(` Next: ${action.command} — ${action.description}`);
379
+ return lines.join("\n");
380
+ }
381
+ if (error instanceof Error) {
382
+ return process.env.SWALLOWKIT_DEBUG ? String(error.stack ?? error.message) : `❌ ${error.message}`;
383
+ }
384
+ return `❌ ${String(error)}`;
385
+ }
386
+
351
387
  if (require.main === module) {
352
388
  void runCli().catch((error) => {
353
- console.error(error);
389
+ console.error(formatCliError(error));
354
390
  process.exitCode = 1;
355
391
  });
356
392
  }
@@ -1,7 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import { createRequire } from "module";
3
3
  import * as path from "path";
4
- import { AuthConfig, AuthProvider, BackendLanguage, ConnectorDefinition, NormalizedAuthConfig, SwallowKitConfig } from "../types";
4
+ import { AuthConfig, AuthProvider, BackendLanguage, ConnectorDefinition, FrontendFramework, NormalizedAuthConfig, SwallowKitConfig } from "../types";
5
5
  import { detectFromProject, getCommands } from "../utils/package-manager";
6
6
 
7
7
  const requireFromHere = createRequire(__filename);
@@ -21,6 +21,7 @@ const VALID_BACKEND_LANGUAGES: BackendLanguage[] = ["typescript", "csharp", "pyt
21
21
  * デフォルト設定
22
22
  */
23
23
  const DEFAULT_CONFIG: SwallowKitConfig = {
24
+ frontend: { framework: "nextjs" },
24
25
  database: {},
25
26
  backend: {
26
27
  language: "typescript",
@@ -78,6 +79,10 @@ export function loadConfig(configPath?: string, throwOnError = false, quiet = fa
78
79
  */
79
80
  function mergeConfig(defaultConfig: SwallowKitConfig, userConfig: Partial<SwallowKitConfig>): SwallowKitConfig {
80
81
  return {
82
+ frontend: {
83
+ ...defaultConfig.frontend,
84
+ ...userConfig.frontend,
85
+ },
81
86
  database: {
82
87
  ...defaultConfig.database,
83
88
  ...userConfig.database,
@@ -108,6 +113,9 @@ function mergeConfig(defaultConfig: SwallowKitConfig, userConfig: Partial<Swallo
108
113
  export function generateConfig(outputPath: string = "swallowkit.config.json"): void {
109
114
  const config = {
110
115
  $schema: "https://swallowkit.dev/schema.json",
116
+ frontend: {
117
+ framework: "nextjs",
118
+ },
111
119
  database: {
112
120
  connectionString: "your-cosmos-connection-string",
113
121
  databaseName: "SwallowKitDB",
@@ -176,6 +184,14 @@ export function getBackendLanguage(configPath?: string): BackendLanguage {
176
184
  return "typescript";
177
185
  }
178
186
 
187
+ export function getFrontendFramework(configPath?: string): FrontendFramework {
188
+ const framework = getFullConfig(configPath).frontend?.framework ?? "nextjs";
189
+ if (framework !== "nextjs" && framework !== "react") {
190
+ throw new Error(`Unsupported frontend framework '${framework}'; use nextjs or react`);
191
+ }
192
+ return framework;
193
+ }
194
+
179
195
  /**
180
196
  * 完全な設定を取得(ファイル + 環境変数)
181
197
  */
@@ -250,6 +266,10 @@ function hasConfiguredDatabase(config: SwallowKitConfig): boolean {
250
266
  export function validateConfig(config: SwallowKitConfig): { valid: boolean; errors: string[] } {
251
267
  const errors: string[] = [];
252
268
 
269
+ if (config.frontend?.framework && !["nextjs", "react"].includes(config.frontend.framework)) {
270
+ errors.push("Frontend framework must be one of: nextjs, react");
271
+ }
272
+
253
273
  const auth = normalizeAuthConfig(config.auth);
254
274
  if (auth) {
255
275
  for (const [name, scheme] of Object.entries(auth.schemes)) {
@@ -1,4 +1,5 @@
1
1
  import * as path from "path";
2
+ import * as ts from "typescript";
2
3
  import { ensureSwallowKitProject, loadConfig } from "../config";
3
4
  import { toPascalCase } from "../scaffold/model-parser";
4
5
  import { syncProjectManifest } from "../project/manifest";
@@ -90,21 +91,58 @@ export const connectorConfig = {
90
91
  `;
91
92
  }
92
93
 
93
- function updateSharedIndex(kebabName: string, pascalName: string, session: FileOperationSession): boolean {
94
+ export function updateSharedIndex(modelPath: string, schemaName: string, session: FileOperationSession): boolean {
94
95
  const indexPath = path.join(process.cwd(), "shared", "index.ts");
95
-
96
- if (!session.fileExists(indexPath)) {
97
- return false;
96
+ session.registerInput(indexPath);
97
+ const content = session.fileExists(indexPath) ? session.readFile(indexPath) : "";
98
+ const modulePath = `./${path.relative(path.dirname(indexPath), modelPath).replace(/\\/g, "/").replace(/\.ts$/, "")}`;
99
+ const source = ts.createSourceFile(indexPath, content, ts.ScriptTarget.Latest, true);
100
+ // Text edits that turn a type-only re-export of this schema into nothing (a value export of the same
101
+ // name also exports its type); keeping both would be a duplicate-identifier error.
102
+ const edits: Array<{ start: number; end: number; text: string }> = [];
103
+ let hasValueExport = false;
104
+ for (const statement of source.statements) {
105
+ if (!ts.isExportDeclaration(statement) || !statement.moduleSpecifier || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
106
+ const exportedPath = statement.moduleSpecifier.text.replace(/\.(ts|js)$/, "");
107
+ if (exportedPath !== modulePath) continue;
108
+ if (!statement.exportClause) {
109
+ if (!statement.isTypeOnly) hasValueExport = true;
110
+ continue;
111
+ }
112
+ if (!ts.isNamedExports(statement.exportClause)) continue;
113
+ const elements = statement.exportClause.elements;
114
+ const matches = elements.filter((element) =>
115
+ element.name.text === schemaName && (element.propertyName?.text ?? element.name.text) === schemaName);
116
+ for (const element of matches) {
117
+ if (!statement.isTypeOnly && !element.isTypeOnly) {
118
+ hasValueExport = true;
119
+ } else if (!statement.isTypeOnly) {
120
+ // export { type Todo } -> export { Todo }
121
+ edits.push({ start: element.getStart(source), end: element.getEnd(), text: schemaName });
122
+ hasValueExport = true;
123
+ } else if (elements.length === 1) {
124
+ let end = statement.getEnd();
125
+ if (content.slice(end, end + 2) === "\r\n") end += 2;
126
+ else if (content[end] === "\n") end += 1;
127
+ edits.push({ start: statement.getStart(source), end, text: "" });
128
+ } else {
129
+ const index = elements.indexOf(element);
130
+ const start = index === 0 ? element.getStart(source) : elements[index - 1].getEnd();
131
+ const end = index === 0 ? elements[index + 1].getStart(source) : element.getEnd();
132
+ edits.push({ start, end, text: "" });
133
+ }
134
+ }
98
135
  }
99
-
100
- const content = session.readFile(indexPath);
101
- const exportLine = `export { ${pascalName} } from './models/${kebabName}';`;
102
-
103
- if (content.includes(exportLine)) {
104
- return false;
136
+ let updated = content;
137
+ for (const edit of edits.sort((left, right) => right.start - left.start)) {
138
+ updated = `${updated.slice(0, edit.start)}${edit.text}${updated.slice(edit.end)}`;
105
139
  }
106
-
107
- session.writeFile(indexPath, `${content}${exportLine}\n`, {
140
+ if (hasValueExport && updated === content) return false;
141
+ if (!hasValueExport) {
142
+ const exportLine = `export { ${schemaName} } from '${modulePath}';`;
143
+ updated = `${updated}${updated && !updated.endsWith("\n") ? "\n" : ""}${exportLine}\n`;
144
+ }
145
+ session.writeFile(indexPath, updated, {
108
146
  ownership: "extension-point",
109
147
  generator: CREATE_MODEL_GENERATOR,
110
148
  });
@@ -172,7 +210,7 @@ export async function createModelOperation(options: CreateModelOperationOptions)
172
210
  });
173
211
  createdFiles.push(filePath.replace(/\\/g, "/"));
174
212
 
175
- if (updateSharedIndex(kebabName, pascalName, session)) {
213
+ if (updateSharedIndex(resolveFromRoot(filePath), pascalName, session)) {
176
214
  updatedIndex = true;
177
215
  }
178
216
  }
@@ -152,6 +152,19 @@ param location string
152
152
  return migration;
153
153
  }
154
154
 
155
+ const SUFFIXED_COSMOS_ACCOUNT_NAME = "'cosmos-${take(toLower(projectName), 30)}-${take(uniqueString(resourceGroup().id, projectName), 6)}'";
156
+ const LEGACY_COSMOS_ACCOUNT_NAME = "'cosmos-${projectName}'";
157
+
158
+ // Projects initialized before resource-group suffixes keep their original
159
+ // account name so model migrations target the account their baseline created.
160
+ function cosmosAccountNameExpression(session: FileOperationSession): string {
161
+ const mainBicep = path.join(session.rootDirectory, "infra", "main.bicep");
162
+ if (session.fileExists(mainBicep) && /var\s+resourceSuffix\s*=/.test(session.readFile(mainBicep))) {
163
+ return SUFFIXED_COSMOS_ACCOUNT_NAME;
164
+ }
165
+ return LEGACY_COSMOS_ACCOUNT_NAME;
166
+ }
167
+
155
168
  export function generateModelContainerMigration(
156
169
  model: { name: string; partitionKey: string },
157
170
  session: FileOperationSession = new FileOperationSession("commit")
@@ -175,7 +188,7 @@ param projectName string
175
188
  param location string
176
189
 
177
190
  resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
178
- name: 'cosmos-\${projectName}'
191
+ name: ${cosmosAccountNameExpression(session)}
179
192
  }
180
193
 
181
194
  resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' existing = {