swallowkit 1.0.0-beta.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/README.ja.md +87 -181
  2. package/README.md +87 -238
  3. package/dist/__tests__/fixtures.d.ts +12 -0
  4. package/dist/__tests__/fixtures.d.ts.map +1 -1
  5. package/dist/__tests__/fixtures.js +101 -0
  6. package/dist/__tests__/fixtures.js.map +1 -1
  7. package/dist/cli/commands/add-auth.d.ts +15 -0
  8. package/dist/cli/commands/add-auth.d.ts.map +1 -0
  9. package/dist/cli/commands/add-auth.js +790 -0
  10. package/dist/cli/commands/add-auth.js.map +1 -0
  11. package/dist/cli/commands/add-connector.d.ts +20 -0
  12. package/dist/cli/commands/add-connector.d.ts.map +1 -0
  13. package/dist/cli/commands/add-connector.js +164 -0
  14. package/dist/cli/commands/add-connector.js.map +1 -0
  15. package/dist/cli/commands/cosmos-worktree-isolation.d.ts +25 -0
  16. package/dist/cli/commands/cosmos-worktree-isolation.d.ts.map +1 -0
  17. package/dist/cli/commands/cosmos-worktree-isolation.js +124 -0
  18. package/dist/cli/commands/cosmos-worktree-isolation.js.map +1 -0
  19. package/dist/cli/commands/create-model.d.ts +1 -4
  20. package/dist/cli/commands/create-model.d.ts.map +1 -1
  21. package/dist/cli/commands/create-model.js +20 -82
  22. package/dist/cli/commands/create-model.js.map +1 -1
  23. package/dist/cli/commands/dev-seeds.d.ts +27 -0
  24. package/dist/cli/commands/dev-seeds.d.ts.map +1 -1
  25. package/dist/cli/commands/dev-seeds.js +188 -6
  26. package/dist/cli/commands/dev-seeds.js.map +1 -1
  27. package/dist/cli/commands/dev.d.ts +30 -0
  28. package/dist/cli/commands/dev.d.ts.map +1 -1
  29. package/dist/cli/commands/dev.js +495 -129
  30. package/dist/cli/commands/dev.js.map +1 -1
  31. package/dist/cli/commands/init.d.ts +70 -0
  32. package/dist/cli/commands/init.d.ts.map +1 -1
  33. package/dist/cli/commands/init.js +797 -768
  34. package/dist/cli/commands/init.js.map +1 -1
  35. package/dist/cli/commands/scaffold.d.ts +24 -3
  36. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  37. package/dist/cli/commands/scaffold.js +408 -189
  38. package/dist/cli/commands/scaffold.js.map +1 -1
  39. package/dist/cli/index.d.ts +5 -1
  40. package/dist/cli/index.d.ts.map +1 -1
  41. package/dist/cli/index.js +335 -58
  42. package/dist/cli/index.js.map +1 -1
  43. package/dist/core/config.d.ts +13 -3
  44. package/dist/core/config.d.ts.map +1 -1
  45. package/dist/core/config.js +151 -8
  46. package/dist/core/config.js.map +1 -1
  47. package/dist/core/mock/connector-mock-server.d.ts +101 -0
  48. package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
  49. package/dist/core/mock/connector-mock-server.js +480 -0
  50. package/dist/core/mock/connector-mock-server.js.map +1 -0
  51. package/dist/core/mock/zod-mock-generator.d.ts +14 -0
  52. package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
  53. package/dist/core/mock/zod-mock-generator.js +163 -0
  54. package/dist/core/mock/zod-mock-generator.js.map +1 -0
  55. package/dist/core/operations/auth-operations.d.ts +61 -0
  56. package/dist/core/operations/auth-operations.d.ts.map +1 -0
  57. package/dist/core/operations/auth-operations.js +272 -0
  58. package/dist/core/operations/auth-operations.js.map +1 -0
  59. package/dist/core/operations/create-model.d.ts +15 -0
  60. package/dist/core/operations/create-model.d.ts.map +1 -0
  61. package/dist/core/operations/create-model.js +185 -0
  62. package/dist/core/operations/create-model.js.map +1 -0
  63. package/dist/core/operations/file-session.d.ts +56 -0
  64. package/dist/core/operations/file-session.d.ts.map +1 -0
  65. package/dist/core/operations/file-session.js +203 -0
  66. package/dist/core/operations/file-session.js.map +1 -0
  67. package/dist/core/operations/provision-operations.d.ts +52 -0
  68. package/dist/core/operations/provision-operations.d.ts.map +1 -0
  69. package/dist/core/operations/provision-operations.js +211 -0
  70. package/dist/core/operations/provision-operations.js.map +1 -0
  71. package/dist/core/operations/runtime.d.ts +37 -0
  72. package/dist/core/operations/runtime.d.ts.map +1 -0
  73. package/dist/core/operations/runtime.js +248 -0
  74. package/dist/core/operations/runtime.js.map +1 -0
  75. package/dist/core/operations/scaffold-apply.d.ts +21 -0
  76. package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
  77. package/dist/core/operations/scaffold-apply.js +57 -0
  78. package/dist/core/operations/scaffold-apply.js.map +1 -0
  79. package/dist/core/operations/scaffold-machine.d.ts +16 -0
  80. package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
  81. package/dist/core/operations/scaffold-machine.js +51 -0
  82. package/dist/core/operations/scaffold-machine.js.map +1 -0
  83. package/dist/core/operations/scaffold-plan.d.ts +43 -0
  84. package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
  85. package/dist/core/operations/scaffold-plan.js +187 -0
  86. package/dist/core/operations/scaffold-plan.js.map +1 -0
  87. package/dist/core/project/agent-instructions.d.ts +29 -0
  88. package/dist/core/project/agent-instructions.d.ts.map +1 -0
  89. package/dist/core/project/agent-instructions.js +160 -0
  90. package/dist/core/project/agent-instructions.js.map +1 -0
  91. package/dist/core/project/artifacts.d.ts +58 -0
  92. package/dist/core/project/artifacts.d.ts.map +1 -0
  93. package/dist/core/project/artifacts.js +179 -0
  94. package/dist/core/project/artifacts.js.map +1 -0
  95. package/dist/core/project/boundaries.d.ts +50 -0
  96. package/dist/core/project/boundaries.d.ts.map +1 -0
  97. package/dist/core/project/boundaries.js +117 -0
  98. package/dist/core/project/boundaries.js.map +1 -0
  99. package/dist/core/project/capabilities.d.ts +58 -0
  100. package/dist/core/project/capabilities.d.ts.map +1 -0
  101. package/dist/core/project/capabilities.js +135 -0
  102. package/dist/core/project/capabilities.js.map +1 -0
  103. package/dist/core/project/drift.d.ts +29 -0
  104. package/dist/core/project/drift.d.ts.map +1 -0
  105. package/dist/core/project/drift.js +188 -0
  106. package/dist/core/project/drift.js.map +1 -0
  107. package/dist/core/project/infra.d.ts +49 -0
  108. package/dist/core/project/infra.d.ts.map +1 -0
  109. package/dist/core/project/infra.js +167 -0
  110. package/dist/core/project/infra.js.map +1 -0
  111. package/dist/core/project/manifest.d.ts +92 -0
  112. package/dist/core/project/manifest.d.ts.map +1 -0
  113. package/dist/core/project/manifest.js +321 -0
  114. package/dist/core/project/manifest.js.map +1 -0
  115. package/dist/core/project/state.d.ts +15 -0
  116. package/dist/core/project/state.d.ts.map +1 -0
  117. package/dist/core/project/state.js +106 -0
  118. package/dist/core/project/state.js.map +1 -0
  119. package/dist/core/project/validation.d.ts +20 -0
  120. package/dist/core/project/validation.d.ts.map +1 -0
  121. package/dist/core/project/validation.js +274 -0
  122. package/dist/core/project/validation.js.map +1 -0
  123. package/dist/core/project/workflows.d.ts +43 -0
  124. package/dist/core/project/workflows.d.ts.map +1 -0
  125. package/dist/core/project/workflows.js +299 -0
  126. package/dist/core/project/workflows.js.map +1 -0
  127. package/dist/core/scaffold/auth-generator.d.ts +63 -0
  128. package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
  129. package/dist/core/scaffold/auth-generator.js +1774 -0
  130. package/dist/core/scaffold/auth-generator.js.map +1 -0
  131. package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
  132. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
  133. package/dist/core/scaffold/connector-functions-generator.js +1029 -0
  134. package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
  135. package/dist/core/scaffold/functions-generator.d.ts +4 -3
  136. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  137. package/dist/core/scaffold/functions-generator.js +405 -134
  138. package/dist/core/scaffold/functions-generator.js.map +1 -1
  139. package/dist/core/scaffold/model-parser.d.ts +20 -0
  140. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  141. package/dist/core/scaffold/model-parser.js +229 -78
  142. package/dist/core/scaffold/model-parser.js.map +1 -1
  143. package/dist/core/scaffold/native-schema-generator.d.ts +19 -0
  144. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
  145. package/dist/core/scaffold/native-schema-generator.js +753 -0
  146. package/dist/core/scaffold/native-schema-generator.js.map +1 -0
  147. package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
  148. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  149. package/dist/core/scaffold/nextjs-generator.js +152 -8
  150. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  151. package/dist/core/scaffold/ui-generator.d.ts +17 -4
  152. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  153. package/dist/core/scaffold/ui-generator.js +174 -34
  154. package/dist/core/scaffold/ui-generator.js.map +1 -1
  155. package/dist/core/verify/index.d.ts +69 -0
  156. package/dist/core/verify/index.d.ts.map +1 -0
  157. package/dist/core/verify/index.js +412 -0
  158. package/dist/core/verify/index.js.map +1 -0
  159. package/dist/machine/contracts.d.ts +24 -0
  160. package/dist/machine/contracts.d.ts.map +1 -0
  161. package/dist/machine/contracts.js +3 -0
  162. package/dist/machine/contracts.js.map +1 -0
  163. package/dist/machine/errors.d.ts +14 -0
  164. package/dist/machine/errors.d.ts.map +1 -0
  165. package/dist/machine/errors.js +42 -0
  166. package/dist/machine/errors.js.map +1 -0
  167. package/dist/machine/index.d.ts +3 -0
  168. package/dist/machine/index.d.ts.map +1 -0
  169. package/dist/machine/index.js +566 -0
  170. package/dist/machine/index.js.map +1 -0
  171. package/dist/mcp/index.d.ts +25 -0
  172. package/dist/mcp/index.d.ts.map +1 -0
  173. package/dist/mcp/index.js +425 -0
  174. package/dist/mcp/index.js.map +1 -0
  175. package/dist/types/index.d.ts +105 -0
  176. package/dist/types/index.d.ts.map +1 -1
  177. package/dist/utils/package-manager.d.ts +7 -11
  178. package/dist/utils/package-manager.d.ts.map +1 -1
  179. package/dist/utils/package-manager.js +42 -17
  180. package/dist/utils/package-manager.js.map +1 -1
  181. package/dist/utils/python-uv.d.ts +21 -0
  182. package/dist/utils/python-uv.d.ts.map +1 -0
  183. package/dist/utils/python-uv.js +111 -0
  184. package/dist/utils/python-uv.js.map +1 -0
  185. package/dist/version.d.ts +2 -0
  186. package/dist/version.d.ts.map +1 -0
  187. package/dist/version.js +49 -0
  188. package/dist/version.js.map +1 -0
  189. package/package.json +15 -17
  190. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +716 -22
  191. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +19 -7
  192. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +20 -12
  193. package/src/__tests__/agent-instructions.test.ts +237 -0
  194. package/src/__tests__/agent-loop-phase34.test.ts +513 -0
  195. package/src/__tests__/auth.test.ts +770 -0
  196. package/src/__tests__/bff-swa-auth.test.ts +275 -0
  197. package/src/__tests__/config.test.ts +176 -3
  198. package/src/__tests__/connector-functions-generator.test.ts +288 -0
  199. package/src/__tests__/connector-mock-server.test.ts +459 -0
  200. package/src/__tests__/connector-model-bff.test.ts +162 -0
  201. package/src/__tests__/cosmos-worktree-isolation.test.ts +149 -0
  202. package/src/__tests__/dev-seeds.test.ts +124 -0
  203. package/src/__tests__/dev.test.ts +254 -0
  204. package/src/__tests__/file-session.test.ts +98 -0
  205. package/src/__tests__/fixtures.ts +101 -0
  206. package/src/__tests__/functions-generator.test.ts +137 -1
  207. package/src/__tests__/init.test.ts +386 -1
  208. package/src/__tests__/jest-globals.d.ts +8 -0
  209. package/src/__tests__/machine.test.ts +371 -0
  210. package/src/__tests__/mcp.test.ts +253 -0
  211. package/src/__tests__/model-parser.test.ts +142 -0
  212. package/src/__tests__/multi-auth.test.ts +160 -0
  213. package/src/__tests__/nextjs-generator.test.ts +8 -0
  214. package/src/__tests__/package-manager.test.ts +59 -4
  215. package/src/__tests__/plan-apply.test.ts +499 -0
  216. package/src/__tests__/python-uv.test.ts +48 -0
  217. package/src/__tests__/scaffold.test.ts +287 -24
  218. package/src/__tests__/tsconfig.json +15 -0
  219. package/src/__tests__/ui-generator.test.ts +104 -0
  220. package/src/__tests__/zod-mock-generator.test.ts +132 -0
  221. package/src/cli/commands/add-auth.ts +853 -0
  222. package/src/cli/commands/add-connector.ts +159 -0
  223. package/src/cli/commands/cosmos-worktree-isolation.ts +130 -0
  224. package/src/cli/commands/create-model.ts +21 -100
  225. package/src/cli/commands/dev-seeds.ts +278 -7
  226. package/src/cli/commands/dev.ts +607 -160
  227. package/src/cli/commands/init.ts +880 -808
  228. package/src/cli/commands/scaffold.ts +531 -236
  229. package/src/cli/index.ts +338 -60
  230. package/src/core/config.ts +145 -9
  231. package/src/core/mock/connector-mock-server.ts +555 -0
  232. package/src/core/mock/zod-mock-generator.ts +205 -0
  233. package/src/core/operations/auth-operations.ts +360 -0
  234. package/src/core/operations/create-model.ts +191 -0
  235. package/src/core/operations/file-session.ts +222 -0
  236. package/src/core/operations/provision-operations.ts +297 -0
  237. package/src/core/operations/runtime.ts +258 -0
  238. package/src/core/operations/scaffold-apply.ts +106 -0
  239. package/src/core/operations/scaffold-machine.ts +77 -0
  240. package/src/core/operations/scaffold-plan.ts +215 -0
  241. package/src/core/project/agent-instructions.ts +167 -0
  242. package/src/core/project/agent-templates/AGENTS.md +504 -0
  243. package/src/core/project/agent-templates/CLAUDE.md +31 -0
  244. package/src/core/project/agent-templates/azure-functions.instructions.md +22 -0
  245. package/src/core/project/agent-templates/bff-routes.instructions.md +18 -0
  246. package/src/core/project/agent-templates/copilot-instructions.md +27 -0
  247. package/src/core/project/agent-templates/shared-models.instructions.md +72 -0
  248. package/src/core/project/artifacts.ts +205 -0
  249. package/src/core/project/boundaries.ts +170 -0
  250. package/src/core/project/capabilities.ts +193 -0
  251. package/src/core/project/drift.ts +202 -0
  252. package/src/core/project/infra.ts +196 -0
  253. package/src/core/project/manifest.ts +402 -0
  254. package/src/core/project/state.ts +77 -0
  255. package/src/core/project/validation.ts +295 -0
  256. package/src/core/project/workflows.ts +295 -0
  257. package/src/core/scaffold/auth-generator.ts +1824 -0
  258. package/src/core/scaffold/connector-functions-generator.ts +1133 -0
  259. package/src/core/scaffold/functions-generator.ts +432 -134
  260. package/src/core/scaffold/model-parser.ts +244 -90
  261. package/src/core/scaffold/native-schema-generator.ts +904 -0
  262. package/src/core/scaffold/nextjs-generator.ts +174 -9
  263. package/src/core/scaffold/ui-generator.ts +191 -36
  264. package/src/core/verify/index.ts +468 -0
  265. package/src/machine/contracts.ts +33 -0
  266. package/src/machine/errors.ts +45 -0
  267. package/src/machine/index.ts +690 -0
  268. package/src/mcp/index.ts +423 -0
  269. package/src/types/index.ts +137 -0
  270. package/src/utils/package-manager.ts +41 -19
  271. package/src/utils/python-uv.ts +96 -0
  272. package/src/version.ts +13 -0
