swallowkit 1.0.0-beta.6 → 1.0.0-beta.60

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 (296) hide show
  1. package/README.ja.md +86 -177
  2. package/README.md +86 -178
  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 +163 -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 +19 -81
  22. package/dist/cli/commands/create-model.js.map +1 -1
  23. package/dist/cli/commands/dev-seeds.d.ts +62 -0
  24. package/dist/cli/commands/dev-seeds.d.ts.map +1 -0
  25. package/dist/cli/commands/dev-seeds.js +474 -0
  26. package/dist/cli/commands/dev-seeds.js.map +1 -0
  27. package/dist/cli/commands/dev.d.ts +38 -0
  28. package/dist/cli/commands/dev.d.ts.map +1 -1
  29. package/dist/cli/commands/dev.js +733 -155
  30. package/dist/cli/commands/dev.js.map +1 -1
  31. package/dist/cli/commands/index.d.ts +1 -0
  32. package/dist/cli/commands/index.d.ts.map +1 -1
  33. package/dist/cli/commands/index.js +3 -1
  34. package/dist/cli/commands/index.js.map +1 -1
  35. package/dist/cli/commands/init.d.ts +78 -0
  36. package/dist/cli/commands/init.d.ts.map +1 -1
  37. package/dist/cli/commands/init.js +1453 -898
  38. package/dist/cli/commands/init.js.map +1 -1
  39. package/dist/cli/commands/scaffold.d.ts +24 -0
  40. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  41. package/dist/cli/commands/scaffold.js +473 -90
  42. package/dist/cli/commands/scaffold.js.map +1 -1
  43. package/dist/cli/index.d.ts +5 -1
  44. package/dist/cli/index.d.ts.map +1 -1
  45. package/dist/cli/index.js +333 -55
  46. package/dist/cli/index.js.map +1 -1
  47. package/dist/core/config.d.ts +14 -3
  48. package/dist/core/config.d.ts.map +1 -1
  49. package/dist/core/config.js +179 -8
  50. package/dist/core/config.js.map +1 -1
  51. package/dist/core/mock/connector-mock-server.d.ts +101 -0
  52. package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
  53. package/dist/core/mock/connector-mock-server.js +480 -0
  54. package/dist/core/mock/connector-mock-server.js.map +1 -0
  55. package/dist/core/mock/zod-mock-generator.d.ts +14 -0
  56. package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
  57. package/dist/core/mock/zod-mock-generator.js +163 -0
  58. package/dist/core/mock/zod-mock-generator.js.map +1 -0
  59. package/dist/core/operations/auth-operations.d.ts +61 -0
  60. package/dist/core/operations/auth-operations.d.ts.map +1 -0
  61. package/dist/core/operations/auth-operations.js +272 -0
  62. package/dist/core/operations/auth-operations.js.map +1 -0
  63. package/dist/core/operations/create-model.d.ts +15 -0
  64. package/dist/core/operations/create-model.d.ts.map +1 -0
  65. package/dist/core/operations/create-model.js +185 -0
  66. package/dist/core/operations/create-model.js.map +1 -0
  67. package/dist/core/operations/file-session.d.ts +56 -0
  68. package/dist/core/operations/file-session.d.ts.map +1 -0
  69. package/dist/core/operations/file-session.js +203 -0
  70. package/dist/core/operations/file-session.js.map +1 -0
  71. package/dist/core/operations/provision-operations.d.ts +52 -0
  72. package/dist/core/operations/provision-operations.d.ts.map +1 -0
  73. package/dist/core/operations/provision-operations.js +211 -0
  74. package/dist/core/operations/provision-operations.js.map +1 -0
  75. package/dist/core/operations/runtime.d.ts +37 -0
  76. package/dist/core/operations/runtime.d.ts.map +1 -0
  77. package/dist/core/operations/runtime.js +248 -0
  78. package/dist/core/operations/runtime.js.map +1 -0
  79. package/dist/core/operations/scaffold-apply.d.ts +21 -0
  80. package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
  81. package/dist/core/operations/scaffold-apply.js +57 -0
  82. package/dist/core/operations/scaffold-apply.js.map +1 -0
  83. package/dist/core/operations/scaffold-machine.d.ts +16 -0
  84. package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
  85. package/dist/core/operations/scaffold-machine.js +51 -0
  86. package/dist/core/operations/scaffold-machine.js.map +1 -0
  87. package/dist/core/operations/scaffold-plan.d.ts +43 -0
  88. package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
  89. package/dist/core/operations/scaffold-plan.js +187 -0
  90. package/dist/core/operations/scaffold-plan.js.map +1 -0
  91. package/dist/core/project/agent-instructions.d.ts +29 -0
  92. package/dist/core/project/agent-instructions.d.ts.map +1 -0
  93. package/dist/core/project/agent-instructions.js +159 -0
  94. package/dist/core/project/agent-instructions.js.map +1 -0
  95. package/dist/core/project/artifacts.d.ts +58 -0
  96. package/dist/core/project/artifacts.d.ts.map +1 -0
  97. package/dist/core/project/artifacts.js +179 -0
  98. package/dist/core/project/artifacts.js.map +1 -0
  99. package/dist/core/project/boundaries.d.ts +50 -0
  100. package/dist/core/project/boundaries.d.ts.map +1 -0
  101. package/dist/core/project/boundaries.js +117 -0
  102. package/dist/core/project/boundaries.js.map +1 -0
  103. package/dist/core/project/capabilities.d.ts +58 -0
  104. package/dist/core/project/capabilities.d.ts.map +1 -0
  105. package/dist/core/project/capabilities.js +135 -0
  106. package/dist/core/project/capabilities.js.map +1 -0
  107. package/dist/core/project/drift.d.ts +29 -0
  108. package/dist/core/project/drift.d.ts.map +1 -0
  109. package/dist/core/project/drift.js +188 -0
  110. package/dist/core/project/drift.js.map +1 -0
  111. package/dist/core/project/infra.d.ts +49 -0
  112. package/dist/core/project/infra.d.ts.map +1 -0
  113. package/dist/core/project/infra.js +167 -0
  114. package/dist/core/project/infra.js.map +1 -0
  115. package/dist/core/project/manifest.d.ts +92 -0
  116. package/dist/core/project/manifest.d.ts.map +1 -0
  117. package/dist/core/project/manifest.js +321 -0
  118. package/dist/core/project/manifest.js.map +1 -0
  119. package/dist/core/project/state.d.ts +15 -0
  120. package/dist/core/project/state.d.ts.map +1 -0
  121. package/dist/core/project/state.js +106 -0
  122. package/dist/core/project/state.js.map +1 -0
  123. package/dist/core/project/validation.d.ts +20 -0
  124. package/dist/core/project/validation.d.ts.map +1 -0
  125. package/dist/core/project/validation.js +274 -0
  126. package/dist/core/project/validation.js.map +1 -0
  127. package/dist/core/project/workflows.d.ts +43 -0
  128. package/dist/core/project/workflows.d.ts.map +1 -0
  129. package/dist/core/project/workflows.js +299 -0
  130. package/dist/core/project/workflows.js.map +1 -0
  131. package/dist/core/scaffold/auth-generator.d.ts +63 -0
  132. package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
  133. package/dist/core/scaffold/auth-generator.js +1774 -0
  134. package/dist/core/scaffold/auth-generator.js.map +1 -0
  135. package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
  136. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
  137. package/dist/core/scaffold/connector-functions-generator.js +1029 -0
  138. package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
  139. package/dist/core/scaffold/functions-generator.d.ts +7 -1
  140. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  141. package/dist/core/scaffold/functions-generator.js +771 -69
  142. package/dist/core/scaffold/functions-generator.js.map +1 -1
  143. package/dist/core/scaffold/model-parser.d.ts +21 -1
  144. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  145. package/dist/core/scaffold/model-parser.js +243 -43
  146. package/dist/core/scaffold/model-parser.js.map +1 -1
  147. package/dist/core/scaffold/native-schema-generator.d.ts +19 -0
  148. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
  149. package/dist/core/scaffold/native-schema-generator.js +753 -0
  150. package/dist/core/scaffold/native-schema-generator.js.map +1 -0
  151. package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
  152. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  153. package/dist/core/scaffold/nextjs-generator.js +153 -9
  154. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  155. package/dist/core/scaffold/openapi-generator.d.ts +3 -0
  156. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
  157. package/dist/core/scaffold/openapi-generator.js +190 -0
  158. package/dist/core/scaffold/openapi-generator.js.map +1 -0
  159. package/dist/core/scaffold/ui-generator.d.ts +17 -4
  160. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  161. package/dist/core/scaffold/ui-generator.js +174 -34
  162. package/dist/core/scaffold/ui-generator.js.map +1 -1
  163. package/dist/core/verify/index.d.ts +69 -0
  164. package/dist/core/verify/index.d.ts.map +1 -0
  165. package/dist/core/verify/index.js +412 -0
  166. package/dist/core/verify/index.js.map +1 -0
  167. package/dist/database/base-model.d.ts +3 -3
  168. package/dist/database/base-model.js +3 -3
  169. package/dist/index.d.ts +2 -2
  170. package/dist/index.d.ts.map +1 -1
  171. package/dist/index.js +2 -1
  172. package/dist/index.js.map +1 -1
  173. package/dist/machine/contracts.d.ts +24 -0
  174. package/dist/machine/contracts.d.ts.map +1 -0
  175. package/dist/machine/contracts.js +3 -0
  176. package/dist/machine/contracts.js.map +1 -0
  177. package/dist/machine/errors.d.ts +14 -0
  178. package/dist/machine/errors.d.ts.map +1 -0
  179. package/dist/machine/errors.js +42 -0
  180. package/dist/machine/errors.js.map +1 -0
  181. package/dist/machine/index.d.ts +3 -0
  182. package/dist/machine/index.d.ts.map +1 -0
  183. package/dist/machine/index.js +566 -0
  184. package/dist/machine/index.js.map +1 -0
  185. package/dist/mcp/index.d.ts +25 -0
  186. package/dist/mcp/index.d.ts.map +1 -0
  187. package/dist/mcp/index.js +425 -0
  188. package/dist/mcp/index.js.map +1 -0
  189. package/dist/types/index.d.ts +109 -0
  190. package/dist/types/index.d.ts.map +1 -1
  191. package/dist/utils/package-manager.d.ts +5 -2
  192. package/dist/utils/package-manager.d.ts.map +1 -1
  193. package/dist/utils/package-manager.js +30 -9
  194. package/dist/utils/package-manager.js.map +1 -1
  195. package/dist/utils/python-uv.d.ts +21 -0
  196. package/dist/utils/python-uv.d.ts.map +1 -0
  197. package/dist/utils/python-uv.js +111 -0
  198. package/dist/utils/python-uv.js.map +1 -0
  199. package/dist/version.d.ts +2 -0
  200. package/dist/version.d.ts.map +1 -0
  201. package/dist/version.js +49 -0
  202. package/dist/version.js.map +1 -0
  203. package/package.json +9 -5
  204. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +1139 -0
  205. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +206 -0
  206. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +532 -0
  207. package/src/__tests__/agent-instructions.test.ts +237 -0
  208. package/src/__tests__/agent-loop-phase34.test.ts +513 -0
  209. package/src/__tests__/auth.test.ts +770 -0
  210. package/src/__tests__/bff-swa-auth.test.ts +275 -0
  211. package/src/__tests__/config.test.ts +295 -0
  212. package/src/__tests__/connector-functions-generator.test.ts +288 -0
  213. package/src/__tests__/connector-mock-server.test.ts +439 -0
  214. package/src/__tests__/connector-model-bff.test.ts +162 -0
  215. package/src/__tests__/cosmos-worktree-isolation.test.ts +149 -0
  216. package/src/__tests__/dev-seeds.test.ts +236 -0
  217. package/src/__tests__/dev.test.ts +296 -0
  218. package/src/__tests__/file-session.test.ts +98 -0
  219. package/src/__tests__/fixtures.ts +184 -0
  220. package/src/__tests__/functions-generator.test.ts +237 -0
  221. package/src/__tests__/init.test.ts +411 -0
  222. package/src/__tests__/jest-globals.d.ts +8 -0
  223. package/src/__tests__/machine.test.ts +371 -0
  224. package/src/__tests__/mcp.test.ts +253 -0
  225. package/src/__tests__/model-parser.test.ts +81 -0
  226. package/src/__tests__/multi-auth.test.ts +160 -0
  227. package/src/__tests__/nextjs-generator.test.ts +105 -0
  228. package/src/__tests__/openapi-generator.test.ts +43 -0
  229. package/src/__tests__/package-manager.test.ts +227 -0
  230. package/src/__tests__/plan-apply.test.ts +499 -0
  231. package/src/__tests__/python-uv.test.ts +48 -0
  232. package/src/__tests__/scaffold.test.ts +302 -0
  233. package/src/__tests__/string-utils.test.ts +75 -0
  234. package/src/__tests__/tsconfig.json +15 -0
  235. package/src/__tests__/ui-generator.test.ts +248 -0
  236. package/src/__tests__/zod-mock-generator.test.ts +132 -0
  237. package/src/cli/commands/add-auth.ts +853 -0
  238. package/src/cli/commands/add-connector.ts +158 -0
  239. package/src/cli/commands/cosmos-worktree-isolation.ts +130 -0
  240. package/src/cli/commands/create-model.ts +62 -0
  241. package/src/cli/commands/dev-seeds.ts +629 -0
  242. package/src/cli/commands/dev.ts +1252 -0
  243. package/src/cli/commands/index.ts +9 -0
  244. package/src/cli/commands/init.ts +3541 -0
  245. package/src/cli/commands/provision.ts +193 -0
  246. package/src/cli/commands/scaffold.ts +1081 -0
  247. package/src/cli/index.ts +350 -0
  248. package/src/core/config.ts +380 -0
  249. package/src/core/mock/connector-mock-server.ts +555 -0
  250. package/src/core/mock/zod-mock-generator.ts +205 -0
  251. package/src/core/operations/auth-operations.ts +360 -0
  252. package/src/core/operations/create-model.ts +191 -0
  253. package/src/core/operations/file-session.ts +222 -0
  254. package/src/core/operations/provision-operations.ts +297 -0
  255. package/src/core/operations/runtime.ts +258 -0
  256. package/src/core/operations/scaffold-apply.ts +106 -0
  257. package/src/core/operations/scaffold-machine.ts +77 -0
  258. package/src/core/operations/scaffold-plan.ts +215 -0
  259. package/src/core/project/agent-instructions.ts +167 -0
  260. package/src/core/project/agent-templates/AGENTS.md +502 -0
  261. package/src/core/project/agent-templates/CLAUDE.md +31 -0
  262. package/src/core/project/agent-templates/azure-functions.instructions.md +22 -0
  263. package/src/core/project/agent-templates/bff-routes.instructions.md +18 -0
  264. package/src/core/project/agent-templates/copilot-instructions.md +27 -0
  265. package/src/core/project/agent-templates/shared-models.instructions.md +72 -0
  266. package/src/core/project/artifacts.ts +205 -0
  267. package/src/core/project/boundaries.ts +170 -0
  268. package/src/core/project/capabilities.ts +193 -0
  269. package/src/core/project/drift.ts +202 -0
  270. package/src/core/project/infra.ts +196 -0
  271. package/src/core/project/manifest.ts +402 -0
  272. package/src/core/project/state.ts +77 -0
  273. package/src/core/project/validation.ts +295 -0
  274. package/src/core/project/workflows.ts +295 -0
  275. package/src/core/scaffold/auth-generator.ts +1824 -0
  276. package/src/core/scaffold/connector-functions-generator.ts +1133 -0
  277. package/src/core/scaffold/functions-generator.ts +972 -0
  278. package/src/core/scaffold/model-parser.ts +854 -0
  279. package/src/core/scaffold/native-schema-generator.ts +904 -0
  280. package/src/core/scaffold/nextjs-generator.ts +382 -0
  281. package/src/core/scaffold/openapi-generator.ts +212 -0
  282. package/src/core/scaffold/ui-generator.ts +1100 -0
  283. package/src/core/verify/index.ts +468 -0
  284. package/src/database/base-model.ts +184 -0
  285. package/src/database/client.ts +140 -0
  286. package/src/database/repository.ts +104 -0
  287. package/src/database/runtime-check.ts +25 -0
  288. package/src/index.ts +27 -0
  289. package/src/machine/contracts.ts +33 -0
  290. package/src/machine/errors.ts +45 -0
  291. package/src/machine/index.ts +690 -0
  292. package/src/mcp/index.ts +423 -0
  293. package/src/types/index.ts +182 -0
  294. package/src/utils/package-manager.ts +246 -0
  295. package/src/utils/python-uv.ts +96 -0
  296. package/src/version.ts +13 -0