@@ -5,25 +5,53 @@
5
5
 
6
6
  import * as fs from "fs";
7
7
  import * as path from "path";
8
- import { spawn } from "child_process";
9
- import { getBackendLanguage, ensureSwallowKitProject } from "../../core/config";
10
- import { ModelInfo, parseModelFile, toKebabCase, toPascalCase } from "../../core/scaffold/model-parser";
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";
11
11
  import {
12
12
  generateCSharpAzureFunctionsCRUD,
13
13
  generateCompactAzureFunctionsCRUD,
14
14
  generatePythonAzureFunctionsCRUD,
15
15
  } from "../../core/scaffold/functions-generator";
16
- import { generateCompactBFFRoutes, generateBFFCallFunction } from "../../core/scaffold/nextjs-generator";
17
- import { generateOpenApiDocument } from "../../core/scaffold/openapi-generator";
16
+ import {
17
+ generateRdbConnectorFunctionTS,
18
+ generateApiConnectorFunctionTS,
19
+ generateRdbConnectorFunctionCSharp,
20
+ generateApiConnectorFunctionCSharp,
21
+ generateRdbConnectorFunctionPython,
22
+ generateApiConnectorFunctionPython,
23
+ } from "../../core/scaffold/connector-functions-generator";
24
+ import { generateCompactBFFRoutes, generateBFFCallFunction, generateConnectorBFFRoutes } from "../../core/scaffold/nextjs-generator";
18
25
  import {
19
26
  generateListPage,
20
27
  generateDetailPage,
21
28
  generateFormComponent,
22
29
  generateNewPage,
23
30
  generateEditPage,
31
+ generateAuthLayout,
32
+ UIAuthOptions,
24
33
  } from "../../core/scaffold/ui-generator";
34
+ import { generateLanguageSchemaArtifacts, validateNativeSchemaToolchain } from "../../core/scaffold/native-schema-generator";
25
35
  import { detectFromProject, getCommands } from "../../utils/package-manager";
26
- import { BackendLanguage } from "../../types";
36
+ import {
37
+ BackendLanguage,
38
+ RdbConnectorConfig,
39
+ ApiConnectorConfig,
40
+ RdbModelConnectorConfig,
41
+ ApiModelConnectorConfig,
42
+ ModelAuthPolicy,
43
+ AuthConfig,
44
+ } from "../../types";
45
+ import { syncProjectManifest } from "../../core/project/manifest";
46
+ import { generateBFFCallFunctionWithAuth, generateBFFCallFunctionWithMultipleAuth, generateBFFCallFunctionWithSwaAuth, generateNamedAuthRouterCSharp, generateNamedAuthRouterPython, generateNamedAuthRouterTS } from "../../core/scaffold/auth-generator";
47
+ import {
48
+ FileOperationSession,
49
+ getActiveFileSession,
50
+ hashFileIfExists,
51
+ } from "../../core/operations/file-session";
52
+ import { recordSessionOperations } from "../../core/project/artifacts";
53
+
54
+ const SCAFFOLD_GENERATOR = "scaffold";
27
55
 
28
56
  interface ScaffoldOptions {
29
57
  model: string; // モデルファイルのパス(例: "lib/models/todo.ts" or "todo")
@@ -32,10 +60,36 @@ interface ScaffoldOptions {
32
60
  apiOnly?: boolean; // true の場合、UI を生成しない(デフォルト: false)
33
61
  }
34
62
 
63
+ function getMachineAwareStdio(): "inherit" | "pipe" {
64
+ return process.env.SWALLOWKIT_MACHINE_OUTPUT === "1" ? "pipe" : "inherit";
65
+ }
66
+
67
+ function runSpawnSyncCommand(command: string, args: string[], cwd: string): void {
68
+ const result = spawnSync(command, args, {
69
+ cwd,
70
+ stdio: getMachineAwareStdio(),
71
+ shell: true,
72
+ });
73
+
74
+ if (typeof result.status === "number" && result.status !== 0) {
75
+ throw new Error(`${command} ${args.join(" ")} exited with code ${result.status}`);
76
+ }
77
+
78
+ if (result.error) {
79
+ throw result.error;
80
+ }
81
+ }
82
+
35
83
  export async function scaffoldCommand(options: ScaffoldOptions) {
36
84
  // SwallowKit プロジェクトディレクトリかどうかを検証
37
85
  ensureSwallowKitProject("scaffold");
38
86
 
87
+ const configValidation = validateConfig(getFullConfig());
88
+ if (!configValidation.valid) throw new Error(`Invalid SwallowKit configuration:\n${configValidation.errors.map(error => `- ${error}`).join("\n")}`);
89
+
90
+ // Plan(dry-run) や Apply からセッションが供給される。単体実行時は commit セッションを生成。
91
+ const session = getActiveFileSession() ?? new FileOperationSession("commit");
92
+
39
93
  console.log("🏗️ SwallowKit Scaffold: Generating CRUD operations...\n");
40
94
 
41
95
  try {
@@ -51,6 +105,13 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
51
105
  const backendLanguage = getBackendLanguage();
52
106
  console.log(`🧠 Backend language: ${backendLanguage}`);
53
107
 
108
+ // コネクタモデルかどうかを判定
109
+ const isConnectorModel = !!modelInfo.connectorConfig;
110
+ if (isConnectorModel) {
111
+ console.log(`🔌 Connector model detected: ${modelInfo.connectorConfig!.connector}`);
112
+ console.log(` Operations: ${modelInfo.connectorConfig!.operations.join(", ")}`);
113
+ }
114
+
54
115
  // ネストスキーマ参照があれば表示
55
116
  if (modelInfo.nestedSchemaRefs.length > 0) {
56
117
  console.log(`🔗 Nested schema references detected:`);
@@ -74,28 +135,74 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
74
135
  const relatedModels = backendLanguage === "typescript"
75
136
  ? [modelInfo]
76
137
  : await collectModelGraph(modelPath);
138
+ for (const relatedModel of relatedModels) {
139
+ session.registerInput(relatedModel.filePath);
140
+ }
141
+
142
+ // 4b. 書き込み前に外部 codegen toolchain を検証し、失敗時はプロジェクト無変更で終了する
143
+ if (session.mode === "commit" && backendLanguage !== "typescript" && !isConnectorModel) {
144
+ await validateNativeSchemaToolchain(
145
+ relatedModels,
146
+ modelInfo,
147
+ backendLanguage,
148
+ path.join(process.cwd(), functionsDir)
149
+ );
150
+ }
151
+
152
+ if (options.apiOnly) {
153
+ console.log("ℹ️ --api-only skips UI generation only; Functions, BFF routes, OpenAPI, and native schema assets are still updated.");
154
+ }
77
155
 
78
156
  // 5. Generate BFF callFunction helper
79
- await generateCallFunctionHelper();
157
+ await generateCallFunctionHelper(session);
80
158
 
81
159
  // 6. Generate Azure Functions code
82
- await generateFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage);
83
-
84
- if (backendLanguage !== "typescript") {
85
- await generateLanguageSchemaArtifacts(relatedModels, modelInfo, functionsDir, backendLanguage);
160
+ if (isConnectorModel) {
161
+ await generateConnectorFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage, session);
162
+ // 6b. Install connector driver dependencies
163
+ await installConnectorDriverDependencies(modelInfo, functionsDir, backendLanguage, session);
164
+ } else {
165
+ await generateFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage, session);
166
+
167
+ if (backendLanguage !== "typescript") {
168
+ await generateLanguageSchemaArtifacts(relatedModels, modelInfo, functionsDir, backendLanguage, session);
169
+ }
86
170
  }
87
171
 
88
172
  // 7. Generate Next.js BFF API Routes
89
173
  const apiDir = options.apiDir || "app/api";
90
- await generateBFFRoutes(modelInfo, apiDir, sharedPackageName);
174
+ if (isConnectorModel) {
175
+ await generateConnectorBFFRoutesFiles(modelInfo, apiDir, sharedPackageName, session);
176
+ } else {
177
+ await generateBFFRoutes(modelInfo, apiDir, sharedPackageName, session);
178
+ }
91
179
 
92
- // 8. Generate Cosmos DB container Bicep file
93
- await generateCosmosContainer(modelInfo);
180
+ // 8. Generate Cosmos DB container Bicep file (skip for connector models)
181
+ if (!isConnectorModel) {
182
+ await generateCosmosContainer(modelInfo, session);
183
+ }
94
184
 
95
185
  // 9. Generate UI components (unless --api-only)
96
186
  if (!options.apiOnly) {
97
- await generateUIComponents(modelInfo, sharedPackageName);
98
- await updateNavigationMenu(modelInfo);
187
+ const uiAuthConfig = getAuthConfig();
188
+ const uiAuthPolicy = resolveAuthPolicy(modelInfo, uiAuthConfig);
189
+ const uiAuthOptions: UIAuthOptions | undefined =
190
+ uiAuthPolicy && uiAuthConfig && uiAuthConfig.provider !== 'none'
191
+ ? createUIAuthOptions(uiAuthPolicy, uiAuthConfig)
192
+ : undefined;
193
+ await generateUIComponents(modelInfo, sharedPackageName, uiAuthOptions, session);
194
+ if (uiAuthOptions?.authenticatedFetchImport) {
195
+ applyExternalTokenFetchToGeneratedUi(modelInfo, session, uiAuthOptions.authenticatedFetchImport);
196
+ }
197
+ await updateNavigationMenu(modelInfo, session);
198
+ }
199
+
200
+ if (session.mode === "commit") {
201
+ recordSessionOperations(session.operations, {
202
+ schemaHash: hashFileIfExists(modelPath) ?? undefined,
203
+ sourceModel: modelInfo.name,
204
+ });
205
+ await syncProjectManifest();
99
206
  }
100
207
 
101
208
  console.log("\n✅ Scaffold completed successfully!");
@@ -106,7 +213,7 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
106
213
  console.log(" 3. Navigate to the model from the homepage menu");
107
214
  }
108
215
  if (backendLanguage !== "typescript") {
109
- console.log(` ${options.apiOnly ? "2" : "4"}. Review generated OpenAPI assets in ${functionsDir}/openapi/ and ${functionsDir}/generated/`);
216
+ console.log(` ${options.apiOnly ? "2" : "4"}. Review generated OpenAPI export and native schema assets in ${functionsDir}/openapi/ and ${functionsDir}/generated/`);
110
217
  }
111
218
  console.log(
112
219
  ` ${options.apiOnly ? (backendLanguage === "typescript" ? "2" : "3") : (backendLanguage === "typescript" ? "4" : "5")}. Ensure BACKEND_FUNCTIONS_BASE_URL is set in your .env.local file`
@@ -114,17 +221,73 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
114
221
  console.log(
115
222
  ` ${options.apiOnly ? (backendLanguage === "typescript" ? "3" : "4") : (backendLanguage === "typescript" ? "5" : "6")}. Configure CosmosDBConnection in functions/local.settings.json`
116
223
  );
117
- console.log(` ${options.apiOnly ? (backendLanguage === "typescript" ? "4" : "5") : (backendLanguage === "typescript" ? "6" : "7")}. Run '${getCommands(detectFromProject()).dlx} swallowkit dev' to test the generated code`);
224
+ console.log(` ${options.apiOnly ? (backendLanguage === "typescript" ? "4" : "5") : (backendLanguage === "typescript" ? "6" : "7")}. Run '${getCommands(detectFromProject()).exec} swallowkit dev' to test the generated code`);
118
225
  } catch (error: any) {
119
226
  console.error("\n❌ Scaffold failed:", error.message);
120
227
  process.exit(1);
121
228
  }
122
229
  }