@@ -0,0 +1,1081 @@
1
+ /**
2
+ * SwallowKit Scaffold ć‚³ćƒžćƒ³ćƒ‰
3
+ * Zod ćƒ¢ćƒ‡ćƒ«ć‹ć‚‰ Azure Functions と Next.js BFF 恮 CRUD ć‚³ćƒ¼ćƒ‰ć‚’ē”Ÿęˆ
4
+ */
5
+
6
+ import * as fs from "fs";
7
+ import * as path from "path";
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
+ import {
12
+ generateCSharpAzureFunctionsCRUD,
13
+ generateCompactAzureFunctionsCRUD,
14
+ generatePythonAzureFunctionsCRUD,
15
+ } from "../../core/scaffold/functions-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";
25
+ import {
26
+ generateListPage,
27
+ generateDetailPage,
28
+ generateFormComponent,
29
+ generateNewPage,
30
+ generateEditPage,
31
+ generateAuthLayout,
32
+ UIAuthOptions,
33
+ } from "../../core/scaffold/ui-generator";
34
+ import { generateLanguageSchemaArtifacts, validateNativeSchemaToolchain } from "../../core/scaffold/native-schema-generator";
35
+ import { detectFromProject, getCommands } from "../../utils/package-manager";
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";
55
+
56
+ interface ScaffoldOptions {
57
+ model: string; // ćƒ¢ćƒ‡ćƒ«ćƒ•ć‚”ć‚¤ćƒ«ć®ćƒ‘ć‚¹ļ¼ˆä¾‹: "lib/models/todo.ts" or "todo")
58
+ functionsDir?: string; // Azure Functions ć®ćƒ‡ć‚£ćƒ¬ć‚ÆćƒˆćƒŖļ¼ˆćƒ‡ćƒ•ć‚©ćƒ«ćƒˆ: "functions")
59
+ apiDir?: string; // Next.js API routes ć®ćƒ‡ć‚£ćƒ¬ć‚ÆćƒˆćƒŖļ¼ˆćƒ‡ćƒ•ć‚©ćƒ«ćƒˆ: "app/api")
60
+ apiOnly?: boolean; // true ć®å “åˆć€UI ć‚’ē”Ÿęˆć—ćŖć„ļ¼ˆćƒ‡ćƒ•ć‚©ćƒ«ćƒˆ: false)
61
+ }
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
+
83
+ export async function scaffoldCommand(options: ScaffoldOptions) {
84
+ // SwallowKit ćƒ—ćƒ­ć‚øć‚§ć‚Æćƒˆćƒ‡ć‚£ćƒ¬ć‚ÆćƒˆćƒŖć‹ć©ć†ć‹ć‚’ę¤œčØ¼
85
+ ensureSwallowKitProject("scaffold");
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
+
93
+ console.log("šŸ—ļø SwallowKit Scaffold: Generating CRUD operations...\n");
94
+
95
+ try {
96
+ // 1. Resolve model file path
97
+ const modelPath = resolveModelPath(options.model);
98
+ console.log(`šŸ“„ Model file: ${modelPath}`);
99
+
100
+ // 2. Parse model file
101
+ console.log("šŸ” Parsing model file...");
102
+ const modelInfo = await parseModelFile(modelPath);
103
+ console.log(`āœ… Model parsed: ${modelInfo.name} (${modelInfo.schemaName})`);
104
+
105
+ const backendLanguage = getBackendLanguage();
106
+ console.log(`🧠 Backend language: ${backendLanguage}`);
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
+
115
+ // ćƒć‚¹ćƒˆć‚¹ć‚­ćƒ¼ćƒžå‚ē…§ćŒć‚ć‚Œć°č”Øē¤ŗ
116
+ if (modelInfo.nestedSchemaRefs.length > 0) {
117
+ console.log(`šŸ”— Nested schema references detected:`);
118
+ for (const ref of modelInfo.nestedSchemaRefs) {
119
+ const relType = ref.isArray ? 'array' : 'single';
120
+ const optional = ref.isOptional ? ' (optional)' : '';
121
+ console.log(` - ${ref.fieldName}: ${ref.modelName} [${relType}]${optional}`);
122
+ }
123
+ }
124
+
125
+ // 3. Check for ID field
126
+ if (!modelInfo.hasId) {
127
+ console.warn(
128
+ "āš ļø Warning: Model does not have an 'id' field. CRUD operations may not work correctly."
129
+ );
130
+ }
131
+
132
+ // 4. Read shared package name
133
+ const functionsDir = options.functionsDir || "functions";
134
+ const sharedPackageName = readSharedPackageName();
135
+ const relatedModels = backendLanguage === "typescript"
136
+ ? [modelInfo]
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
+ }
155
+
156
+ // 5. Generate BFF callFunction helper
157
+ await generateCallFunctionHelper(session);
158
+
159
+ // 6. Generate Azure Functions code
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
+ }
170
+ }
171
+
172
+ // 7. Generate Next.js BFF API Routes
173
+ const apiDir = options.apiDir || "app/api";
174
+ if (isConnectorModel) {
175
+ await generateConnectorBFFRoutesFiles(modelInfo, apiDir, sharedPackageName, session);
176
+ } else {
177
+ await generateBFFRoutes(modelInfo, apiDir, sharedPackageName, session);
178
+ }
179
+
180
+ // 8. Generate Cosmos DB container Bicep file (skip for connector models)
181
+ if (!isConnectorModel) {
182
+ await generateCosmosContainer(modelInfo, session);
183
+ }
184
+
185
+ // 9. Generate UI components (unless --api-only)
186
+ if (!options.apiOnly) {
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();
206
+ }
207
+
208
+ console.log("\nāœ… Scaffold completed successfully!");
209
+ console.log("\nšŸ“ Next steps:");
210
+ console.log(` 1. Review generated files in ${describeFunctionsOutputPath(functionsDir, backendLanguage)} and ${apiDir}/`);
211
+ if (!options.apiOnly) {
212
+ console.log(` 2. Check the generated UI pages in app/${toKebabCase(modelInfo.name)}/`);
213
+ console.log(" 3. Navigate to the model from the homepage menu");
214
+ }
215
+ if (backendLanguage !== "typescript") {
216
+ console.log(` ${options.apiOnly ? "2" : "4"}. Review generated OpenAPI export and native schema assets in ${functionsDir}/openapi/ and ${functionsDir}/generated/`);
217
+ }
218
+ console.log(
219
+ ` ${options.apiOnly ? (backendLanguage === "typescript" ? "2" : "3") : (backendLanguage === "typescript" ? "4" : "5")}. Ensure BACKEND_FUNCTIONS_BASE_URL is set in your .env.local file`
220
+ );
221
+ console.log(
222
+ ` ${options.apiOnly ? (backendLanguage === "typescript" ? "3" : "4") : (backendLanguage === "typescript" ? "5" : "6")}. Configure CosmosDBConnection in functions/local.settings.json`
223
+ );
224
+ console.log(` ${options.apiOnly ? (backendLanguage === "typescript" ? "4" : "5") : (backendLanguage === "typescript" ? "6" : "7")}. Run '${getCommands(detectFromProject()).dlx} swallowkit dev' to test the generated code`);
225
+ } catch (error: any) {
226
+ console.error("\nāŒ Scaffold failed:", error.message);
227
+ process.exit(1);
228
+ }
229
+ }
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
+
287
+ /**
288
+ * ćƒ¢ćƒ‡ćƒ«ćƒ•ć‚”ć‚¤ćƒ«ć®ćƒ‘ć‚¹ć‚’č§£ę±ŗ
289
+ */
290
+ export function resolveModelPath(modelInput: string): string {
291
+ const cwd = process.cwd();
292
+
293
+ // ēµ¶åÆ¾ćƒ‘ć‚¹ć®å “åˆ
294
+ if (path.isAbsolute(modelInput)) {
295
+ return modelInput;
296
+ }
297
+
298
+ // ę‹”å¼µå­ćŒć‚ć‚‹å “åˆļ¼ˆē›øåÆ¾ćƒ‘ć‚¹ļ¼‰
299
+ if (modelInput.endsWith(".ts")) {
300
+ const fullPath = path.join(cwd, modelInput);
301
+ if (fs.existsSync(fullPath)) {
302
+ return fullPath;
303
+ }
304
+ }
305
+
306
+ // ę‹”å¼µå­ćŒćŖć„å “åˆć€shared/models/ ć§ęŽ¢ć™
307
+ const defaultPath = path.join(cwd, "shared", "models", `${modelInput}.ts`);
308
+ if (fs.existsSync(defaultPath)) {
309
+ return defaultPath;
310
+ }
311
+
312
+ // lib/models/ ć§ć‚‚ęŽ¢ć™ļ¼ˆå¾Œę–¹äŗ’ę›ę€§ļ¼‰
313
+ const libPath = path.join(cwd, "lib", "models", `${modelInput}.ts`);
314
+ if (fs.existsSync(libPath)) {
315
+ return libPath;
316
+ }
317
+
318
+ // src/models/ ć§ć‚‚ęŽ¢ć™
319
+ const srcPath = path.join(cwd, "src", "models", `${modelInput}.ts`);
320
+ if (fs.existsSync(srcPath)) {
321
+ return srcPath;
322
+ }
323
+
324
+ // č¦‹ć¤ć‹ć‚‰ćŖć„å “åˆćÆć‚Øćƒ©ćƒ¼
325
+ throw new Error(
326
+ `Model file not found: ${modelInput}\n` +
327
+ ` Tried:\n` +
328
+ ` - ${modelInput}\n` +
329
+ ` - ${defaultPath}\n` +
330
+ ` - ${libPath}\n` +
331
+ ` - ${srcPath}\n` +
332
+ ` Please specify a valid model file path.`
333
+ );
334
+ }
335
+
336
+ /**
337
+ * Read shared package name from shared/package.json
338
+ */
339
+ function readSharedPackageName(): string {
340
+ const cwd = process.cwd();
341
+ const sharedPkgPath = path.join(cwd, "shared", "package.json");
342
+
343
+ if (!fs.existsSync(sharedPkgPath)) {
344
+ throw new Error(
345
+ "shared/package.json not found.\n" +
346
+ "The shared package is required for model imports.\n" +
347
+ `Run "${getCommands(detectFromProject()).dlx} swallowkit init" to set up your project.`
348
+ );
349
+ }
350
+
351
+ const pkg = JSON.parse(fs.readFileSync(sharedPkgPath, "utf-8"));
352
+ return pkg.name;
353
+ }
354
+
355
+ /**
356
+ * Generate BFF callFunction helper (lib/api/call-function.ts)
357
+ */
358
+ async function generateCallFunctionHelper(session: FileOperationSession): Promise<void> {
359
+ console.log("\nšŸ“¦ Generating BFF callFunction helper...");
360
+
361
+ const cwd = process.cwd();
362
+
363
+ const helperDir = path.join(cwd, "lib", "api");
364
+ const helperPath = path.join(helperDir, "call-function.ts");
365
+
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();
378
+ }
379
+
380
+ session.writeFile(helperPath, helperCode, { ownership: "managed", generator: SCAFFOLD_GENERATOR });
381
+ console.log(`āœ… Created: ${helperPath}`);
382
+ }
383
+
384
+ /**
385
+ * Generate Azure Functions CRUD code
386
+ */
387
+ async function generateFunctionsCode(
388
+ modelInfo: ModelInfo,
389
+ functionsDir: string,
390
+ sharedPackageName: string,
391
+ backendLanguage: BackendLanguage,
392
+ session: FileOperationSession
393
+ ): Promise<void> {
394
+ console.log("\nšŸ”Ø Generating Azure Functions CRUD code...");
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
+
503
+ const modelKebab = toKebabCase(modelInfo.name);
504
+ const connectorManagedMeta = { ownership: "managed" as const, generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name };
505
+
506
+ if (backendLanguage === "typescript") {
507
+ const functionFilePath = path.join(
508
+ process.cwd(),
509
+ functionsDir,
510
+ "src",
511
+ `${modelKebab}.ts`
512
+ );
513
+
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
+ );
531
+ }
532
+
533
+ session.writeFile(functionFilePath, code, connectorManagedMeta);
534
+ console.log(`āœ… Created: ${functionFilePath}`);
535
+ return;
536
+ }
537
+
538
+ if (backendLanguage === "csharp") {
539
+ const functionFilePath = path.join(
540
+ process.cwd(),
541
+ functionsDir,
542
+ "Connectors",
543
+ `${modelInfo.name}ConnectorFunctions.cs`
544
+ );
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);
562
+ console.log(`āœ… Created: ${functionFilePath}`);
563
+ return;
564
+ }
565
+
566
+ // Python
567
+ const blueprintsDir = path.join(process.cwd(), functionsDir, "blueprints");
568
+ const blueprintPath = path.join(blueprintsDir, `${modelKebab.replace(/-/g, "_")}.py`);
569
+
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);
587
+ console.log(`āœ… Created: ${blueprintPath}`);
588
+ }
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
+
711
+ async function collectModelGraph(modelPath: string, seen = new Map<string, ModelInfo>()): Promise<ModelInfo[]> {
712
+ const resolvedPath = path.resolve(modelPath);
713
+ if (seen.has(resolvedPath)) {
714
+ return Array.from(seen.values());
715
+ }
716
+
717
+ const modelInfo = await parseModelFile(resolvedPath);
718
+ seen.set(resolvedPath, modelInfo);
719
+
720
+ for (const ref of modelInfo.nestedSchemaRefs) {
721
+ const nestedPath = resolveNestedModelPath(resolvedPath, ref.importPath);
722
+ await collectModelGraph(nestedPath, seen);
723
+ }
724
+
725
+ return Array.from(seen.values());
726
+ }
727
+
728
+ function resolveNestedModelPath(modelPath: string, importPath: string): string {
729
+ let resolvedPath = path.resolve(path.dirname(modelPath), importPath);
730
+ if (!resolvedPath.endsWith(".ts")) {
731
+ resolvedPath += ".ts";
732
+ }
733
+ return resolvedPath;
734
+ }
735
+
736
+ function describeFunctionsOutputPath(functionsDir: string, backendLanguage: BackendLanguage): string {
737
+ if (backendLanguage === "typescript") {
738
+ return `${functionsDir}/src/`;
739
+ }
740
+ if (backendLanguage === "csharp") {
741
+ return `${functionsDir}/Crud/`;
742
+ }
743
+ return `${functionsDir}/blueprints/`;
744
+ }
745
+
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}`);
749
+ }
750
+
751
+ const content = session.readFile(functionAppPath);
752
+
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)) {
756
+ return;
757
+ }
758
+ if (content.includes(registration)) {
759
+ return;
760
+ }
761
+
762
+ const marker = "# SwallowKit scaffold registrations";
763
+ if (!content.includes(marker)) {
764
+ throw new Error(`Could not find scaffold registration marker in ${functionAppPath}`);
765
+ }
766
+
767
+ const updated = content.replace(marker, `${registration}\n${marker}`);
768
+ session.writeFile(functionAppPath, updated, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
769
+ }
770
+
771
+ /**
772
+ * Next.js BFF API Routes ć‚’ē”Ÿęˆ
773
+ */
774
+ async function generateBFFRoutes(
775
+ modelInfo: any,
776
+ apiDir: string,
777
+ sharedPackageName: string,
778
+ session: FileOperationSession
779
+ ): Promise<void> {
780
+ console.log("\nšŸ”Ø Generating Next.js BFF API routes...");
781
+
782
+ const modelCamel = modelInfo.name.charAt(0).toLowerCase() + modelInfo.name.slice(1);
783
+
784
+ // List route: app/api/[model]/route.ts
785
+ const listRoutePath = path.join(
786
+ process.cwd(),
787
+ apiDir,
788
+ modelCamel,
789
+ "route.ts"
790
+ );
791
+
792
+ // Detail route: app/api/[model]/[id]/route.ts
793
+ const detailRoutePath = path.join(
794
+ process.cwd(),
795
+ apiDir,
796
+ modelCamel,
797
+ "[id]",
798
+ "route.ts"
799
+ );
800
+
801
+ // ć‚³ćƒ¼ćƒ‰ć‚’ē”Ÿęˆ
802
+ const routes = generateCompactBFFRoutes(modelInfo, sharedPackageName);
803
+
804
+ // ćƒ•ć‚”ć‚¤ćƒ«ć«ę›øćč¾¼ćæ
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);
808
+
809
+ console.log(`āœ… Created: ${listRoutePath}`);
810
+ console.log(`āœ… Created: ${detailRoutePath}`);
811
+ }
812
+
813
+ /**
814
+ * Generate Cosmos DB container Bicep file
815
+ */
816
+ export async function generateCosmosContainer(modelInfo: any, session: FileOperationSession = new FileOperationSession("commit")): Promise<void> {
817
+ console.log("\nšŸ—„ļø Generating Cosmos DB container Bicep file...");
818
+
819
+ const modelKebab = toKebabCase(modelInfo.name);
820
+ const modelPascal = toPascalCase(modelInfo.name);
821
+ const cwd = process.cwd();
822
+
823
+ // Check if infra directory exists
824
+ const infraDir = path.join(cwd, "infra");
825
+ if (!fs.existsSync(infraDir)) {
826
+ console.log("ā„¹ļø infra directory not found. Skipping Cosmos DB container generation.");
827
+ return;
828
+ }
829
+
830
+ // Generate container Bicep file
831
+ const containersDir = path.join(infraDir, "containers");
832
+ const containerFileName = `${modelKebab}-container.bicep`;
833
+ const containerFilePath = path.join(containersDir, containerFileName);
834
+
835
+ const bicepContent = `@description('Cosmos DB account name')
836
+ param cosmosAccountName string
837
+
838
+ @description('Database name')
839
+ param databaseName string
840
+
841
+ @description('Container name')
842
+ param containerName string = '${modelPascal}s'
843
+
844
+ @description('Partition key path')
845
+ param partitionKeyPath string = '${modelInfo.partitionKey}'
846
+
847
+ // Reference existing Cosmos DB account
848
+ resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
849
+ name: cosmosAccountName
850
+ }
851
+
852
+ // Reference existing database
853
+ resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' existing = {
854
+ parent: cosmosAccount
855
+ name: databaseName
856
+ }
857
+
858
+ // Container for ${modelPascal}
859
+ resource container 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-11-15' = {
860
+ parent: database
861
+ name: containerName
862
+ properties: {
863
+ resource: {
864
+ id: containerName
865
+ partitionKey: {
866
+ paths: [
867
+ partitionKeyPath
868
+ ]
869
+ kind: 'Hash'
870
+ }
871
+ indexingPolicy: {
872
+ automatic: true
873
+ indexingMode: 'consistent'
874
+ includedPaths: [
875
+ {
876
+ path: '/*'
877
+ }
878
+ ]
879
+ excludedPaths: [
880
+ {
881
+ path: '/_etag/?'
882
+ }
883
+ ]
884
+ }
885
+ }
886
+ }
887
+ }
888
+
889
+ output containerName string = container.name
890
+ `;
891
+
892
+ // Write Bicep file (overwrite if exists)
893
+ session.writeFile(containerFilePath, bicepContent, { ownership: "managed", generator: SCAFFOLD_GENERATOR, sourceModel: modelInfo.name });
894
+ console.log(`āœ… Created: ${containerFilePath}`);
895
+
896
+ // Update main.bicep to include this container module
897
+ await updateMainBicepWithContainer(modelKebab, modelPascal, session);
898
+ }
899
+
900
+ /**
901
+ * Update main.bicep to include new container module
902
+ */
903
+ async function updateMainBicepWithContainer(modelKebab: string, modelPascal: string, session: FileOperationSession): Promise<void> {
904
+ const cwd = process.cwd();
905
+ const mainBicepPath = path.join(cwd, "infra", "main.bicep");
906
+
907
+ if (!session.fileExists(mainBicepPath)) {
908
+ console.log("ā„¹ļø main.bicep not found. Please manually add the container module.");
909
+ return;
910
+ }
911
+
912
+ let mainBicepContent = session.readFile(mainBicepPath);
913
+
914
+ // Check if container module already exists
915
+ const containerModuleName = `${modelPascal.charAt(0).toLowerCase()}${modelPascal.slice(1)}Container`;
916
+ if (mainBicepContent.includes(`module ${containerModuleName}`)) {
917
+ console.log(`ā„¹ļø Container module '${containerModuleName}' already exists in main.bicep`);
918
+ return;
919
+ }
920
+
921
+ // Find the position to insert the container module (after cosmosDb modules)
922
+ // Look for the end of both cosmosDb modules (FreeTier and Serverless)
923
+ const cosmosModulePattern = /module cosmosDbServerless ['"]modules\/cosmosdb-serverless\.bicep['"] = if \(cosmosDbMode == 'serverless'\) \{[\s\S]*?\n\}/;
924
+ const cosmosModuleMatch = mainBicepContent.match(cosmosModulePattern);
925
+
926
+ if (!cosmosModuleMatch) {
927
+ console.log("āš ļø Could not find Cosmos DB Serverless module in main.bicep. Please manually add the container module:");
928
+ console.log(`\nmodule ${containerModuleName} 'containers/${modelKebab}-container.bicep' = {
929
+ name: '${modelKebab}-container'
930
+ params: {
931
+ cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
932
+ databaseName: databaseName
933
+ cosmosDbMode: cosmosDbMode
934
+ }
935
+ dependsOn: [
936
+ cosmosDbFreeTier
937
+ cosmosDbServerless
938
+ ]
939
+ }\n`);
940
+ return;
941
+ }
942
+
943
+ // Find the end of the cosmosDbServerless module
944
+ const insertPosition = cosmosModuleMatch.index! + cosmosModuleMatch[0].length;
945
+
946
+ // Create the container module declaration
947
+ const containerModule = `
948
+
949
+ // ${modelPascal} Container
950
+ module ${containerModuleName} 'containers/${modelKebab}-container.bicep' = {
951
+ name: '${modelKebab}-container'
952
+ params: {
953
+ cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
954
+ databaseName: '\${projectName}Database'
955
+ }
956
+ }`;
957
+
958
+ // Insert the module
959
+ mainBicepContent =
960
+ mainBicepContent.slice(0, insertPosition) +
961
+ containerModule +
962
+ mainBicepContent.slice(insertPosition);
963
+
964
+ session.writeFile(mainBicepPath, mainBicepContent, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
965
+ console.log(`āœ… Added container module to main.bicep`);
966
+ }
967
+
968
+ /**
969
+ * Generate UI components (list, detail, form, create, edit pages)
970
+ */
971
+ export async function generateUIComponents(modelInfo: ModelInfo, sharedPackageName: string, authOptions?: UIAuthOptions, session: FileOperationSession = new FileOperationSession("commit")): Promise<void> {
972
+ console.log("\nšŸŽØ Generating UI components...");
973
+
974
+ const modelKebab = toKebabCase(modelInfo.name);
975
+ const modelName = modelInfo.name;
976
+ const cwd = process.cwd();
977
+
978
+ // Create directory structure: app/[model]/
979
+ const modelDir = path.join(cwd, "app", modelKebab);
980
+ const componentsDir = path.join(modelDir, "_components");
981
+ const newDir = path.join(modelDir, "new");
982
+ const idDir = path.join(modelDir, "[id]");
983
+ const editDir = path.join(idDir, "edit");
984
+
985
+ // Generate and write files
986
+ const listPage = generateListPage(modelInfo, sharedPackageName, authOptions);
987
+ const detailPage = generateDetailPage(modelInfo, sharedPackageName, authOptions);
988
+ const formComponent = generateFormComponent(modelInfo, sharedPackageName);
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
+ }
1002
+
1003
+ console.log(`āœ… Created: ${path.join(modelDir, "page.tsx")}`);
1004
+ console.log(`āœ… Created: ${path.join(idDir, "page.tsx")}`);
1005
+ console.log(`āœ… Created: ${path.join(componentsDir, `${modelName}Form.tsx`)}`);
1006
+ console.log(`āœ… Created: ${path.join(newDir, "page.tsx")}`);
1007
+ console.log(`āœ… Created: ${path.join(editDir, "page.tsx")}`);
1008
+ if (authLayout) {
1009
+ console.log(`āœ… Created: ${path.join(modelDir, "layout.tsx")}`);
1010
+ }
1011
+ }
1012
+
1013
+ /**
1014
+ * Update navigation menu on the homepage
1015
+ */
1016
+ async function updateNavigationMenu(modelInfo: any, session: FileOperationSession): Promise<void> {
1017
+ console.log("\nšŸ“‹ Updating navigation menu...");
1018
+
1019
+ const modelKebab = toKebabCase(modelInfo.name);
1020
+ const cwd = process.cwd();
1021
+
1022
+ // Update scaffold config
1023
+ const configPath = path.join(cwd, "lib", "scaffold-config.ts");
1024
+
1025
+ if (!session.fileExists(configPath)) {
1026
+ console.log("āš ļø scaffold-config.ts not found. Skipping navigation menu update.");
1027
+ return;
1028
+ }
1029
+
1030
+ // Read existing config
1031
+ const configContent = session.readFile(configPath);
1032
+
1033
+ // Parse models array - extract each model entry
1034
+ const models: Array<{ name: string; path: string; label: string }> = [];
1035
+ const modelsMatch = configContent.match(/models:\s*\[([\s\S]*?)\]\s*as\s*ScaffoldModel\[\]/);
1036
+
1037
+ if (modelsMatch) {
1038
+ const modelsArrayContent = modelsMatch[1];
1039
+ // Match each object in the array: { name: '...', path: '...', label: '...' }
1040
+ const modelPattern = /\{\s*name:\s*['"]([^'"]+)['"]\s*,\s*path:\s*['"]([^'"]+)['"]\s*,\s*label:\s*['"]([^'"]+)['"]\s*\}/g;
1041
+ let match;
1042
+ while ((match = modelPattern.exec(modelsArrayContent)) !== null) {
1043
+ models.push({
1044
+ name: match[1],
1045
+ path: match[2],
1046
+ label: match[3]
1047
+ });
1048
+ }
1049
+ }
1050
+
1051
+ // Check if model already exists
1052
+ if (models.find(m => m.name === modelInfo.name)) {
1053
+ console.log(`ā„¹ļø ${modelInfo.name} already in navigation menu`);
1054
+ return;
1055
+ }
1056
+
1057
+ // Add new model
1058
+ models.push({
1059
+ name: modelInfo.name,
1060
+ path: `/${modelKebab}`,
1061
+ label: modelInfo.displayName
1062
+ });
1063
+
1064
+ // Generate new scaffold-config.ts content
1065
+ const newConfigContent = `export interface ScaffoldModel {
1066
+ name: string;
1067
+ path: string;
1068
+ label: string;
1069
+ }
1070
+
1071
+ export const scaffoldConfig = {
1072
+ models: [
1073
+ ${models.map(m => ` { name: '${m.name}', path: '${m.path}', label: '${m.label}' },`).join('\n')}
1074
+ ] as ScaffoldModel[]
1075
+ };
1076
+ `;
1077
+
1078
+ session.writeFile(configPath, newConfigContent, { ownership: "extension-point", generator: SCAFFOLD_GENERATOR });
1079
+ console.log(`āœ… Added ${modelInfo.name} to navigation menu`);
1080
+ }
1081
+