123
230
 
231
+ function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo, session: FileOperationSession, importPath = '@/lib/auth/authenticated-fetch'): void {
232
+ const uiPrefix = `app/${toKebabCase(modelInfo.name)}/`;
233
+ for (const operation of session.operations) {
234
+ if (!operation.path.startsWith(uiPrefix) || !operation.path.endsWith('.tsx') || operation.action === 'delete') continue;
235
+ let content = session.readFile(operation.absolutePath);
236
+ if (!content.includes('fetch(')) continue;
237
+ content = content.replace(/\bfetch\(/g, 'authenticatedFetch(');
238
+ content = content.replace(
239
+ /(if \(!res\.ok\) throw new Error\([^\n]+\);)/g,
240
+ `if (res.status === 401) throw new Error('Authentication required');\n $1`
241
+ );
242
+ if (!content.includes(importPath)) {
243
+ const directive = content.match(/^'use client';\s*\r?\n/)?.[0] ?? '';
244
+ content = `${directive}import { authenticatedFetch } from '${importPath}';\n${content.slice(directive.length)}`;
245
+ }
246
+ session.writeFile(operation.absolutePath, content, {
247
+ ownership: 'managed',
248
+ generator: SCAFFOLD_GENERATOR,
249
+ sourceModel: modelInfo.name,
250
+ });
251
+ }
252
+ }
253
+
254
+ /**
255
+ * モデルの authPolicy と auth config から実効的な認可ポリシーを解決
256
+ * - モデルに authPolicy があればそれを使用
257
+ * - なければ auth.authorization.defaultPolicy に従う
258
+ * - auth 設定がなければ undefined(ガードなし)
259
+ */
260
+ export function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): ModelAuthPolicy | undefined {
261
+ // モデルに明示的な authPolicy がある場合はそれを使用
262
+ if (modelInfo.authPolicy) {
263
+ const normalized = normalizeAuthConfig(authConfig);
264
+ 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))) {
265
+ if (!normalized?.authorization.policies[policyName]) throw new Error(`Model '${modelInfo.name}' references undefined auth policy '${policyName}'. Define auth.authorization.policies.${policyName}.`);
266
+ }
267
+ return modelInfo.authPolicy;
268
+ }
269
+
270
+ // auth 設定がない場合はガードなし
271
+ if (!authConfig || authConfig.provider === 'none') {
272
+ return undefined;
273
+ }
274
+
275
+ // defaultPolicy が 'authenticated' なら認証のみ(ロール指定なし)のポリシーを返す
276
+ const defaultPolicy = authConfig.authorization?.defaultPolicy === 'public' ? 'anonymous' : authConfig.authorization?.defaultPolicy ?? 'authenticated';
277
+ if (defaultPolicy === 'authenticated') {
278
+ return {}; // 空のポリシー = 認証のみ、ロール制限なし
279
+ }
280
+
281
+ if (defaultPolicy !== 'anonymous') return { policy: defaultPolicy };
282
+
283
+ // defaultPolicy が 'anonymous' なら認証不要
284
+ return undefined;
285
+ }
286
+
124
287
  /**
125
288
  * モデルファイルのパスを解決
126
289
  */
127
- function resolveModelPath(modelInput: string): string {
290
+ export function resolveModelPath(modelInput: string): string {
128
291
  const cwd = process.cwd();
129
292
 
130
293
  // 絶対パスの場合
@@ -192,7 +355,7 @@ function readSharedPackageName(): string {
192
355
  /**
193
356
  * Generate BFF callFunction helper (lib/api/call-function.ts)
194
357
  */
195
- async function generateCallFunctionHelper(): Promise<void> {
358
+ async function generateCallFunctionHelper(session: FileOperationSession): Promise<void> {
196
359
  console.log("\n📦 Generating BFF callFunction helper...");
197
360
 
198
361
  const cwd = process.cwd();
@@ -200,12 +363,21 @@ async function generateCallFunctionHelper(): Promise<void> {
200
363
  const helperDir = path.join(cwd, "lib", "api");
201
364
  const helperPath = path.join(helperDir, "call-function.ts");
202
365
 
203
- if (!fs.existsSync(helperDir)) {
204
- fs.mkdirSync(helperDir, { recursive: true });
366
+ // auth 設定がある場合は Authorization ヘッダー転送版を生成
367
+ const authConfig = getAuthConfig();
368
+ const normalizedAuth = normalizeAuthConfig(authConfig);
369
+ const hasAuth = Boolean(normalizedAuth && Object.keys(normalizedAuth.schemes).length > 0);
370
+ let helperCode: string;
371
+ if (hasAuth) {
372
+ const providers = new Set(Object.values(normalizedAuth!.schemes).map(scheme => scheme.provider));
373
+ helperCode = normalizedAuth!.schemes.default && Object.keys(normalizedAuth!.schemes).length === 1
374
+ ? authConfig?.provider === "swa" ? generateBFFCallFunctionWithSwaAuth() : generateBFFCallFunctionWithAuth()
375
+ : providers.has("swa") ? generateBFFCallFunctionWithMultipleAuth() : generateBFFCallFunctionWithAuth();
376
+ } else {
377
+ helperCode = generateBFFCallFunction();
205
378
  }
206
379
 
207
- const helperCode = generateBFFCallFunction();
208
- fs.writeFileSync(helperPath, helperCode, "utf-8");
380
+ session.writeFile(helperPath, helperCode, { ownership: "managed", generator: SCAFFOLD_GENERATOR });
209
381
  console.log(`✅ Created: ${helperPath}`);
210
382
  }
211
383
 
@@ -216,11 +388,121 @@ async function generateFunctionsCode(
216
388
  modelInfo: ModelInfo,
217
389
  functionsDir: string,
218
390
  sharedPackageName: string,
219
- backendLanguage: BackendLanguage
391
+ backendLanguage: BackendLanguage,
392
+ session: FileOperationSession
220
393
  ): Promise<void> {
221
394
  console.log("\n🔨 Generating Azure Functions CRUD code...");
222
395
 
396
+ // Resolve auth policy: model-level authPolicy or global defaultPolicy
397
+ const authConfig = getAuthConfig();
398
+ const authPolicy = resolveAuthPolicy(modelInfo, authConfig);
399
+ if (authPolicy) {
400
+ console.log(`🔐 Auth policy detected: ${JSON.stringify(authPolicy)}`);
401
+ }
402
+
403
+ const modelKebab = toKebabCase(modelInfo.name);
404
+ const managedMeta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
405
+ if (backendLanguage === "typescript") {
406
+ const normalizedAuth = getNormalizedAuthConfig();
407
+ if (normalizedAuth && Object.keys(normalizedAuth.schemes).some(name => name !== "default")) {
408
+ const routerPath = path.join(process.cwd(), functionsDir, "src", "auth", "auth-router.ts");
409
+ session.writeFile(routerPath, generateNamedAuthRouterTS(normalizedAuth), { ownership: "managed", generator: SCAFFOLD_GENERATOR });
410
+ }
411
+ const functionFilePath = path.join(
412
+ process.cwd(),
413
+ functionsDir,
414
+ "src",
415
+ `${modelKebab}.ts`
416
+ );
417
+
418
+ const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName, authPolicy, authConfig?.provider);
419
+ session.writeFile(functionFilePath, code, managedMeta);
420
+ console.log(`✅ Created: ${functionFilePath}`);
421
+ return;
422
+ }
423
+
424
+ if (backendLanguage === "csharp") {
425
+ const normalizedAuth = getNormalizedAuthConfig();
426
+ if (normalizedAuth && Object.keys(normalizedAuth.schemes).some(name => name !== "default")) {
427
+ const routerPath = path.join(process.cwd(), functionsDir, "Auth", "AuthRouter.cs");
428
+ session.writeFile(routerPath, generateNamedAuthRouterCSharp(normalizedAuth), { ownership: "managed", generator: SCAFFOLD_GENERATOR });
429
+ }
430
+ const crudDir = path.join(process.cwd(), functionsDir, "Crud");
431
+ const functionFilePath = path.join(crudDir, `${modelInfo.name}CrudFunctions.cs`);
432
+
433
+ // Remove init-generated template (singular) to avoid route conflicts
434
+ const templatePath = path.join(crudDir, `${modelInfo.name}Function.cs`);
435
+ if (session.fileExists(templatePath)) {
436
+ session.deleteFile(templatePath, managedMeta);
437
+ console.log(`🗑️ Removed template: ${templatePath}`);
438
+ }
439
+
440
+ session.writeFile(functionFilePath, generateCSharpAzureFunctionsCRUD(modelInfo, authPolicy), managedMeta);
441
+ console.log(`✅ Created: ${functionFilePath}`);
442
+ return;
443
+ }
444
+
445
+ const normalizedAuth = getNormalizedAuthConfig();
446
+ if (normalizedAuth && Object.keys(normalizedAuth.schemes).some(name => name !== "default")) {
447
+ const routerPath = path.join(process.cwd(), functionsDir, "auth", "auth_router.py");
448
+ session.writeFile(routerPath, generateNamedAuthRouterPython(normalizedAuth), { ownership: "managed", generator: SCAFFOLD_GENERATOR });
449
+ }
450
+
451
+ const blueprintsDir = path.join(process.cwd(), functionsDir, "blueprints");
452
+ const blueprintPath = path.join(blueprintsDir, `${modelKebab.replace(/-/g, "_")}.py`);
453
+
454
+ const { blueprint, registration } = generatePythonAzureFunctionsCRUD(modelInfo, authPolicy);
455
+ session.writeFile(blueprintPath, blueprint, managedMeta);
456
+ updatePythonFunctionRegistrations(path.join(process.cwd(), functionsDir, "function_app.py"), registration, session);
457
+ console.log(`✅ Created: ${blueprintPath}`);
458
+ }
459
+
460
+ export function createUIAuthOptions(policy: ModelAuthPolicy, authConfig: AuthConfig): UIAuthOptions {
461
+ const normalized = normalizeAuthConfig(authConfig);
462
+ const readPolicyName = typeof policy.read === 'string' ? policy.read : policy.policy;
463
+ const writePolicyName = typeof policy.write === 'string' ? policy.write : policy.policy;
464
+ const readNamed = readPolicyName ? normalized?.authorization.policies[readPolicyName] : undefined;
465
+ const named = writePolicyName ? normalized?.authorization.policies[writePolicyName] : undefined;
466
+ const writeRoles = named?.roles ?? (Array.isArray(policy.write) ? policy.write : policy.roles) ?? [];
467
+ const externalScheme = [...(readNamed?.schemes ?? []), ...(named?.schemes ?? [])].find(name => normalized?.schemes[name]?.provider === 'external-token');
468
+ const authScheme = readNamed?.schemes[0] ?? named?.schemes[0];
469
+ const authenticatedFetchImport = externalScheme ? `@/lib/auth/schemes/${toKebabCase(externalScheme)}/authenticated-fetch` : authConfig.provider === 'external-token' ? '@/lib/auth/authenticated-fetch' : undefined;
470
+ const authContextImport = authScheme ? `@/lib/auth/schemes/${toKebabCase(authScheme)}/auth-context` : '@/lib/auth/auth-context';
471
+ return { authPolicy: policy, writeRoles, authenticatedFetchImport, authContextImport };
472
+ }
473
+
474
+ /**
475
+ * コネクタモデル用 Azure Functions コード生成
476
+ */
477
+ async function generateConnectorFunctionsCode(
478
+ modelInfo: ModelInfo,
479
+ functionsDir: string,
480
+ sharedPackageName: string,
481
+ backendLanguage: BackendLanguage,
482
+ session: FileOperationSession
483
+ ): Promise<void> {
484
+ console.log("\n🔌 Generating Connector Azure Functions code...");
485
+
486
+ const connectorConfig = modelInfo.connectorConfig!;
487
+ const connectorDef = getConnectorDefinition(connectorConfig.connector);
488
+
489
+ if (!connectorDef) {
490
+ throw new Error(
491
+ `Connector '${connectorConfig.connector}' not found in swallowkit.config.js.\n` +
492
+ ` Please add it to the 'connectors' section of your configuration.`
493
+ );
494
+ }
495
+
496
+ // Resolve auth policy (same logic as Cosmos model scaffolding)
497
+ const authConfig = getAuthConfig();
498
+ const authPolicy = resolveAuthPolicy(modelInfo, authConfig);
499
+ if (authPolicy) {
500
+ console.log(`🔐 Auth policy detected: ${JSON.stringify(authPolicy)}`);
501
+ }
502
+
223
503
  const modelKebab = toKebabCase(modelInfo.name);
504
+ const connectorManagedMeta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
505
+
224
506
  if (backendLanguage === "typescript") {
225
507
  const functionFilePath = path.join(
226
508
  process.cwd(),
@@ -229,13 +511,26 @@ async function generateFunctionsCode(
229
511
  `${modelKebab}.ts`
230
512
  );
231
513
 
232
- const functionDir = path.dirname(functionFilePath);
233
- if (!fs.existsSync(functionDir)) {
234
- fs.mkdirSync(functionDir, { recursive: true });
514
+ let code: string;
515
+ if (connectorDef.type === "rdb") {
516
+ code = generateRdbConnectorFunctionTS(
517
+ modelInfo, sharedPackageName,
518
+ connectorDef as RdbConnectorConfig,
519
+ connectorConfig as RdbModelConnectorConfig,
520
+ authPolicy,
521
+ authConfig?.provider
522
+ );
523
+ } else {
524
+ code = generateApiConnectorFunctionTS(
525
+ modelInfo, sharedPackageName,
526
+ connectorDef as ApiConnectorConfig,
527
+ connectorConfig as ApiModelConnectorConfig,
528
+ authPolicy,
529
+ authConfig?.provider
530
+ );
235
531
  }
236
532
 
237
- const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName);
238
- fs.writeFileSync(functionFilePath, code, "utf-8");
533
+ session.writeFile(functionFilePath, code, connectorManagedMeta);
239
534
  console.log(`✅ Created: ${functionFilePath}`);
240
535
  return;
241
536
  }
@@ -244,25 +539,175 @@ async function generateFunctionsCode(
244
539
  const functionFilePath = path.join(
245
540
  process.cwd(),
246
541
  functionsDir,
247
- "Crud",
248
- `${modelInfo.name}Functions.cs`
542
+ "Connectors",
543
+ `${modelInfo.name}ConnectorFunctions.cs`
249
544
  );
250
- fs.mkdirSync(path.dirname(functionFilePath), { recursive: true });
251
- fs.writeFileSync(functionFilePath, generateCSharpAzureFunctionsCRUD(modelInfo), "utf-8");
545
+
546
+ let code: string;
547
+ if (connectorDef.type === "rdb") {
548
+ code = generateRdbConnectorFunctionCSharp(
549
+ modelInfo,
550
+ connectorDef as RdbConnectorConfig,
551
+ connectorConfig as RdbModelConnectorConfig
552
+ );
553
+ } else {
554
+ code = generateApiConnectorFunctionCSharp(
555
+ modelInfo,
556
+ connectorDef as ApiConnectorConfig,
557
+ connectorConfig as ApiModelConnectorConfig
558
+ );
559
+ }
560
+
561
+ session.writeFile(functionFilePath, code, connectorManagedMeta);
252
562
  console.log(`✅ Created: ${functionFilePath}`);
253
563
  return;
254
564
  }
255
565
 
566
+ // Python
256
567
  const blueprintsDir = path.join(process.cwd(), functionsDir, "blueprints");
257
568
  const blueprintPath = path.join(blueprintsDir, `${modelKebab.replace(/-/g, "_")}.py`);
258
- fs.mkdirSync(blueprintsDir, { recursive: true });
259
569
 
260
- const { blueprint, registration } = generatePythonAzureFunctionsCRUD(modelInfo);
261
- fs.writeFileSync(blueprintPath, blueprint, "utf-8");
262
- updatePythonFunctionRegistrations(path.join(process.cwd(), functionsDir, "function_app.py"), registration);
570
+ let result: { blueprint: string; registration: string };
571
+ if (connectorDef.type === "rdb") {
572
+ result = generateRdbConnectorFunctionPython(
573
+ modelInfo,
574
+ connectorDef as RdbConnectorConfig,
575
+ connectorConfig as RdbModelConnectorConfig
576
+ );
577
+ } else {
578
+ result = generateApiConnectorFunctionPython(
579
+ modelInfo,
580
+ connectorDef as ApiConnectorConfig,
581
+ connectorConfig as ApiModelConnectorConfig
582
+ );
583
+ }
584
+
585
+ session.writeFile(blueprintPath, result.blueprint, connectorManagedMeta);
586
+ updatePythonFunctionRegistrations(path.join(process.cwd(), functionsDir, "function_app.py"), result.registration, session);
263
587
  console.log(`✅ Created: ${blueprintPath}`);
264
588
  }
265
589
 
590
+ /**
591
+ * コネクタモデルの scaffold 後に、生成コードが必要とする RDB/API ドライバを
592
+ * functions ディレクトリにインストールする。
593
+ */
594
+ async function installConnectorDriverDependencies(
595
+ modelInfo: ModelInfo,
596
+ functionsDir: string,
597
+ backendLanguage: BackendLanguage,
598
+ session: FileOperationSession
599
+ ): Promise<void> {
600
+ const connectorConfig = modelInfo.connectorConfig!;
601
+ const connectorDef = getConnectorDefinition(connectorConfig.connector);
602
+ if (!connectorDef || connectorDef.type !== "rdb") return;
603
+
604
+ const rdbDef = connectorDef as RdbConnectorConfig;
605
+ const functionsPath = path.join(process.cwd(), functionsDir);
606
+
607
+ if (backendLanguage === "typescript") {
608
+ const driverMap: Record<string, { deps: string[]; devDeps: string[] }> = {
609
+ mysql: { deps: ["mysql2"], devDeps: [] },
610
+ postgres: { deps: ["pg"], devDeps: ["@types/pg"] },
611
+ sqlserver: { deps: ["mssql"], devDeps: [] },
612
+ };
613
+ const entry = driverMap[rdbDef.provider];
614
+ if (!entry) return;
615
+
616
+ // package.json を読んで、既にインストール済みならスキップ
617
+ const pkgJsonPath = path.join(functionsPath, "package.json");
618
+ if (fs.existsSync(pkgJsonPath)) {
619
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
620
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
621
+ const missingDeps = entry.deps.filter(d => !allDeps[d]);
622
+ const missingDevDeps = entry.devDeps.filter(d => !allDeps[d]);
623
+ if (missingDeps.length === 0 && missingDevDeps.length === 0) return;
624
+ }
625
+
626
+ if (session.mode === "collect") {
627
+ session.addWarning(
628
+ `external-install: ${rdbDef.provider} driver dependencies (${[...entry.deps, ...entry.devDeps].join(", ")}) will be installed into ${functionsDir}/ on apply.`
629
+ );
630
+ return;
631
+ }
632
+
633
+ console.log(`\n📦 Installing ${rdbDef.provider} driver dependencies...`);
634
+ const pm = detectFromProject(functionsPath);
635
+ const cmds = getCommands(pm);
636
+
637
+ if (entry.deps.length > 0) {
638
+ runSpawnSyncCommand(cmds.name, [pm === "pnpm" ? "add" : "install", ...entry.deps], functionsPath);
639
+ }
640
+ if (entry.devDeps.length > 0) {
641
+ runSpawnSyncCommand(cmds.name, [pm === "pnpm" ? "add" : "install", "-D", ...entry.devDeps], functionsPath);
642
+ }
643
+ console.log(`✅ ${rdbDef.provider} driver installed`);
644
+ return;
645
+ }
646
+
647
+ if (backendLanguage === "csharp") {
648
+ const nugetMap: Record<string, string> = {
649
+ mysql: "MySqlConnector",
650
+ postgres: "Npgsql",
651
+ sqlserver: "Microsoft.Data.SqlClient",
652
+ };
653
+ const pkg = nugetMap[rdbDef.provider];
654
+ if (!pkg) return;
655
+ if (session.mode === "collect") {
656
+ session.addWarning(`external-install: NuGet package ${pkg} will be added to ${functionsDir}/functions.csproj on apply.`);
657
+ return;
658
+ }
659
+ console.log(`\n📦 Installing ${rdbDef.provider} NuGet package...`);
660
+ runSpawnSyncCommand("dotnet", ["add", path.join(functionsPath, "functions.csproj"), "package", pkg], functionsPath);
661
+ console.log(`✅ ${pkg} installed`);
662
+ return;
663
+ }
664
+
665
+ // Python — requirements.txt に追記
666
+ const pipMap: Record<string, string> = {
667
+ mysql: "mysql-connector-python",
668
+ postgres: "psycopg2-binary",
669
+ sqlserver: "pymssql",
670
+ };
671
+ const pipPkg = pipMap[rdbDef.provider];
672
+ if (!pipPkg) return;
673
+ const reqPath = path.join(functionsPath, "requirements.txt");
674
+ if (session.fileExists(reqPath)) {
675
+ const existing = session.readFile(reqPath);
676
+ if (!existing.includes(pipPkg)) {
677
+ console.log(`\n📦 Adding ${pipPkg} to requirements.txt...`);
678
+ session.writeFile(reqPath, `${existing}\n${pipPkg}\n`, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
679
+ console.log(`✅ ${pipPkg} added`);
680
+ }
681
+ }
682
+ }
683
+
684
+ /**
685
+ * コネクタモデル用 BFF ルート生成(操作制限に対応)
686
+ */
687
+ async function generateConnectorBFFRoutesFiles(
688
+ modelInfo: ModelInfo,
689
+ apiDir: string,
690
+ sharedPackageName: string,
691
+ session: FileOperationSession
692
+ ): Promise<void> {
693
+ console.log("\n🔌 Generating Connector BFF API routes...");
694
+
695
+ const modelCamel = toCamelCase(modelInfo.name);
696
+ const operations = modelInfo.connectorConfig!.operations;
697
+
698
+ const listRoutePath = path.join(process.cwd(), apiDir, modelCamel, "route.ts");
699
+ const detailRoutePath = path.join(process.cwd(), apiDir, modelCamel, "[id]", "route.ts");
700
+
701
+ const routes = generateConnectorBFFRoutes(modelInfo, sharedPackageName, operations);
702
+
703
+ const meta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
704
+ session.writeFile(listRoutePath, routes.listRoute, meta);
705
+ session.writeFile(detailRoutePath, routes.detailRoute, meta);
706
+
707
+ console.log(`✅ Created: ${listRoutePath}`);
708
+ console.log(`✅ Created: ${detailRoutePath}`);
709
+ }
710
+
266
711
  async function collectModelGraph(modelPath: string, seen = new Map<string, ModelInfo>()): Promise<ModelInfo[]> {
267
712
  const resolvedPath = path.resolve(modelPath);
268
713
  if (seen.has(resolvedPath)) {
@@ -298,140 +743,18 @@ function describeFunctionsOutputPath(functionsDir: string, backendLanguage: Back
298
743
  return `${functionsDir}/blueprints/`;
299
744
  }
300
745
 
301
- async function generateLanguageSchemaArtifacts(
302
- models: ModelInfo[],
303
- rootModel: ModelInfo,
304
- functionsDir: string,
305
- backendLanguage: Exclude<BackendLanguage, "typescript">
306
- ): Promise<void> {
307
- console.log("\n🧬 Generating OpenAPI-backed schema artifacts...");
308
-
309
- const openApiDir = path.join(process.cwd(), functionsDir, "openapi");
310
- fs.mkdirSync(openApiDir, { recursive: true });
311
-
312
- const specPath = path.join(openApiDir, `${toKebabCase(rootModel.name)}.openapi.json`);
313
- fs.writeFileSync(specPath, generateOpenApiDocument(models, rootModel), "utf-8");
314
- console.log(`✅ Created: ${specPath}`);
315
-
316
- const outputDir = path.join(
317
- process.cwd(),
318
- functionsDir,
319
- "generated",
320
- backendLanguage === "csharp" ? "csharp-models" : "python-models"
321
- );
322
-
323
- fs.mkdirSync(outputDir, { recursive: true });
324
- await runOpenApiGenerator(specPath, outputDir, backendLanguage);
325
- if (backendLanguage === "csharp") {
326
- pruneGeneratedCSharpArtifacts(outputDir);
746
+ function updatePythonFunctionRegistrations(functionAppPath: string, registration: string, session: FileOperationSession): void {
747
+ if (!session.fileExists(functionAppPath)) {
748
+ throw new Error(`Python Functions entrypoint not found: ${functionAppPath}`);
327
749
  }
328
- console.log(`✅ Generated ${backendLanguage} schema assets: ${outputDir}`);
329
- }
330
-
331
- async function runOpenApiGenerator(
332
- specPath: string,
333
- outputDir: string,
334
- backendLanguage: Exclude<BackendLanguage, "typescript">
335
- ): Promise<void> {
336
- const pm = detectFromProject();
337
- const command = pm === "pnpm" ? "pnpm" : "npx";
338
- const args = pm === "pnpm"
339
- ? ["exec", "openapi-generator-cli", ...getOpenApiGeneratorArgs(specPath, outputDir, backendLanguage)]
340
- : ["openapi-generator-cli", ...getOpenApiGeneratorArgs(specPath, outputDir, backendLanguage)];
341
-
342
- await new Promise<void>((resolve, reject) => {
343
- const child = spawn(command, args, {
344
- cwd: process.cwd(),
345
- shell: true,
346
- stdio: "inherit",
347
- });
348
-
349
- child.on("close", (code) => {
350
- if (code === 0) {
351
- resolve();
352
- return;
353
- }
354
750
 
355
- reject(new Error(`OpenAPI generator exited with code ${code}`));
356
- });
357
-
358
- child.on("error", reject);
359
- });
360
- }
751
+ const content = session.readFile(functionAppPath);
361
752
 
362
- export function getOpenApiGeneratorArgs(
363
- specPath: string,
364
- outputDir: string,
365
- backendLanguage: Exclude<BackendLanguage, "typescript">
366
- ): string[] {
367
- const globalProperty = backendLanguage === "csharp"
368
- ? "models,supportingFiles,apis=false,modelDocs=false,modelTests=false"
369
- : "models,apis=false,supportingFiles=false,modelDocs=false,modelTests=false";
370
-
371
- const baseArgs = [
372
- "generate",
373
- "-i",
374
- specPath,
375
- "-o",
376
- outputDir,
377
- "--global-property",
378
- globalProperty,
379
- ];
380
-
381
- if (backendLanguage === "csharp") {
382
- return [
383
- ...baseArgs,
384
- "-g",
385
- "csharp",
386
- "--additional-properties",
387
- "packageName=SwallowKitBackendModels,targetFramework=net8.0,nullableReferenceTypes=true",
388
- ];
389
- }
390
-
391
- return [
392
- ...baseArgs,
393
- "-g",
394
- "python",
395
- "--additional-properties",
396
- "packageName=backend_models,projectName=backend-models",
397
- ];
398
- }
399
-
400
- export function getCSharpSchemaArtifactPruneTargets(outputDir: string): string[] {
401
- return [
402
- path.join(outputDir, "src", "SwallowKitBackendModels.Test"),
403
- path.join(outputDir, "src", "SwallowKitBackendModels", "Api"),
404
- path.join(outputDir, "src", "SwallowKitBackendModels", "Extensions"),
405
- ];
406
- }
407
-
408
- function pruneGeneratedCSharpArtifacts(outputDir: string): void {
409
- for (const target of getCSharpSchemaArtifactPruneTargets(outputDir)) {
410
- if (fs.existsSync(target)) {
411
- fs.rmSync(target, { recursive: true, force: true });
412
- }
413
- }
414
-
415
- const clientDir = path.join(outputDir, "src", "SwallowKitBackendModels", "Client");
416
- if (!fs.existsSync(clientDir)) {
753
+ // Check if import line already exists (handles init-generated layout)
754
+ const importLine = registration.split("\n").find((l) => l.startsWith("from ") || l.startsWith("import "));
755
+ if (importLine && content.includes(importLine)) {
417
756
  return;
418
757
  }
419
-
420
- for (const entry of fs.readdirSync(clientDir, { withFileTypes: true })) {
421
- if (!entry.isFile() || entry.name === "Option.cs") {
422
- continue;
423
- }
424
-
425
- fs.rmSync(path.join(clientDir, entry.name), { force: true });
426
- }
427
- }
428
-
429
- function updatePythonFunctionRegistrations(functionAppPath: string, registration: string): void {
430
- if (!fs.existsSync(functionAppPath)) {
431
- throw new Error(`Python Functions entrypoint not found: ${functionAppPath}`);
432
- }
433
-
434
- const content = fs.readFileSync(functionAppPath, "utf-8");
435
758
  if (content.includes(registration)) {
436
759
  return;
437
760
  }
@@ -442,7 +765,7 @@ function updatePythonFunctionRegistrations(functionAppPath: string, registration
442
765
  }
443
766
 
444
767
  const updated = content.replace(marker, `${registration}\n${marker}`);
445
- fs.writeFileSync(functionAppPath, updated, "utf-8");
768
+ session.writeFile(functionAppPath, updated, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
446
769
  }
447
770
 
448
771
  /**
@@ -451,11 +774,11 @@ function updatePythonFunctionRegistrations(functionAppPath: string, registration
451
774
  async function generateBFFRoutes(
452
775
  modelInfo: any,
453
776
  apiDir: string,
454
- sharedPackageName: string
777
+ sharedPackageName: string,
778
+ session: FileOperationSession
455
779
  ): Promise<void> {
456
780
  console.log("\n🔨 Generating Next.js BFF API routes...");
457
781
 
458
- const modelKebab = toKebabCase(modelInfo.name);
459
782
  const modelCamel = modelInfo.name.charAt(0).toLowerCase() + modelInfo.name.slice(1);
460
783
 
461
784
  // List route: app/api/[model]/route.ts
@@ -475,24 +798,13 @@ async function generateBFFRoutes(
475
798
  "route.ts"
476
799
  );
477
800
 
478
- // ディレクトリを作成
479
- const listRouteDir = path.dirname(listRoutePath);
480
- const detailRouteDir = path.dirname(detailRoutePath);
481
-
482
- if (!fs.existsSync(listRouteDir)) {
483
- fs.mkdirSync(listRouteDir, { recursive: true });
484
- }
485
-
486
- if (!fs.existsSync(detailRouteDir)) {
487
- fs.mkdirSync(detailRouteDir, { recursive: true });
488
- }
489
-
490
801
  // コードを生成
491
802
  const routes = generateCompactBFFRoutes(modelInfo, sharedPackageName);
492
803
 
493
804
  // ファイルに書き込み
494
- fs.writeFileSync(listRoutePath, routes.listRoute, "utf-8");
495
- fs.writeFileSync(detailRoutePath, routes.detailRoute, "utf-8");
805
+ const meta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
806
+ session.writeFile(listRoutePath, routes.listRoute, meta);
807
+ session.writeFile(detailRoutePath, routes.detailRoute, meta);
496
808
 
497
809
  console.log(`✅ Created: ${listRoutePath}`);
498
810
  console.log(`✅ Created: ${detailRoutePath}`);
@@ -501,7 +813,7 @@ async function generateBFFRoutes(
501
813
  /**
502
814
  * Generate Cosmos DB container Bicep file
503
815
  */
504
- async function generateCosmosContainer(modelInfo: any): Promise<void> {
816
+ export async function generateCosmosContainer(modelInfo: any, session: FileOperationSession = new FileOperationSession("commit")): Promise<void> {
505
817
  console.log("\n🗄️ Generating Cosmos DB container Bicep file...");
506
818
 
507
819
  const modelKebab = toKebabCase(modelInfo.name);
@@ -515,13 +827,8 @@ async function generateCosmosContainer(modelInfo: any): Promise<void> {
515
827
  return;
516
828
  }
517
829
 
518
- // Check if containers directory exists, if not create it
519
- const containersDir = path.join(infraDir, "containers");
520
- if (!fs.existsSync(containersDir)) {
521
- fs.mkdirSync(containersDir, { recursive: true });
522
- }
523
-
524
830
  // Generate container Bicep file
831
+ const containersDir = path.join(infraDir, "containers");
525
832
  const containerFileName = `${modelKebab}-container.bicep`;
526
833
  const containerFilePath = path.join(containersDir, containerFileName);
527
834
 
@@ -535,13 +842,7 @@ param databaseName string
535
842
  param containerName string = '${modelPascal}s'
536
843
 
537
844
  @description('Partition key path')
538
- param partitionKeyPath string = '/id'
539
-
540
- @description('Throughput (RU/s) - only used for Free Tier')
541
- param throughput int = 400
542
-
543
- @description('Cosmos DB mode: freetier or serverless')
544
- param cosmosDbMode string
845
+ param partitionKeyPath string = '${modelInfo.partitionKey}'
545
846
 
546
847
  // Reference existing Cosmos DB account
547
848
  resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
@@ -582,9 +883,6 @@ resource container 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/container
582
883
  ]
583
884
  }
584
885
  }
585
- options: cosmosDbMode == 'freetier' ? {
586
- throughput: throughput
587
- } : {}
588
886
  }
589
887
  }
590
888
 
@@ -592,29 +890,29 @@ output containerName string = container.name
592
890
  `;
593
891
 
594
892
  // Write Bicep file (overwrite if exists)
595
- fs.writeFileSync(containerFilePath, bicepContent, "utf-8");
893
+ session.writeFile(containerFilePath, bicepContent, { ownership: "managed", generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name });
596
894
  console.log(`✅ Created: ${containerFilePath}`);
597
895
 
598
896
  // Update main.bicep to include this container module
599
- await updateMainBicepWithContainer(modelKebab, modelPascal);
897
+ await updateMainBicepWithContainer(modelKebab, modelPascal, session);
600
898
  }
601
899
 
602
900
  /**
603
901
  * Update main.bicep to include new container module
604
902
  */
605
- async function updateMainBicepWithContainer(modelKebab: string, modelPascal: string): Promise<void> {
903
+ async function updateMainBicepWithContainer(modelKebab: string, modelPascal: string, session: FileOperationSession): Promise<void> {
606
904
  const cwd = process.cwd();
607
905
  const mainBicepPath = path.join(cwd, "infra", "main.bicep");
608
906
 
609
- if (!fs.existsSync(mainBicepPath)) {
907
+ if (!session.fileExists(mainBicepPath)) {
610
908
  console.log("ℹ️ main.bicep not found. Please manually add the container module.");
611
909
  return;
612
910
  }
613
911
 
614
- let mainBicepContent = fs.readFileSync(mainBicepPath, "utf-8");
912
+ let mainBicepContent = session.readFile(mainBicepPath);
615
913
 
616
914
  // Check if container module already exists
617
- const containerModuleName = `${modelKebab.replace(/-/g, '')}Container`;
915
+ const containerModuleName = `${modelPascal.charAt(0).toLowerCase()}${modelPascal.slice(1)}Container`;
618
916
  if (mainBicepContent.includes(`module ${containerModuleName}`)) {
619
917
  console.log(`ℹ️ Container module '${containerModuleName}' already exists in main.bicep`);
620
918
  return;
@@ -653,13 +951,8 @@ module ${containerModuleName} 'containers/${modelKebab}-container.bicep' = {
653
951
  name: '${modelKebab}-container'
654
952
  params: {
655
953
  cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
656
- databaseName: '$` + `{projectName}Database'
657
- cosmosDbMode: cosmosDbMode
954
+ databaseName: '\${projectName}Database'
658
955
  }
659
- dependsOn: [
660
- cosmosDbFreeTier
661
- cosmosDbServerless
662
- ]
663
956
  }`;
664
957
 
665
958
  // Insert the module
@@ -668,14 +961,14 @@ module ${containerModuleName} 'containers/${modelKebab}-container.bicep' = {
668
961
  containerModule +
669
962
  mainBicepContent.slice(insertPosition);
670
963
 
671
- fs.writeFileSync(mainBicepPath, mainBicepContent, "utf-8");
964
+ session.writeFile(mainBicepPath, mainBicepContent, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
672
965
  console.log(`✅ Added container module to main.bicep`);
673
966
  }
674
967
 
675
968
  /**
676
969
  * Generate UI components (list, detail, form, create, edit pages)
677
970
  */
678
- async function generateUIComponents(modelInfo: any, sharedPackageName: string): Promise<void> {
971
+ export async function generateUIComponents(modelInfo: ModelInfo, sharedPackageName: string, authOptions?: UIAuthOptions, session: FileOperationSession = new FileOperationSession("commit")): Promise<void> {
679
972
  console.log("\n🎨 Generating UI components...");
680
973
 
681
974
  const modelKebab = toKebabCase(modelInfo.name);
@@ -689,37 +982,38 @@ async function generateUIComponents(modelInfo: any, sharedPackageName: string):
689
982
  const idDir = path.join(modelDir, "[id]");
690
983
  const editDir = path.join(idDir, "edit");
691
984
 
692
- // Create directories
693
- [componentsDir, newDir, editDir].forEach(dir => {
694
- if (!fs.existsSync(dir)) {
695
- fs.mkdirSync(dir, { recursive: true });
696
- }
697
- });
698
-
699
985
  // Generate and write files
700
- const listPage = generateListPage(modelInfo, sharedPackageName);
701
- const detailPage = generateDetailPage(modelInfo, sharedPackageName);
986
+ const listPage = generateListPage(modelInfo, sharedPackageName, authOptions);
987
+ const detailPage = generateDetailPage(modelInfo, sharedPackageName, authOptions);
702
988
  const formComponent = generateFormComponent(modelInfo, sharedPackageName);
703
- const newPage = generateNewPage(modelInfo);
704
- const editPage = generateEditPage(modelInfo, sharedPackageName);
705
-
706
- fs.writeFileSync(path.join(modelDir, "page.tsx"), listPage, "utf-8");
707
- fs.writeFileSync(path.join(idDir, "page.tsx"), detailPage, "utf-8");
708
- fs.writeFileSync(path.join(componentsDir, `${modelName}Form.tsx`), formComponent, "utf-8");
709
- fs.writeFileSync(path.join(newDir, "page.tsx"), newPage, "utf-8");
710
- fs.writeFileSync(path.join(editDir, "page.tsx"), editPage, "utf-8");
989
+ const newPage = generateNewPage(modelInfo, authOptions);
990
+ const editPage = generateEditPage(modelInfo, sharedPackageName, authOptions);
991
+ const authLayout = generateAuthLayout(modelInfo, authOptions);
992
+
993
+ const meta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
994
+ session.writeFile(path.join(modelDir, "page.tsx"), listPage, meta);
995
+ session.writeFile(path.join(idDir, "page.tsx"), detailPage, meta);
996
+ session.writeFile(path.join(componentsDir, `${modelName}Form.tsx`), formComponent, meta);
997
+ session.writeFile(path.join(newDir, "page.tsx"), newPage, meta);
998
+ session.writeFile(path.join(editDir, "page.tsx"), editPage, meta);
999
+ if (authLayout) {
1000
+ session.writeFile(path.join(modelDir, "layout.tsx"), authLayout, meta);
1001
+ }
711
1002
 
712
1003
  console.log(`✅ Created: ${path.join(modelDir, "page.tsx")}`);
713
1004
  console.log(`✅ Created: ${path.join(idDir, "page.tsx")}`);
714
1005
  console.log(`✅ Created: ${path.join(componentsDir, `${modelName}Form.tsx`)}`);
715
1006
  console.log(`✅ Created: ${path.join(newDir, "page.tsx")}`);
716
1007
  console.log(`✅ Created: ${path.join(editDir, "page.tsx")}`);
1008
+ if (authLayout) {
1009
+ console.log(`✅ Created: ${path.join(modelDir, "layout.tsx")}`);
1010
+ }
717
1011
  }
718
1012
 
719
1013
  /**
720
1014
  * Update navigation menu on the homepage
721
1015
  */
722
- async function updateNavigationMenu(modelInfo: any): Promise<void> {
1016
+ async function updateNavigationMenu(modelInfo: any, session: FileOperationSession): Promise<void> {
723
1017
  console.log("\n📋 Updating navigation menu...");
724
1018
 
725
1019
  const modelKebab = toKebabCase(modelInfo.name);
@@ -728,13 +1022,13 @@ async function updateNavigationMenu(modelInfo: any): Promise<void> {
728
1022
  // Update scaffold config
729
1023
  const configPath = path.join(cwd, "lib", "scaffold-config.ts");
730
1024
 
731
- if (!fs.existsSync(configPath)) {
1025
+ if (!session.fileExists(configPath)) {
732
1026
  console.log("⚠️ scaffold-config.ts not found. Skipping navigation menu update.");
733
1027
  return;
734
1028
  }
735
1029
 
736
1030
  // Read existing config
737
- const configContent = fs.readFileSync(configPath, "utf-8");
1031
+ const configContent = session.readFile(configPath);
738
1032
 
739
1033
  // Parse models array - extract each model entry
740
1034
  const models: Array<{ name: string; path: string; label: string }> = [];
@@ -781,6 +1075,7 @@ ${models.map(m => ` { name: '${m.name}', path: '${m.path}', label: '${m.label
781
1075
  };
782
1076
  `;
783
1077
 
784
- fs.writeFileSync(configPath, newConfigContent, "utf-8");
1078
+ session.writeFile(configPath, newConfigContent, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
785
1079
  console.log(`✅ Added ${modelInfo.name} to navigation menu`);
786
1080
  }
1081
+