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
@@ -36,12 +36,160 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.getStaticWebAppSku = getStaticWebAppSku;
40
+ exports.buildCosmosDbFreeTierBicepSource = buildCosmosDbFreeTierBicepSource;
41
+ exports.buildSharedTsConfig = buildSharedTsConfig;
39
42
  exports.initCommand = initCommand;
43
+ exports.withNpmLockfileSafeManifests = withNpmLockfileSafeManifests;
44
+ exports.parseIgnoredBuilds = parseIgnoredBuilds;
45
+ exports.approveBuildsViaWorkspaceYaml = approveBuildsViaWorkspaceYaml;
46
+ exports.injectSwallowKitNextConfig = injectSwallowKitNextConfig;
47
+ exports.buildCSharpFunctionsProgramSource = buildCSharpFunctionsProgramSource;
48
+ exports.buildCSharpFunctionsProjectSource = buildCSharpFunctionsProjectSource;
49
+ exports.buildSwallowKitConfigSource = buildSwallowKitConfigSource;
50
+ exports.buildGeneratedProjectDependencies = buildGeneratedProjectDependencies;
51
+ exports.buildGeneratedProjectDevDependencies = buildGeneratedProjectDevDependencies;
52
+ exports.buildSwallowKitMcpProjectConfigSource = buildSwallowKitMcpProjectConfigSource;
53
+ exports.buildTypeScriptFunctionsPackageJson = buildTypeScriptFunctionsPackageJson;
54
+ exports.buildFunctionsHostKeyBicepExpression = buildFunctionsHostKeyBicepExpression;
55
+ exports.buildStaticWebAppConfigBicepSource = buildStaticWebAppConfigBicepSource;
56
+ exports.createInfrastructure = createInfrastructure;
57
+ exports.getGitHubFunctionsWorkflow = getGitHubFunctionsWorkflow;
58
+ exports.getAzureFunctionsPipeline = getAzureFunctionsPipeline;
59
+ exports.buildAzureSwaPipeline = buildAzureSwaPipeline;
60
+ exports.buildGitHubSwaWorkflow = buildGitHubSwaWorkflow;
40
61
  const fs = __importStar(require("fs"));
41
62
  const path = __importStar(require("path"));
42
63
  const child_process_1 = require("child_process");
43
64
  const prompts_1 = __importDefault(require("prompts"));
44
65
  const package_manager_1 = require("../../utils/package-manager");
66
+ const manifest_1 = require("../../core/project/manifest");
67
+ const workflows_1 = require("../../core/project/workflows");
68
+ const agent_instructions_1 = require("../../core/project/agent-instructions");
69
+ const native_schema_generator_1 = require("../../core/scaffold/native-schema-generator");
70
+ const python_uv_1 = require("../../utils/python-uv");
71
+ function getStaticWebAppSku(plan) {
72
+ return plan === 'free' ? 'Free' : 'Standard';
73
+ }
74
+ const BACKEND_LANGUAGE_CHOICES = [
75
+ { title: "TypeScript", value: "typescript" },
76
+ { title: "C#", value: "csharp" },
77
+ { title: "Python", value: "python" },
78
+ ];
79
+ function usesNodeFunctionsProject(backendLanguage) {
80
+ return backendLanguage === "typescript";
81
+ }
82
+ function getBackendLanguageLabel(backendLanguage) {
83
+ return BACKEND_LANGUAGE_CHOICES.find((choice) => choice.value === backendLanguage)?.title || backendLanguage;
84
+ }
85
+ function buildCosmosDbFreeTierBicepSource() {
86
+ return `@description('Cosmos DB account name')
87
+ param accountName string
88
+
89
+ @description('Database name')
90
+ param databaseName string
91
+
92
+ @description('Location for Cosmos DB')
93
+ param location string
94
+
95
+ @description('Public network access')
96
+ @allowed(['Enabled', 'Disabled'])
97
+ param publicNetworkAccess string = 'Enabled'
98
+
99
+ // Cosmos DB Account (Free Tier)
100
+ resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
101
+ name: accountName
102
+ location: location
103
+ kind: 'GlobalDocumentDB'
104
+ properties: {
105
+ databaseAccountOfferType: 'Standard'
106
+ enableAutomaticFailover: false
107
+ enableFreeTier: true
108
+ publicNetworkAccess: publicNetworkAccess
109
+ disableLocalAuth: true
110
+ consistencyPolicy: {
111
+ defaultConsistencyLevel: 'Session'
112
+ }
113
+ locations: [
114
+ {
115
+ locationName: location
116
+ failoverPriority: 0
117
+ isZoneRedundant: false
118
+ }
119
+ ]
120
+ disableKeyBasedMetadataWriteAccess: true
121
+ }
122
+ }
123
+
124
+ // Cosmos DB Database
125
+ resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
126
+ parent: cosmosAccount
127
+ name: databaseName
128
+ properties: {
129
+ resource: {
130
+ id: databaseName
131
+ }
132
+ options: {
133
+ throughput: 1000
134
+ }
135
+ }
136
+ }
137
+
138
+ output id string = cosmosAccount.id
139
+ output accountName string = cosmosAccount.name
140
+ output endpoint string = cosmosAccount.properties.documentEndpoint
141
+ output databaseName string = database.name
142
+ `;
143
+ }
144
+ function getFunctionsWorkerRuntime(backendLanguage) {
145
+ if (backendLanguage === "csharp") {
146
+ return "dotnet-isolated";
147
+ }
148
+ if (backendLanguage === "python") {
149
+ return "python";
150
+ }
151
+ return "node";
152
+ }
153
+ function getFunctionsRuntimeConfig(backendLanguage) {
154
+ if (backendLanguage === "csharp") {
155
+ return { name: "dotnet-isolated", version: "10.0" };
156
+ }
157
+ if (backendLanguage === "python") {
158
+ return { name: "python", version: "3.11" };
159
+ }
160
+ return { name: "node", version: "22" };
161
+ }
162
+ function buildSharedTsConfig() {
163
+ return {
164
+ compilerOptions: {
165
+ target: "ES2020",
166
+ module: "Node16",
167
+ moduleResolution: "node16",
168
+ lib: ["ES2020"],
169
+ outDir: "dist",
170
+ rootDir: ".",
171
+ declaration: true,
172
+ declarationMap: true,
173
+ sourceMap: true,
174
+ strict: true,
175
+ esModuleInterop: true,
176
+ skipLibCheck: true,
177
+ forceConsistentCasingInFileNames: true,
178
+ },
179
+ include: ["index.ts", "models/**/*"],
180
+ exclude: ["node_modules", "dist"],
181
+ };
182
+ }
183
+ async function promptBackendLanguage() {
184
+ const response = await (0, prompts_1.default)({
185
+ type: "select",
186
+ name: "backendLanguage",
187
+ message: "Azure Functions backend language:",
188
+ choices: BACKEND_LANGUAGE_CHOICES,
189
+ initial: 0,
190
+ });
191
+ return response.backendLanguage || "typescript";
192
+ }
45
193
  async function promptCiCd() {
46
194
  const response = await (0, prompts_1.default)({
47
195
  type: 'select',
@@ -56,40 +204,69 @@ async function promptCiCd() {
56
204
  });
57
205
  return response.cicd || 'skip';
58
206
  }
59
- async function promptAzureConfig() {
60
- const cosmosResponse = await (0, prompts_1.default)({
61
- type: 'select',
62
- name: 'mode',
63
- message: 'Cosmos DB mode (affects cost):',
64
- choices: [
65
- { title: 'Free Tier (1000 RU/s free, best for first project)', value: 'freetier' },
66
- { title: 'Serverless (pay-per-use, flexible)', value: 'serverless' }
67
- ],
68
- initial: 0
69
- });
70
- const vnetResponse = await (0, prompts_1.default)({
71
- type: 'select',
72
- name: 'vnet',
73
- message: 'Network security:',
74
- choices: [
75
- { title: 'VNet Integration (recommended) - Cosmos DB via Private Endpoint', value: 'outbound' },
76
- { title: 'None - Public endpoints, simplest but least secure', value: 'none' }
77
- ],
78
- initial: 0
79
- });
207
+ async function promptAzureConfig(options) {
208
+ let swaPlan = options.swaPlan;
209
+ if (!swaPlan) {
210
+ const response = await (0, prompts_1.default)({
211
+ type: 'select',
212
+ name: 'plan',
213
+ message: 'Azure Static Web Apps plan:',
214
+ choices: [
215
+ { title: 'Standard (production, SLA and advanced auth/networking features)', value: 'standard' },
216
+ { title: 'Free (personal projects, no SLA and lower quotas)', value: 'free' }
217
+ ],
218
+ initial: 0
219
+ });
220
+ swaPlan = response.plan || 'standard';
221
+ }
222
+ let cosmosDbMode = options.cosmosDbMode;
223
+ if (!cosmosDbMode) {
224
+ const response = await (0, prompts_1.default)({
225
+ type: 'select',
226
+ name: 'mode',
227
+ message: 'Cosmos DB mode (affects cost):',
228
+ choices: [
229
+ { title: 'Free Tier (1000 RU/s free, best for first project)', value: 'freetier' },
230
+ { title: 'Serverless (pay-per-use, flexible)', value: 'serverless' }
231
+ ],
232
+ initial: 0
233
+ });
234
+ cosmosDbMode = response.mode || 'freetier';
235
+ }
236
+ let vnetOption = options.vnet;
237
+ if (!vnetOption) {
238
+ const response = await (0, prompts_1.default)({
239
+ type: 'select',
240
+ name: 'vnet',
241
+ message: 'Network security:',
242
+ choices: [
243
+ { title: 'VNet Integration (recommended) - Cosmos DB via Private Endpoint', value: 'outbound' },
244
+ { title: 'None - Public endpoints, simplest but least secure', value: 'none' }
245
+ ],
246
+ initial: 0
247
+ });
248
+ vnetOption = response.vnet || 'outbound';
249
+ }
80
250
  return {
81
- cosmosDbMode: cosmosResponse.mode || 'freetier',
82
- vnetOption: vnetResponse.vnet || 'outbound'
251
+ cosmosDbMode: cosmosDbMode || 'freetier',
252
+ vnetOption: vnetOption || 'outbound',
253
+ swaPlan: swaPlan || 'standard'
83
254
  };
84
255
  }
85
256
  const VALID_CICD = ['github', 'azure', 'skip'];
257
+ const VALID_BACKEND_LANGUAGE = ['typescript', 'csharp', 'python'];
86
258
  const VALID_COSMOS_DB_MODE = ['freetier', 'serverless'];
87
259
  const VALID_VNET = ['none', 'outbound'];
260
+ const VALID_SWA_PLAN = ['free', 'standard'];
88
261
  function validateInitFlags(options) {
89
262
  if (options.cicd && !VALID_CICD.includes(options.cicd)) {
90
263
  console.error(`❌ Invalid --cicd value: "${options.cicd}". Must be: ${VALID_CICD.join(', ')}`);
91
264
  process.exit(1);
92
265
  }
266
+ if (options.backendLanguage && !VALID_BACKEND_LANGUAGE.includes(options.backendLanguage)) {
267
+ console.error(`❌ Invalid --backend-language value: "${options.backendLanguage}". Must be: ${VALID_BACKEND_LANGUAGE.join(', ')}`);
268
+ process.exit(1);
269
+ }
93
270
  if (options.cosmosDbMode && !VALID_COSMOS_DB_MODE.includes(options.cosmosDbMode)) {
94
271
  console.error(`❌ Invalid --cosmos-db-mode value: "${options.cosmosDbMode}". Must be: ${VALID_COSMOS_DB_MODE.join(', ')}`);
95
272
  process.exit(1);
@@ -98,6 +275,10 @@ function validateInitFlags(options) {
98
275
  console.error(`❌ Invalid --vnet value: "${options.vnet}". Must be: ${VALID_VNET.join(', ')}`);
99
276
  process.exit(1);
100
277
  }
278
+ if (options.swaPlan && !VALID_SWA_PLAN.includes(options.swaPlan)) {
279
+ console.error(`❌ Invalid --swa-plan value: "${options.swaPlan}". Must be: ${VALID_SWA_PLAN.join(', ')}`);
280
+ process.exit(1);
281
+ }
101
282
  }
102
283
  async function initCommand(options) {
103
284
  // Validate flag values before doing anything
@@ -115,26 +296,46 @@ async function initCommand(options) {
115
296
  console.error(`❌ Directory "${options.name}" already exists.`);
116
297
  process.exit(1);
117
298
  }
299
+ // 非対話環境でプロンプトに入るとハングするため、不足フラグを明示エラーで案内する
300
+ const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true && !process.env.CI;
301
+ if (!interactive) {
302
+ const missing = [];
303
+ if (!options.cicd)
304
+ missing.push('--cicd <github|azure|skip>');
305
+ if (!options.backendLanguage)
306
+ missing.push('--backend-language <typescript|csharp|python>');
307
+ if (!options.cosmosDbMode)
308
+ missing.push('--cosmos-db-mode <freetier|serverless>');
309
+ if (!options.vnet)
310
+ missing.push('--vnet <outbound|none>');
311
+ if (!options.swaPlan)
312
+ missing.push('--swa-plan <free|standard>');
313
+ if (missing.length > 0) {
314
+ console.error(`❌ Non-interactive environment detected. Pass all configuration flags to run init without prompts:\n` +
315
+ missing.map((flag) => ` ${flag}`).join('\n'));
316
+ process.exit(1);
317
+ }
318
+ }
118
319
  // Use flag values if provided, otherwise prompt interactively
119
320
  const cicdProvider = options.cicd || await promptCiCd();
120
- const azureConfig = (options.cosmosDbMode && options.vnet)
121
- ? { cosmosDbMode: options.cosmosDbMode, vnetOption: options.vnet }
122
- : await promptAzureConfig();
321
+ const backendLanguage = options.backendLanguage || await promptBackendLanguage();
322
+ const azureConfig = await promptAzureConfig(options);
123
323
  // Create Next.js project with create-next-app
124
324
  await createNextJsProject(options.name, pm);
125
325
  // Upgrade Next.js to specified version (or latest) to avoid cached old versions
126
326
  await upgradeNextJs(projectDir, options.nextVersion || 'latest', pm);
127
327
  // Add SwallowKit specific files
128
- await addSwallowKitFiles(projectDir, options, cicdProvider, azureConfig, pm);
328
+ await addSwallowKitFiles(projectDir, options, cicdProvider, azureConfig, pm, backendLanguage);
129
329
  // Create infrastructure files (Bicep)
130
- await createInfrastructure(projectDir, options.name, azureConfig);
330
+ await createInfrastructure(projectDir, options.name, azureConfig, backendLanguage);
131
331
  // Create CI/CD files based on choice
132
332
  if (cicdProvider === 'github') {
133
- await createGitHubActionsWorkflows(projectDir, azureConfig, pm);
333
+ await createGitHubActionsWorkflows(projectDir, azureConfig, pm, backendLanguage);
134
334
  }
135
335
  else if (cicdProvider === 'azure') {
136
- await createAzurePipelines(projectDir, pm);
336
+ await createAzurePipelines(projectDir, pm, backendLanguage);
137
337
  }
338
+ await (0, manifest_1.syncProjectManifest)(projectDir);
138
339
  // Initialize Git repository and create initial commit
139
340
  try {
140
341
  // Try git init with -b main (Git 2.28+), fallback to git init
@@ -179,7 +380,7 @@ async function initCommand(options) {
179
380
  console.log("\n📝 Next steps:");
180
381
  console.log(` cd ${options.name}`);
181
382
  console.log(` ${pmCmd.dlx} swallowkit create-model <name> # Create your first model`);
182
- console.log(` ${pmCmd.dlx} swallowkit scaffold lib/models/<name>.ts # Generate CRUD code`);
383
+ console.log(` ${pmCmd.dlx} swallowkit scaffold shared/models/<name>.ts # Generate CRUD code`);
183
384
  console.log(` ${pmCmd.dlx} swallowkit dev # Start development servers`);
184
385
  console.log("\n🚀 Deploy to Azure:");
185
386
  console.log(` ${pmCmd.dlx} swallowkit provision --resource-group <name>`);
@@ -238,92 +439,416 @@ async function createNextJsProject(projectName, pm) {
238
439
  });
239
440
  }
240
441
  async function upgradeNextJs(projectDir, version, pm) {
241
- return new Promise((resolve, reject) => {
242
- console.log(`\n📦 Installing Next.js ${version} (to ensure latest security patches)...\n`);
243
- // pnpm: pnpm add next@... ; npm: npm install next@...
244
- const args = pm === 'pnpm'
245
- ? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact']
246
- : ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact'];
247
- const child = (0, child_process_1.spawn)(pm, args, {
248
- cwd: projectDir,
249
- stdio: 'inherit',
250
- shell: true,
442
+ console.log(`\n📦 Installing Next.js ${version} (to ensure latest security patches)...\n`);
443
+ // pnpm: pnpm add next@... ; npm: npm install next@...
444
+ const args = pm === 'pnpm'
445
+ ? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact']
446
+ : ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact'];
447
+ await runPackageManagerCommand(pm, args, projectDir, `${pm} add next@${version}`);
448
+ console.log(`\n✅ Next.js ${version} installed\n`);
449
+ }
450
+ async function installDependencies(projectDir, pm = 'pnpm') {
451
+ console.log('\n📦 Installing dependencies...\n');
452
+ await runPackageManagerCommand(pm, ['install'], projectDir, `${pm} install`);
453
+ if (pm === 'pnpm') {
454
+ console.log('\n📦 Generating npm lockfile for CI/CD...\n');
455
+ await withNpmLockfileSafeManifests(projectDir, async () => {
456
+ await runPackageManagerCommand('npm', ['install', '--package-lock-only', '--ignore-scripts'], projectDir, 'npm install --package-lock-only');
251
457
  });
252
- child.on('close', (code) => {
253
- if (code !== 0) {
254
- reject(new Error(`${pm} add next@${version} exited with code ${code}`));
458
+ }
459
+ console.log('\n✅ Dependencies installed\n');
460
+ }
461
+ async function withNpmLockfileSafeManifests(projectDir, action) {
462
+ const packageJsonPath = path.join(projectDir, 'package.json');
463
+ const functionsPackageJsonPath = path.join(projectDir, 'functions', 'package.json');
464
+ const sharedPackageJsonPath = path.join(projectDir, 'shared', 'package.json');
465
+ const originalFiles = new Map();
466
+ const hiddenNodeModules = new Map();
467
+ function rememberOriginal(filePath) {
468
+ if (!originalFiles.has(filePath)) {
469
+ originalFiles.set(filePath, fs.readFileSync(filePath, 'utf-8'));
470
+ }
471
+ }
472
+ function readJson(filePath) {
473
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
474
+ }
475
+ function writeJson(filePath, value) {
476
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
477
+ }
478
+ const sharedPackageName = fs.existsSync(sharedPackageJsonPath)
479
+ ? readJson(sharedPackageJsonPath).name
480
+ : undefined;
481
+ function replaceSharedWorkspaceDependency(packageJson, replacement) {
482
+ let changed = false;
483
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
484
+ const dependencies = packageJson[section];
485
+ if (!dependencies)
486
+ continue;
487
+ for (const [name, version] of Object.entries(dependencies)) {
488
+ if (version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
489
+ dependencies[name] = replacement;
490
+ changed = true;
491
+ }
255
492
  }
256
- else {
257
- console.log(`\n✅ Next.js ${version} installed\n`);
258
- resolve();
493
+ }
494
+ return changed;
495
+ }
496
+ function normalizePackageJson(filePath, replacement) {
497
+ if (!fs.existsSync(filePath))
498
+ return;
499
+ const packageJson = readJson(filePath);
500
+ if (!replaceSharedWorkspaceDependency(packageJson, replacement))
501
+ return;
502
+ rememberOriginal(filePath);
503
+ writeJson(filePath, packageJson);
504
+ }
505
+ function hideNodeModules(directory) {
506
+ const nodeModulesPath = path.join(directory, 'node_modules');
507
+ if (!fs.existsSync(nodeModulesPath))
508
+ return;
509
+ const hiddenPath = path.join(directory, `.node_modules.swallowkit-npm-lockfile-${process.pid}`);
510
+ fs.renameSync(nodeModulesPath, hiddenPath);
511
+ hiddenNodeModules.set(nodeModulesPath, hiddenPath);
512
+ }
513
+ try {
514
+ normalizePackageJson(packageJsonPath, 'file:shared');
515
+ normalizePackageJson(functionsPackageJsonPath, 'file:../shared');
516
+ hideNodeModules(projectDir);
517
+ hideNodeModules(path.join(projectDir, 'shared'));
518
+ hideNodeModules(path.join(projectDir, 'functions'));
519
+ await action();
520
+ }
521
+ finally {
522
+ for (const [nodeModulesPath, hiddenPath] of hiddenNodeModules) {
523
+ if (fs.existsSync(nodeModulesPath)) {
524
+ fs.rmSync(nodeModulesPath, { recursive: true, force: true });
259
525
  }
260
- });
261
- child.on('error', (error) => {
262
- reject(error);
263
- });
264
- });
526
+ fs.renameSync(hiddenPath, nodeModulesPath);
527
+ }
528
+ for (const [filePath, content] of originalFiles) {
529
+ fs.writeFileSync(filePath, content);
530
+ }
531
+ }
265
532
  }
266
- async function installDependencies(projectDir, pm = 'pnpm') {
533
+ /**
534
+ * Run a package-manager command with stdio passed through to the user.
535
+ *
536
+ * For pnpm, additionally tee stdout/stderr to a buffer so that if the command
537
+ * fails due to `ERR_PNPM_IGNORED_BUILDS`, we can:
538
+ * 1. detect which packages were ignored,
539
+ * 2. approve them (interactively or automatically),
540
+ * 3. clean-reinstall and retry — repeating if pnpm reports further ignored
541
+ * packages (pnpm does not always list every unapproved package at once).
542
+ */
543
+ async function runPackageManagerCommand(pm, args, projectDir, label) {
544
+ let { code, output } = await spawnAndCapture(pm, args, projectDir, pm === 'pnpm');
545
+ if (code === 0)
546
+ return;
547
+ if (pm === 'pnpm') {
548
+ for (let attempt = 0; attempt < 3; attempt++) {
549
+ const ignoredBuilds = parseIgnoredBuilds(output);
550
+ if (ignoredBuilds.length === 0)
551
+ break;
552
+ const approved = await maybeApproveBuilds(projectDir, ignoredBuilds);
553
+ if (!approved)
554
+ break;
555
+ // 中断された install はパッケージ抽出が不完全なまま "Already up to date"
556
+ // と判定されることがあるため、node_modules を消して決定論的に入れ直す
557
+ removeWorkspaceNodeModules(projectDir);
558
+ const retry = await spawnAndCapture(pm, args, projectDir, true);
559
+ code = retry.code;
560
+ output = retry.output;
561
+ if (code === 0)
562
+ return;
563
+ }
564
+ if (parseIgnoredBuilds(output).length > 0) {
565
+ throw new Error(`${label} exited with code ${code}: pnpm still reports unapproved build scripts. ` +
566
+ 'Run "pnpm approve-builds" inside the project, then re-run the command.');
567
+ }
568
+ }
569
+ throw new Error(`${label} exited with code ${code}`);
570
+ }
571
+ /** workspace root と直下パッケージの node_modules を削除する(クリーン再インストール用)。 */
572
+ function removeWorkspaceNodeModules(projectDir) {
573
+ const targets = [projectDir, ...fs.readdirSync(projectDir, { withFileTypes: true })
574
+ .filter((entry) => entry.isDirectory() && entry.name !== 'node_modules' && !entry.name.startsWith('.'))
575
+ .map((entry) => path.join(projectDir, entry.name))];
576
+ for (const dir of targets) {
577
+ const nodeModules = path.join(dir, 'node_modules');
578
+ if (fs.existsSync(nodeModules)) {
579
+ fs.rmSync(nodeModules, { recursive: true, force: true });
580
+ }
581
+ }
582
+ }
583
+ /**
584
+ * Spawn a child process inheriting stdin (so interactive prompts still work)
585
+ * while either inheriting or capturing+teeing stdout/stderr.
586
+ */
587
+ function spawnAndCapture(pm, args, projectDir, capture) {
267
588
  return new Promise((resolve, reject) => {
268
- console.log('\n📦 Installing dependencies...\n');
269
- const child = (0, child_process_1.spawn)(pm, ['install'], {
589
+ const stdio = capture
590
+ ? ['inherit', 'pipe', 'pipe']
591
+ : ['inherit', 'inherit', 'inherit'];
592
+ const child = (0, child_process_1.spawn)(pm, args, {
270
593
  cwd: projectDir,
271
- stdio: 'inherit',
594
+ stdio,
272
595
  shell: true,
273
596
  });
597
+ let combined = '';
598
+ if (capture) {
599
+ child.stdout?.on('data', (chunk) => {
600
+ process.stdout.write(chunk);
601
+ combined += chunk.toString('utf8');
602
+ });
603
+ child.stderr?.on('data', (chunk) => {
604
+ process.stderr.write(chunk);
605
+ combined += chunk.toString('utf8');
606
+ });
607
+ }
608
+ child.on('close', (code) => resolve({ code: code ?? 0, output: combined }));
609
+ child.on('error', reject);
610
+ });
611
+ }
612
+ /**
613
+ * Parse pnpm's `Ignored build scripts: foo@1.0.0, bar@2.0.0` warning/error and
614
+ * return the bare package names (without versions), de-duplicated.
615
+ */
616
+ function parseIgnoredBuilds(output) {
617
+ const match = output.match(/Ignored build scripts:\s*([^\n\r]+)/i);
618
+ if (!match)
619
+ return [];
620
+ const list = match[1]
621
+ .split(',')
622
+ .map((entry) => entry.trim())
623
+ // strip version suffix: `sharp@0.34.5` -> `sharp`, `@scope/pkg@1.0.0` -> `@scope/pkg`
624
+ .map((entry) => entry.replace(/@[^@]+$/, ''))
625
+ .filter((name) => name.length > 0);
626
+ return Array.from(new Set(list));
627
+ }
628
+ /**
629
+ * Prompt the user and, if approved, run `pnpm approve-builds` followed by
630
+ * `pnpm rebuild <packages>`. Returns true if the user approved (regardless of
631
+ * which packages they actually selected inside approve-builds).
632
+ *
633
+ * In non-interactive environments (CI, coding agents, piped output) prompting
634
+ * would hang forever, so build scripts are approved automatically via
635
+ * package.json `pnpm.onlyBuiltDependencies` instead.
636
+ */
637
+ async function maybeApproveBuilds(projectDir, ignoredBuilds) {
638
+ console.log(`\n⚠️ pnpm refused to run build scripts for: ${ignoredBuilds.join(', ')}\n` +
639
+ ' These packages (e.g. sharp) need their build scripts to run correctly.\n');
640
+ const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true && !process.env.CI;
641
+ if (!interactive) {
642
+ console.log('ℹ️ Non-interactive environment detected — approving build scripts automatically\n' +
643
+ ' via "allowBuilds" in pnpm-workspace.yaml.\n');
644
+ // rebuild は不要: 呼び出し側がクリーン再インストールで build script を実行する
645
+ approveBuildsViaWorkspaceYaml(projectDir, ignoredBuilds);
646
+ return true;
647
+ }
648
+ const response = await (0, prompts_1.default)({
649
+ type: 'confirm',
650
+ name: 'approve',
651
+ message: 'Run `pnpm approve-builds` now to approve these build scripts?',
652
+ initial: true,
653
+ });
654
+ if (!response.approve) {
655
+ console.log('\nℹ️ Skipped. You can run `pnpm approve-builds` later inside the project directory.\n');
656
+ return false;
657
+ }
658
+ await runSimple('pnpm', ['approve-builds'], projectDir);
659
+ await runSimple('pnpm', ['rebuild', ...ignoredBuilds], projectDir);
660
+ console.log('\n✅ Build scripts approved and packages rebuilt\n');
661
+ return true;
662
+ }
663
+ /**
664
+ * pnpm-workspace.yaml の allowBuilds にパッケージを true で登録して build script を許可する。
665
+ * (pnpm 11+ の正準設定。失敗時に pnpm が追記する placeholder 行も true に置き換える)
666
+ */
667
+ function approveBuildsViaWorkspaceYaml(projectDir, packages) {
668
+ const workspaceYamlPath = path.join(projectDir, 'pnpm-workspace.yaml');
669
+ const existing = fs.existsSync(workspaceYamlPath)
670
+ ? fs.readFileSync(workspaceYamlPath, 'utf-8')
671
+ : '';
672
+ const yamlKey = (pkg) => (/^[A-Za-z0-9_.-]+$/.test(pkg) ? pkg : `'${pkg}'`);
673
+ const lines = existing.length > 0 ? existing.split(/\r?\n/) : [];
674
+ const keyIndex = lines.findIndex((line) => /^allowBuilds\s*:/.test(line));
675
+ if (keyIndex === -1) {
676
+ const block = `allowBuilds:\n${packages.map((pkg) => ` ${yamlKey(pkg)}: true`).join('\n')}\n`;
677
+ const base = existing.length > 0 && !existing.endsWith('\n') ? `${existing}\n` : existing;
678
+ fs.writeFileSync(workspaceYamlPath, `${base}${block}`, 'utf-8');
679
+ return;
680
+ }
681
+ // 既存マップ内の対象パッケージ(placeholder 含む)を true に更新し、無いものは追記する
682
+ let endIndex = keyIndex + 1;
683
+ const updated = new Set();
684
+ while (endIndex < lines.length && /^\s+\S/.test(lines[endIndex])) {
685
+ const entry = lines[endIndex].match(/^\s+['"]?([A-Za-z0-9@/_.-]+)['"]?\s*:/);
686
+ if (entry && packages.includes(entry[1])) {
687
+ lines[endIndex] = ` ${yamlKey(entry[1])}: true`;
688
+ updated.add(entry[1]);
689
+ }
690
+ endIndex++;
691
+ }
692
+ const additions = packages.filter((pkg) => !updated.has(pkg)).map((pkg) => ` ${yamlKey(pkg)}: true`);
693
+ lines.splice(endIndex, 0, ...additions);
694
+ fs.writeFileSync(workspaceYamlPath, lines.join('\n'), 'utf-8');
695
+ }
696
+ function runSimple(command, args, cwd) {
697
+ return new Promise((resolve, reject) => {
698
+ const child = (0, child_process_1.spawn)(command, args, { cwd, stdio: 'inherit', shell: true });
274
699
  child.on('close', (code) => {
275
- if (code !== 0) {
276
- reject(new Error(`${pm} install exited with code ${code}`));
277
- }
278
- else {
279
- console.log('\n✅ Dependencies installed\n');
700
+ if (code !== 0)
701
+ reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`));
702
+ else
280
703
  resolve();
281
- }
282
- });
283
- child.on('error', (error) => {
284
- reject(error);
285
704
  });
705
+ child.on('error', reject);
286
706
  });
287
707
  }
288
- async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig, pm) {
708
+ function injectSwallowKitNextConfig(nextConfigContent, projectName) {
709
+ return nextConfigContent.replace(/(const\s+nextConfig[:\s]*(?::\s*NextConfig\s*)?=\s*\{)(\s*\/\*[^*]*\*\/)?/, `$1\n output: 'standalone',\n agentRules: false, // SwallowKit owns the canonical root AGENTS.md contract\n transpilePackages: ['@${projectName}/shared'],\n serverExternalPackages: ['applicationinsights', 'diagnostic-channel-publishers'],$2`);
710
+ }
711
+ function buildCSharpFunctionsProgramSource() {
712
+ return `using Microsoft.Azure.Functions.Worker;
713
+ using Microsoft.Extensions.DependencyInjection;
714
+ using Microsoft.Extensions.Hosting;
715
+ using Microsoft.Azure.Functions.Worker.ApplicationInsights;
716
+
717
+ var host = new HostBuilder()
718
+ .ConfigureFunctionsWorkerDefaults()
719
+ .ConfigureServices(services =>
720
+ {
721
+ services.AddApplicationInsightsTelemetryWorkerService();
722
+ services.ConfigureFunctionsApplicationInsights();
723
+ })
724
+ .Build();
725
+
726
+ host.Run();
727
+ `;
728
+ }
729
+ function buildCSharpFunctionsProjectSource() {
730
+ return `<Project Sdk="Microsoft.NET.Sdk">
731
+ <PropertyGroup>
732
+ <TargetFramework>net10.0</TargetFramework>
733
+ <AzureFunctionsVersion>v4</AzureFunctionsVersion>
734
+ <OutputType>Exe</OutputType>
735
+ <ImplicitUsings>enable</ImplicitUsings>
736
+ <Nullable>enable</Nullable>
737
+ </PropertyGroup>
738
+ <ItemGroup>
739
+ <Compile Remove="generated\\**\\bin\\**\\*.cs;generated\\**\\obj\\**\\*.cs" />
740
+ <EmbeddedResource Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />
741
+ <None Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />
742
+ </ItemGroup>
743
+ <ItemGroup>
744
+ <PackageReference Include="Microsoft.Azure.Cosmos" Version="3.47.0" />
745
+ <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
746
+ <PackageReference Include="Azure.Identity" Version="1.17.0" />
747
+ <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" />
748
+ <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
749
+ <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" OutputItemType="Analyzer" />
750
+ <PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" />
751
+ <PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
752
+ </ItemGroup>
753
+ </Project>
754
+ `;
755
+ }
756
+ function buildSwallowKitConfigSource(backendLanguage) {
757
+ return `module.exports = {
758
+ backend: {
759
+ language: '${backendLanguage}',
760
+ },
761
+ functions: {
762
+ baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
763
+ },
764
+ deployment: {
765
+ resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
766
+ swaName: process.env.AZURE_SWA_NAME || '',
767
+ },
768
+ }
769
+ `;
770
+ }
771
+ function buildGeneratedProjectDependencies(projectName, pm = 'pnpm') {
772
+ return {
773
+ '@azure/cosmos': '^4.0.0',
774
+ 'applicationinsights': '^3.3.0',
775
+ 'zod': '^4.0.0',
776
+ [`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:shared',
777
+ };
778
+ }
779
+ function getSwallowKitPackageMetadata() {
780
+ const packageJsonPath = path.resolve(__dirname, "..", "..", "..", "package.json");
781
+ try {
782
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
783
+ return {
784
+ name: packageJson.name || "swallowkit",
785
+ version: packageJson.version || "latest",
786
+ };
787
+ }
788
+ catch {
789
+ // Fall back to the published package name when package metadata is unavailable.
790
+ }
791
+ return {
792
+ name: "swallowkit",
793
+ version: "latest",
794
+ };
795
+ }
796
+ function buildGeneratedProjectDevDependencies() {
797
+ const { name, version } = getSwallowKitPackageMetadata();
798
+ return {
799
+ [name]: version,
800
+ };
801
+ }
802
+ function buildSwallowKitMcpProjectConfigSource(pm = 'pnpm') {
803
+ const { name } = getSwallowKitPackageMetadata();
804
+ return JSON.stringify({
805
+ mcpServers: {
806
+ swallowkit: {
807
+ command: pm === 'pnpm' ? 'pnpm' : 'npx',
808
+ args: pm === 'pnpm'
809
+ ? ["dlx", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"]
810
+ : ["--yes", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"],
811
+ cwd: ".",
812
+ env: {
813
+ SWALLOWKIT_MCP_VERSION: "latest",
814
+ },
815
+ },
816
+ },
817
+ }, null, 2);
818
+ }
819
+ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig, pm, backendLanguage) {
289
820
  console.log('📦 Adding SwallowKit files...\n');
290
821
  const projectName = options.name;
291
- // 1. Update package.json to add swallowkit and @azure/cosmos dependencies
822
+ // 1. Update package.json to add runtime dependencies for generated projects
292
823
  const packageJsonPath = path.join(projectDir, 'package.json');
293
824
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
294
- // Add SwallowKit dependencies (Next.js version already upgraded by upgradeNextJs)
295
825
  // zod is in the shared workspace package, not here
296
826
  packageJson.dependencies = {
297
827
  ...packageJson.dependencies,
298
- 'swallowkit': 'latest',
299
- '@azure/cosmos': '^4.0.0',
300
- 'applicationinsights': '^3.3.0',
301
- [`@${projectName}/shared`]: '*',
828
+ ...buildGeneratedProjectDependencies(projectName, pm),
829
+ };
830
+ packageJson.devDependencies = {
831
+ ...packageJson.devDependencies,
832
+ ...buildGeneratedProjectDevDependencies(),
302
833
  };
303
834
  packageJson.scripts = {
304
835
  ...packageJson.scripts,
305
836
  'build': (0, package_manager_1.getBuildScript)(pm),
306
837
  'start': 'next start',
307
- 'functions:start': (0, package_manager_1.getFunctionsStartScript)(pm),
838
+ 'functions:start': (0, package_manager_1.getFunctionsStartScript)(pm, backendLanguage),
308
839
  };
309
- if (pm === 'pnpm') {
310
- packageJson.packageManager = 'pnpm@latest';
311
- }
312
840
  packageJson.engines = {
313
841
  node: '20.x',
314
842
  };
315
- // Workspace configuration depends on package manager
316
- const wsConfig = (0, package_manager_1.getWorkspaceConfig)(pm, ['shared', 'functions']);
317
- if (wsConfig.type === 'file') {
318
- // pnpm: workspaces are defined in pnpm-workspace.yaml
319
- delete packageJson.workspaces;
320
- }
321
- else {
322
- // npm: workspaces are defined in package.json
843
+ // Keep package.json workspaces for npm-based CI/CD even when local development uses pnpm.
844
+ const workspacePackages = usesNodeFunctionsProject(backendLanguage) ? ['shared', 'functions'] : ['shared'];
845
+ const wsConfig = (0, package_manager_1.getWorkspaceConfig)(pm, workspacePackages);
846
+ packageJson.workspaces = workspacePackages;
847
+ if (wsConfig.type !== 'file') {
323
848
  packageJson.workspaces = wsConfig.value;
324
849
  }
325
850
  fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
326
- // Create workspace config file if needed (pnpm-workspace.yaml)
851
+ // Create pnpm-workspace.yaml for local pnpm development when needed.
327
852
  if (wsConfig.type === 'file') {
328
853
  fs.writeFileSync(path.join(projectDir, wsConfig.filename), wsConfig.content);
329
854
  }
@@ -336,11 +861,9 @@ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig,
336
861
  }
337
862
  if (fs.existsSync(nextConfigPath)) {
338
863
  let nextConfigContent = fs.readFileSync(nextConfigPath, 'utf-8');
339
- // Add output: 'standalone', experimental.turbopackUseSystemTlsCerts, and serverExternalPackages
864
+ // Add output, transpiled workspace package, and server externals for standalone deployment
340
865
  if (!nextConfigContent.includes("output:") && !nextConfigContent.includes('output =')) {
341
- // Handle TypeScript config format: const nextConfig: NextConfig = {
342
- // Handle JavaScript config format: const nextConfig = {
343
- nextConfigContent = nextConfigContent.replace(/(const\s+nextConfig[:\s]*(?::\s*NextConfig\s*)?=\s*\{)(\s*\/\*[^*]*\*\/)?/, `$1\n output: 'standalone',\n transpilePackages: ['@${projectName}/shared'],\n experimental: {\n turbopackUseSystemTlsCerts: true,\n },\n serverExternalPackages: ['applicationinsights', 'diagnostic-channel-publishers'],$2`);
866
+ nextConfigContent = injectSwallowKitNextConfig(nextConfigContent, projectName);
344
867
  fs.writeFileSync(nextConfigPath, nextConfigContent);
345
868
  }
346
869
  }
@@ -360,17 +883,7 @@ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig,
360
883
  fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
361
884
  }
362
885
  // 3. Create SwallowKit config
363
- const swallowkitConfig = `/** @type {import('swallowkit').SwallowKitConfig} */
364
- module.exports = {
365
- functions: {
366
- baseUrl: process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
367
- },
368
- deployment: {
369
- resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
370
- swaName: process.env.AZURE_SWA_NAME || '',
371
- },
372
- }
373
- `;
886
+ const swallowkitConfig = buildSwallowKitConfigSource(backendLanguage);
374
887
  fs.writeFileSync(path.join(projectDir, 'swallowkit.config.js'), swallowkitConfig);
375
888
  // 4. Create shared workspace package for Zod models (Single Source of Truth)
376
889
  await createSharedPackage(projectDir, projectName);
@@ -382,7 +895,14 @@ module.exports = {
382
895
  // Create backend utility for calling Azure Functions
383
896
  const backendUtilContent = `// Get Functions base URL at runtime (not at build time)
384
897
  function getFunctionsBaseUrl(): string {
385
- return process.env.BACKEND_FUNCTIONS_BASE_URL || 'http://localhost:7071';
898
+ return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
899
+ }
900
+
901
+ function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string> {
902
+ const key = process.env.BACKEND_FUNCTIONS_KEY;
903
+ if (key) return { 'x-functions-key': key };
904
+ if (functionsBaseUrl.startsWith('http://localhost:') || functionsBaseUrl.startsWith('http://127.0.0.1:')) return {};
905
+ throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
386
906
  }
387
907
 
388
908
  /**
@@ -392,10 +912,11 @@ function getFunctionsBaseUrl(): string {
392
912
  async function request<T>(
393
913
  endpoint: string,
394
914
  method: 'GET' | 'POST' | 'PUT' | 'DELETE',
395
- body?: any,
915
+ body?: unknown,
396
916
  queryParams?: Record<string, string>
397
917
  ): Promise<T> {
398
918
  const functionsBaseUrl = getFunctionsBaseUrl();
919
+ const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
399
920
  let url = \`\${functionsBaseUrl}\${endpoint}\`;
400
921
  if (queryParams) {
401
922
  const params = new URLSearchParams(queryParams);
@@ -407,6 +928,7 @@ async function request<T>(
407
928
  method,
408
929
  headers: {
409
930
  'Content-Type': 'application/json',
931
+ ...keyHeaders,
410
932
  },
411
933
  body: body ? JSON.stringify(body) : undefined,
412
934
  });
@@ -462,14 +984,14 @@ export const api = {
462
984
  /**
463
985
  * Make a POST request
464
986
  */
465
- post: <T>(endpoint: string, body?: any): Promise<T> => {
987
+ post: <T>(endpoint: string, body?: unknown): Promise<T> => {
466
988
  return request<T>(endpoint, 'POST', body);
467
989
  },
468
990
 
469
991
  /**
470
992
  * Make a PUT request
471
993
  */
472
- put: <T>(endpoint: string, body?: any): Promise<T> => {
994
+ put: <T>(endpoint: string, body?: unknown): Promise<T> => {
473
995
  return request<T>(endpoint, 'PUT', body);
474
996
  },
475
997
 
@@ -487,7 +1009,8 @@ export const api = {
487
1009
  fs.mkdirSync(componentsDir, { recursive: true });
488
1010
  // 6. Create .env.example
489
1011
  const envExample = `# Azure Functions Backend URL
490
- FUNCTIONS_BASE_URL=http://localhost:7071
1012
+ BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071
1013
+ BACKEND_FUNCTIONS_KEY=
491
1014
 
492
1015
  # Azure Configuration
493
1016
  AZURE_RESOURCE_GROUP=your-resource-group
@@ -526,7 +1049,7 @@ export async function register() {
526
1049
  const originalConsoleError = console.error;
527
1050
  const originalConsoleWarn = console.warn;
528
1051
 
529
- console.log = function(...args: any[]) {
1052
+ console.log = function(...args: unknown[]) {
530
1053
  originalConsoleLog.apply(console, args);
531
1054
  const message = args.map(arg =>
532
1055
  typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
@@ -537,7 +1060,7 @@ export async function register() {
537
1060
  });
538
1061
  };
539
1062
 
540
- console.error = function(...args: any[]) {
1063
+ console.error = function(...args: unknown[]) {
541
1064
  originalConsoleError.apply(console, args);
542
1065
  const message = args.map(arg =>
543
1066
  typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
@@ -548,7 +1071,7 @@ export async function register() {
548
1071
  });
549
1072
  };
550
1073
 
551
- console.warn = function(...args: any[]) {
1074
+ console.warn = function(...args: unknown[]) {
552
1075
  originalConsoleWarn.apply(console, args);
553
1076
  const message = args.map(arg =>
554
1077
  typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
@@ -570,7 +1093,8 @@ export async function register() {
570
1093
  // 8. Create .env.local for local development
571
1094
  const envLocalContent = [
572
1095
  '# Azure Functions Backend URL (Local)',
573
- 'FUNCTIONS_BASE_URL=http://localhost:7071',
1096
+ 'BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071',
1097
+ 'BACKEND_FUNCTIONS_KEY=',
574
1098
  ''
575
1099
  ].join('\n');
576
1100
  fs.writeFileSync(path.join(projectDir, '.env.local'), envLocalContent);
@@ -596,7 +1120,7 @@ export async function register() {
596
1120
  };
597
1121
  fs.writeFileSync(path.join(projectDir, 'staticwebapp.config.json'), JSON.stringify(swaConfig, null, 2));
598
1122
  // 14. Create Azure Functions project
599
- await createAzureFunctionsProject(projectDir, pm);
1123
+ await createAzureFunctionsProject(projectDir, pm, backendLanguage);
600
1124
  // 15. Create BFF API route to call Azure Functions
601
1125
  await createBffApiRoute(projectDir);
602
1126
  // 16. Create home page
@@ -606,9 +1130,9 @@ export async function register() {
606
1130
  await installDependencies(projectDir, pm);
607
1131
  console.log('✅ Project structure created\n');
608
1132
  // 18. Create README.md
609
- createReadme(projectDir, projectName, cicdChoice, azureConfig, pm);
1133
+ createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage);
610
1134
  // 19. Create AI agent instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
611
- createAiAgentFiles(projectDir, projectName);
1135
+ createAiAgentFiles(projectDir, projectName, backendLanguage, pm);
612
1136
  }
613
1137
  async function createSharedPackage(projectDir, projectName) {
614
1138
  console.log('📦 Creating shared workspace package for Zod models...\n');
@@ -617,6 +1141,7 @@ async function createSharedPackage(projectDir, projectName) {
617
1141
  fs.mkdirSync(modelsDir, { recursive: true });
618
1142
  // shared/package.json
619
1143
  const sharedPackageJson = {
1144
+ private: true,
620
1145
  name: `@${projectName}/shared`,
621
1146
  version: '1.0.0',
622
1147
  description: 'Shared Zod models — Single Source of Truth for validation schemas',
@@ -635,61 +1160,127 @@ async function createSharedPackage(projectDir, projectName) {
635
1160
  };
636
1161
  fs.writeFileSync(path.join(sharedDir, 'package.json'), JSON.stringify(sharedPackageJson, null, 2));
637
1162
  // shared/tsconfig.json
638
- const sharedTsConfig = {
639
- compilerOptions: {
640
- target: 'ES2020',
641
- module: 'commonjs',
642
- moduleResolution: 'node',
643
- lib: ['ES2020'],
644
- outDir: 'dist',
645
- rootDir: '.',
646
- declaration: true,
647
- declarationMap: true,
648
- sourceMap: true,
649
- strict: true,
650
- esModuleInterop: true,
651
- skipLibCheck: true,
652
- forceConsistentCasingInFileNames: true,
653
- },
654
- include: ['index.ts', 'models/**/*'],
655
- exclude: ['node_modules', 'dist'],
656
- };
657
- fs.writeFileSync(path.join(sharedDir, 'tsconfig.json'), JSON.stringify(sharedTsConfig, null, 2));
1163
+ fs.writeFileSync(path.join(sharedDir, 'tsconfig.json'), JSON.stringify(buildSharedTsConfig(), null, 2));
658
1164
  // shared/index.ts (empty re-export file, scaffold will add entries)
659
1165
  fs.writeFileSync(path.join(sharedDir, 'index.ts'), `// Shared Zod models — auto-managed by SwallowKit scaffold command\n// Do not edit the export list below manually\n`);
660
1166
  // shared/.gitignore
661
1167
  fs.writeFileSync(path.join(sharedDir, '.gitignore'), `node_modules\ndist\n`);
662
1168
  console.log('✅ Shared package created\n');
663
1169
  }
664
- async function createAzureFunctionsProject(projectDir, pm = 'pnpm') {
665
- console.log('📦 Creating Azure Functions project...\n');
1170
+ async function createAzureFunctionsProject(projectDir, pm = 'pnpm', backendLanguage = 'typescript') {
1171
+ console.log(`📦 Creating Azure Functions project (${getBackendLanguageLabel(backendLanguage)})...\n`);
666
1172
  const functionsDir = path.join(projectDir, 'functions');
667
1173
  fs.mkdirSync(functionsDir, { recursive: true });
668
- // Create functions package.json
669
- const functionsPackageJson = {
670
- name: 'functions',
671
- version: '1.0.0',
672
- description: 'Azure Functions backend',
673
- main: 'dist/*.js',
674
- scripts: {
675
- start: 'func start',
676
- build: 'tsc',
677
- prestart: (0, package_manager_1.getFunctionsPrestart)(pm)
678
- },
679
- dependencies: {
680
- '@azure/functions': '~4.5.0',
681
- '@azure/cosmos': '^4.0.0',
682
- '@azure/identity': '^4.0.0',
683
- 'zod': '>=3.25.0',
684
- [`@${path.basename(projectDir)}/shared`]: '*',
685
- },
686
- devDependencies: {
687
- '@types/node': '^20.0.0',
688
- 'typescript': '^5.0.0'
689
- }
1174
+ const projectName = path.basename(projectDir);
1175
+ const databaseName = `${projectName.charAt(0).toUpperCase() + projectName.slice(1)}Database`;
1176
+ createFunctionsHostFiles(functionsDir, databaseName, backendLanguage);
1177
+ if (backendLanguage === 'typescript') {
1178
+ createTypeScriptFunctionsProject(projectDir, functionsDir, pm);
1179
+ }
1180
+ else if (backendLanguage === 'csharp') {
1181
+ createCSharpFunctionsProject(projectDir, functionsDir);
1182
+ }
1183
+ else {
1184
+ createPythonFunctionsProject(projectDir, functionsDir);
1185
+ }
1186
+ console.log('✅ Azure Functions project created\n');
1187
+ }
1188
+ function createFunctionsHostFiles(functionsDir, databaseName, backendLanguage) {
1189
+ const hostJson = {
1190
+ version: '2.0',
1191
+ logging: {
1192
+ applicationInsights: {
1193
+ samplingSettings: {
1194
+ isEnabled: true,
1195
+ maxTelemetryItemsPerSecond: 20,
1196
+ },
1197
+ },
1198
+ },
1199
+ extensionBundle: {
1200
+ id: 'Microsoft.Azure.Functions.ExtensionBundle',
1201
+ version: '[4.0.0, 4.10.0)',
1202
+ },
1203
+ };
1204
+ fs.writeFileSync(path.join(functionsDir, 'host.json'), JSON.stringify(hostJson, null, 2));
1205
+ const localSettings = {
1206
+ IsEncrypted: false,
1207
+ Values: {
1208
+ AzureWebJobsStorage: '',
1209
+ FUNCTIONS_WORKER_RUNTIME: getFunctionsWorkerRuntime(backendLanguage),
1210
+ AzureWebJobsFeatureFlags: 'EnableWorkerIndexing',
1211
+ CosmosDBConnection: 'AccountEndpoint=http://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==',
1212
+ COSMOS_DB_DATABASE_NAME: databaseName,
1213
+ NODE_TLS_REJECT_UNAUTHORIZED: '0',
1214
+ },
1215
+ };
1216
+ fs.writeFileSync(path.join(functionsDir, 'local.settings.json'), JSON.stringify(localSettings, null, 2));
1217
+ const gitignoreLines = [
1218
+ 'local.settings.json',
1219
+ '*.log',
1220
+ '.vscode',
1221
+ '.DS_Store',
1222
+ ];
1223
+ if (backendLanguage === 'typescript') {
1224
+ gitignoreLines.unshift('node_modules', 'dist');
1225
+ fs.writeFileSync(path.join(functionsDir, '.funcignore'), `node_modules
1226
+ .git
1227
+ .vscode
1228
+ local.settings.json
1229
+ test
1230
+ tsconfig.json
1231
+ *.ts
1232
+ !dist/**/*.js
1233
+ `);
1234
+ }
1235
+ else if (backendLanguage === 'python') {
1236
+ gitignoreLines.unshift('.venv', '.codegen-venv', '__pycache__', '.python_packages');
1237
+ fs.writeFileSync(path.join(functionsDir, '.funcignore'), `.venv
1238
+ .codegen-venv
1239
+ __pycache__
1240
+ .pytest_cache
1241
+ .mypy_cache
1242
+ .ruff_cache
1243
+ local.settings.json
1244
+ tests
1245
+ `);
1246
+ }
1247
+ else {
1248
+ gitignoreLines.unshift('bin', 'obj');
1249
+ fs.writeFileSync(path.join(functionsDir, '.funcignore'), `bin
1250
+ obj
1251
+ local.settings.json
1252
+ tests
1253
+ `);
1254
+ }
1255
+ fs.writeFileSync(path.join(functionsDir, '.gitignore'), `${gitignoreLines.join('\n')}\n`);
1256
+ }
1257
+ function buildTypeScriptFunctionsPackageJson(projectName, pm = 'pnpm') {
1258
+ return {
1259
+ name: 'functions',
1260
+ version: '1.0.0',
1261
+ description: 'Azure Functions backend',
1262
+ main: 'dist/*.js',
1263
+ scripts: {
1264
+ start: 'func start',
1265
+ build: 'tsc',
1266
+ prestart: (0, package_manager_1.getFunctionsPrestart)(pm),
1267
+ },
1268
+ dependencies: {
1269
+ '@azure/functions': '~4.5.0',
1270
+ '@azure/cosmos': '^4.0.0',
1271
+ '@azure/identity': '^4.0.0',
1272
+ zod: '>=3.25.0',
1273
+ [`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:../shared',
1274
+ },
1275
+ devDependencies: {
1276
+ '@types/node': '^20.0.0',
1277
+ typescript: '^5.0.0',
1278
+ },
690
1279
  };
1280
+ }
1281
+ function createTypeScriptFunctionsProject(projectDir, functionsDir, pm) {
1282
+ const functionsPackageJson = buildTypeScriptFunctionsPackageJson(path.basename(projectDir), pm);
691
1283
  fs.writeFileSync(path.join(functionsDir, 'package.json'), JSON.stringify(functionsPackageJson, null, 2));
692
- // Create functions tsconfig.json
693
1284
  const sharedPkgName = `@${path.basename(projectDir)}/shared`;
694
1285
  const functionsTsConfig = {
695
1286
  compilerOptions: {
@@ -709,69 +1300,14 @@ async function createAzureFunctionsProject(projectDir, pm = 'pnpm') {
709
1300
  },
710
1301
  },
711
1302
  include: ['src/**/*'],
712
- exclude: ['node_modules', 'dist']
1303
+ exclude: ['node_modules', 'dist'],
713
1304
  };
714
1305
  fs.writeFileSync(path.join(functionsDir, 'tsconfig.json'), JSON.stringify(functionsTsConfig, null, 2));
715
- // Create host.json
716
- const hostJson = {
717
- version: '2.0',
718
- logging: {
719
- applicationInsights: {
720
- samplingSettings: {
721
- isEnabled: true,
722
- maxTelemetryItemsPerSecond: 20
723
- }
724
- }
725
- },
726
- extensionBundle: {
727
- id: 'Microsoft.Azure.Functions.ExtensionBundle',
728
- version: '[4.0.0, 4.10.0)'
729
- }
730
- };
731
- fs.writeFileSync(path.join(functionsDir, 'host.json'), JSON.stringify(hostJson, null, 2));
732
- // Create .funcignore
733
- const funcignore = `node_modules
734
- .git
735
- .vscode
736
- local.settings.json
737
- test
738
- tsconfig.json
739
- *.ts
740
- !dist/**/*.js
741
- `;
742
- fs.writeFileSync(path.join(functionsDir, '.funcignore'), funcignore);
743
- // Create .gitignore for functions directory
744
- const functionsGitignore = `node_modules
745
- dist
746
- local.settings.json
747
- *.log
748
- .vscode
749
- .DS_Store
750
- `;
751
- fs.writeFileSync(path.join(functionsDir, '.gitignore'), functionsGitignore);
752
- // Create local.settings.json
753
- const projectName = path.basename(projectDir);
754
- const databaseName = `${projectName.charAt(0).toUpperCase() + projectName.slice(1)}Database`;
755
- const localSettings = {
756
- IsEncrypted: false,
757
- Values: {
758
- AzureWebJobsStorage: '',
759
- FUNCTIONS_WORKER_RUNTIME: 'node',
760
- AzureWebJobsFeatureFlags: 'EnableWorkerIndexing',
761
- CosmosDBConnection: 'AccountEndpoint=http://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==',
762
- COSMOS_DB_DATABASE_NAME: databaseName,
763
- NODE_TLS_REJECT_UNAUTHORIZED: '0'
764
- }
765
- };
766
- fs.writeFileSync(path.join(functionsDir, 'local.settings.json'), JSON.stringify(localSettings, null, 2));
767
- // Create src directory
768
1306
  const srcDir = path.join(functionsDir, 'src');
769
1307
  fs.mkdirSync(srcDir, { recursive: true });
770
- // Create greet function directly in src
771
- const greetFunction = `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
1308
+ fs.writeFileSync(path.join(srcDir, 'greet.ts'), `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
772
1309
  import { z } from 'zod/v4';
773
1310
 
774
- // Zod schema for request validation
775
1311
  const greetRequestSchema = z.object({
776
1312
  name: z.string().min(1, 'Name is required').max(50, 'Name must be less than 50 characters'),
777
1313
  });
@@ -780,12 +1316,9 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
780
1316
  context.log('HTTP trigger function processed a request.');
781
1317
 
782
1318
  try {
783
- // Get name from query or body
784
1319
  const name = request.query.get('name') || (await request.text());
785
-
786
- // Validate with Zod
787
1320
  const result = greetRequestSchema.safeParse({ name });
788
-
1321
+
789
1322
  if (!result.success) {
790
1323
  return {
791
1324
  status: 400,
@@ -796,7 +1329,7 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
796
1329
  }
797
1330
 
798
1331
  const greeting = \`Hello, \${result.data.name}! This message is from Azure Functions.\`;
799
-
1332
+
800
1333
  return {
801
1334
  status: 200,
802
1335
  jsonBody: {
@@ -817,13 +1350,109 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
817
1350
 
818
1351
  app.http('greet', {
819
1352
  methods: ['GET', 'POST'],
820
- authLevel: 'anonymous',
1353
+ authLevel: 'function',
821
1354
  handler: greet
822
1355
  });
823
- `;
824
- fs.writeFileSync(path.join(srcDir, 'greet.ts'), greetFunction);
825
- // Dependencies are installed via workspace root install
826
- console.log('✅ Azure Functions project created\n');
1356
+ `);
1357
+ }
1358
+ function createCSharpFunctionsProject(projectDir, functionsDir) {
1359
+ const projectBaseName = path.basename(projectDir);
1360
+ const projectPascal = projectBaseName.charAt(0).toUpperCase() + projectBaseName.slice(1);
1361
+ const csprojName = `${projectPascal}.Functions.csproj`;
1362
+ fs.writeFileSync(path.join(functionsDir, csprojName), buildCSharpFunctionsProjectSource());
1363
+ fs.mkdirSync(path.join(functionsDir, '.config'), { recursive: true });
1364
+ fs.writeFileSync(path.join(functionsDir, '.config', 'dotnet-tools.json'), (0, native_schema_generator_1.buildCSharpCodegenToolManifestSource)());
1365
+ fs.writeFileSync(path.join(functionsDir, 'Program.cs'), buildCSharpFunctionsProgramSource());
1366
+ const crudDir = path.join(functionsDir, 'Crud');
1367
+ fs.mkdirSync(crudDir, { recursive: true });
1368
+ fs.writeFileSync(path.join(crudDir, 'GreetFunction.cs'), `using System.Net;
1369
+ using Microsoft.Azure.Functions.Worker;
1370
+ using Microsoft.Azure.Functions.Worker.Http;
1371
+
1372
+ namespace SwallowKit.Functions;
1373
+
1374
+ public sealed class GreetFunction
1375
+ {
1376
+ [Function("greet")]
1377
+ public async Task<HttpResponseData> Run(
1378
+ [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "greet")] HttpRequestData request)
1379
+ {
1380
+ var query = request.Url.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
1381
+ var name = "SwallowKit";
1382
+ foreach (var segment in query)
1383
+ {
1384
+ var parts = segment.Split('=', 2);
1385
+ if (parts.Length == 2 && parts[0] == "name")
1386
+ {
1387
+ name = Uri.UnescapeDataString(parts[1]);
1388
+ break;
1389
+ }
1390
+ }
1391
+ var response = request.CreateResponse(HttpStatusCode.OK);
1392
+ await response.WriteAsJsonAsync(new
1393
+ {
1394
+ message = $"Hello, {name}! This message is from Azure Functions.",
1395
+ timestamp = DateTimeOffset.UtcNow.ToString("O"),
1396
+ });
1397
+ return response;
1398
+ }
1399
+ }
1400
+ `);
1401
+ }
1402
+ function createPythonFunctionsProject(projectDir, functionsDir) {
1403
+ fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
1404
+ ensureProjectGitignoreEntry(projectDir, '.uv');
1405
+ fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
1406
+ azure-cosmos>=4.9.0
1407
+ azure-identity>=1.19.0
1408
+ `);
1409
+ fs.writeFileSync(path.join(functionsDir, 'requirements.codegen.txt'), (0, native_schema_generator_1.buildPythonCodegenRequirementsSource)());
1410
+ const blueprintsDir = path.join(functionsDir, 'blueprints');
1411
+ fs.mkdirSync(blueprintsDir, { recursive: true });
1412
+ fs.writeFileSync(path.join(blueprintsDir, '__init__.py'), '');
1413
+ fs.writeFileSync(path.join(blueprintsDir, 'greet.py'), `import json
1414
+ from datetime import datetime, timezone
1415
+
1416
+ import azure.functions as func
1417
+
1418
+ bp = func.Blueprint()
1419
+
1420
+
1421
+ @bp.route(route="greet", methods=["GET", "POST"])
1422
+ def greet(req: func.HttpRequest) -> func.HttpResponse:
1423
+ name = req.params.get("name") or "SwallowKit"
1424
+ payload = {
1425
+ "message": f"Hello, {name}! This message is from Azure Functions.",
1426
+ "timestamp": datetime.now(timezone.utc).isoformat(),
1427
+ }
1428
+ return func.HttpResponse(
1429
+ body=json.dumps(payload, ensure_ascii=False),
1430
+ status_code=200,
1431
+ mimetype="application/json",
1432
+ )
1433
+ `);
1434
+ fs.writeFileSync(path.join(functionsDir, 'function_app.py'), `import azure.functions as func
1435
+
1436
+ from blueprints.greet import bp as greet_bp
1437
+
1438
+ app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
1439
+
1440
+ app.register_blueprint(greet_bp)
1441
+ # SwallowKit scaffold registrations
1442
+ `);
1443
+ }
1444
+ function ensureProjectGitignoreEntry(projectDir, entry) {
1445
+ const gitignorePath = path.join(projectDir, '.gitignore');
1446
+ if (!fs.existsSync(gitignorePath)) {
1447
+ return;
1448
+ }
1449
+ const current = fs.readFileSync(gitignorePath, 'utf8');
1450
+ const lines = current.split(/\r?\n/);
1451
+ if (lines.includes(entry)) {
1452
+ return;
1453
+ }
1454
+ const normalized = current.endsWith('\n') ? current : `${current}\n`;
1455
+ fs.writeFileSync(gitignorePath, `${normalized}${entry}\n`);
827
1456
  }
828
1457
  async function createBffApiRoute(projectDir) {
829
1458
  console.log('📦 Creating BFF API route...\n');
@@ -874,17 +1503,17 @@ export async function POST(request: NextRequest) {
874
1503
  }
875
1504
  `;
876
1505
  fs.writeFileSync(path.join(apiDir, 'route.ts'), apiRoute);
877
- // Update .env.example to include FUNCTIONS_BASE_URL
1506
+ // Update .env.example to include BACKEND_FUNCTIONS_BASE_URL
878
1507
  const envExamplePath = path.join(projectDir, '.env.example');
879
1508
  let envExample = fs.readFileSync(envExamplePath, 'utf-8');
880
- if (!envExample.includes('FUNCTIONS_BASE_URL')) {
1509
+ if (!envExample.includes('BACKEND_FUNCTIONS_BASE_URL')) {
881
1510
  envExample += `\n# Azure Functions Backend URL\nBACKEND_FUNCTIONS_BASE_URL=http://localhost:7071\n`;
882
1511
  fs.writeFileSync(envExamplePath, envExample);
883
1512
  }
884
1513
  // Update .env.local
885
1514
  const envLocalPath = path.join(projectDir, '.env.local');
886
1515
  let envLocal = fs.readFileSync(envLocalPath, 'utf-8');
887
- if (!envLocal.includes('FUNCTIONS_BASE_URL')) {
1516
+ if (!envLocal.includes('BACKEND_FUNCTIONS_BASE_URL')) {
888
1517
  envLocal += `\n# Azure Functions Backend URL (Local)\nBACKEND_FUNCTIONS_BASE_URL=http://localhost:7071\n`;
889
1518
  fs.writeFileSync(envLocalPath, envLocal);
890
1519
  }
@@ -985,7 +1614,7 @@ export default function Home() {
985
1614
  Create your first model with Zod and generate CRUD operations automatically.
986
1615
  </p>
987
1616
  <code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
988
- ${pmCmd.dlx} swallowkit scaffold lib/models/your-model.ts
1617
+ ${pmCmd.dlx} swallowkit scaffold shared/models/your-model.ts
989
1618
  </code>
990
1619
  </div>
991
1620
  </section>
@@ -1021,13 +1650,29 @@ export const scaffoldConfig = {
1021
1650
  fs.writeFileSync(path.join(scaffoldConfigDir, 'scaffold-config.ts'), scaffoldConfigContent);
1022
1651
  console.log('✅ Scaffold config created\n');
1023
1652
  }
1024
- function createReadme(projectDir, projectName, cicdChoice, azureConfig, pm) {
1653
+ function createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage) {
1025
1654
  console.log('📝 Creating README.md...\n');
1026
1655
  const pmCmd = (0, package_manager_1.getCommands)(pm);
1027
1656
  const cosmosDbModeLabel = azureConfig.cosmosDbMode === 'freetier' ? 'Free Tier (1000 RU/s)' : 'Serverless';
1028
1657
  const cicdLabel = cicdChoice === 'github' ? 'GitHub Actions' : cicdChoice === 'azure' ? 'Azure Pipelines' : 'None';
1029
1658
  const vnetLabel = azureConfig.vnetOption === 'none' ? 'None (public endpoints)' :
1030
1659
  'Outbound VNet (Cosmos DB Private Endpoint)';
1660
+ const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
1661
+ const schemaBridgeDescription = backendLanguage === 'typescript'
1662
+ ? 'Zod (shared between frontend and backend)'
1663
+ : `Zod + OpenAPI export (Zod in shared/, native-generated ${backendLanguageLabel} schemas in functions/generated/)`;
1664
+ const functionsTree = backendLanguage === 'typescript'
1665
+ ? `│ └── src/\n│ └── greet.ts # Sample function`
1666
+ : backendLanguage === 'csharp'
1667
+ ? `│ ├── Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ # Native-generated C# schema assets`
1668
+ : `│ ├── blueprints/\n│ │ └── greet.py\n│ └── generated/ # Native-generated Python schema assets`;
1669
+ const backendScaffoldNote = backendLanguage === 'typescript'
1670
+ ? '- Azure Functions CRUD endpoints'
1671
+ : `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI export + native-generated ${backendLanguageLabel} schema assets`;
1672
+ const pythonUvPaths = (0, python_uv_1.getProjectLocalUvPaths)(projectDir);
1673
+ const pythonLocalDevNote = backendLanguage === 'python'
1674
+ ? `\n**Python local dev note**: SwallowKit uses \`uv\` for Python backends and keeps the managed Python runtime under \`${path.relative(projectDir, pythonUvPaths.pythonInstallDir)}\`. Local Azure Functions runs from \`functions/.venv\`, schema generation uses \`functions/.codegen-venv\`, and \`swallowkit dev\` bootstraps a project-local \`uv\` binary automatically when needed. Keep \`functions/requirements.txt\` and \`functions/requirements.codegen.txt\` as the dependency sources of truth.\n`
1675
+ : '';
1031
1676
  const readme = `# ${projectName}
1032
1677
 
1033
1678
  A full-stack application built with **SwallowKit** - Next.js on Azure Static Web Apps + Functions + Cosmos DB with Zod schema sharing.
@@ -1036,9 +1681,9 @@ A full-stack application built with **SwallowKit** - Next.js on Azure Static Web
1036
1681
 
1037
1682
  - **Frontend**: Next.js 15 (App Router), React, TypeScript, Tailwind CSS
1038
1683
  - **BFF (Backend for Frontend)**: Next.js API Routes
1039
- - **Backend**: Azure Functions (TypeScript)
1684
+ - **Backend**: Azure Functions (${backendLanguageLabel})
1040
1685
  - **Database**: Azure Cosmos DB
1041
- - **Schema Validation**: Zod (shared between frontend and backend)
1686
+ - **Schema Validation**: ${schemaBridgeDescription}
1042
1687
  - **Infrastructure**: Bicep (Infrastructure as Code)
1043
1688
  - **CI/CD**: ${cicdLabel}
1044
1689
 
@@ -1073,11 +1718,8 @@ ${projectName}/
1073
1718
  │ ├── api/ # BFF API routes (proxy to Functions)
1074
1719
  │ └── page.tsx # Home page
1075
1720
  ├── functions/ # Azure Functions (backend)
1076
- │ └── src/
1077
- │ ├── models/ # Data models (copied from lib/models)
1078
- │ └── hello.ts # Sample function
1721
+ ${functionsTree}
1079
1722
  ├── lib/
1080
- │ ├── models/ # Shared Zod schemas
1081
1723
  │ └── api/ # API client utilities
1082
1724
  ├── infra/ # Bicep infrastructure files
1083
1725
  │ ├── main.bicep
@@ -1095,18 +1737,18 @@ Define your data model with Zod schema:
1095
1737
  ${pmCmd.dlx} swallowkit create-model <model-name>
1096
1738
  \`\`\`
1097
1739
 
1098
- This creates a model file in \`lib/models/<model-name>.ts\`. Edit it to define your schema.
1740
+ This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to define your schema.
1099
1741
 
1100
1742
  ### 2. Generate CRUD Code
1101
1743
 
1102
1744
  Generate complete CRUD operations (Functions, API routes, UI):
1103
1745
 
1104
1746
  \`\`\`bash
1105
- ${pmCmd.dlx} swallowkit scaffold lib/models/<model-name>.ts
1747
+ ${pmCmd.dlx} swallowkit scaffold shared/models/<model-name>.ts
1106
1748
  \`\`\`
1107
1749
 
1108
1750
  This generates:
1109
- - Azure Functions CRUD endpoints
1751
+ ${backendScaffoldNote}
1110
1752
  - Next.js BFF API routes
1111
1753
  - React UI components (list, detail, create, edit)
1112
1754
  - Navigation menu integration
@@ -1123,6 +1765,7 @@ This starts:
1123
1765
  - Cosmos DB Emulator check (must be running separately)
1124
1766
 
1125
1767
  **Note**: You need to start Cosmos DB Emulator manually before running \`swallowkit dev\`.
1768
+ ${pythonLocalDevNote}
1126
1769
 
1127
1770
  ## ☁️ Deploy to Azure
1128
1771
 
@@ -1237,415 +1880,71 @@ This project was generated by SwallowKit. If you encounter any issues or have su
1237
1880
  fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
1238
1881
  console.log('✅ README.md created\n');
1239
1882
  }
1240
- function createAiAgentFiles(projectDir, projectName) {
1883
+ function createAiAgentFiles(projectDir, projectName, backendLanguage, pm) {
1241
1884
  console.log('🤖 Creating AI agent instruction files...\n');
1242
- // ── 1. AGENTS.md (Codex / generic agents) ──────────────────────────
1243
- const agentsMd = `# AGENTS.md
1244
-
1245
- This project was generated by **SwallowKit**.
1246
- All coding agents **must** follow the architecture and conventions described below.
1247
-
1248
- ## Architecture Overview
1249
-
1250
- This is a full-stack TypeScript application deployed on Azure with the following layers:
1251
-
1252
- \`\`\`
1253
- Frontend (React / Next.js App Router)
1254
- ↓ fetch('/api/{model}', ...)
1255
- BFF Layer (Next.js API Routes)
1256
- ↓ HTTP → Azure Functions
1257
- Backend (Azure Functions with Cosmos DB bindings)
1258
- ↓
1259
- Azure Cosmos DB (Document Database)
1260
- \`\`\`
1261
-
1262
- ### Project Structure
1263
-
1264
- \`\`\`
1265
- ${projectName}/
1266
- ├── app/ # Next.js App Router
1267
- │ ├── api/ # BFF API routes (proxy to Azure Functions)
1268
- │ └── {model}/ # UI pages per model (list, detail, create, edit)
1269
- ├── functions/ # Azure Functions (backend)
1270
- │ └── src/ # HTTP trigger handlers with Cosmos DB bindings
1271
- ├── shared/ # Shared workspace package
1272
- │ ├── models/ # Zod schema definitions (single source of truth)
1273
- │ └── index.ts # Re-exports all models
1274
- ├── lib/
1275
- │ └── api/ # API client utilities (backend.ts, call-function.ts)
1276
- ├── components/ # Shared React components
1277
- ├── infra/ # Bicep infrastructure-as-code files
1278
- │ ├── main.bicep
1279
- │ └── modules/
1280
- └── .github/workflows/ # CI/CD workflows (if configured)
1281
- \`\`\`
1282
-
1283
- ## Critical Design Principles
1284
-
1285
- ### 1. Next.js API Routes Are Strictly a BFF (Backend for Frontend)
1286
-
1287
- - \`app/api/\` routes exist **only** to proxy requests to Azure Functions.
1288
- - **Never** place business logic, database access, or direct Cosmos DB calls in Next.js API routes.
1289
- - The BFF layer may validate input/output with Zod schemas before forwarding to Functions.
1290
- - Use the \`callFunction\` helper (\`lib/api/call-function.ts\`) or the \`api\` client (\`lib/api/backend.ts\`) to call Azure Functions.
1291
-
1292
- Example BFF route pattern:
1293
-
1294
- \`\`\`typescript
1295
- // app/api/{model}/route.ts
1296
- import { callFunction } from '@/lib/api/call-function';
1297
- import { ModelSchema } from '@${projectName}/shared';
1298
- import { z } from 'zod/v4';
1299
-
1300
- export async function GET() {
1301
- return callFunction({
1302
- method: 'GET',
1303
- path: '/api/{model}',
1304
- responseSchema: z.array(ModelSchema),
1305
- });
1885
+ const runCmd = pm === 'pnpm' ? 'pnpm' : 'npx';
1886
+ (0, agent_instructions_1.writeAgentInstructionFiles)(projectDir, {
1887
+ projectName,
1888
+ backendLanguage,
1889
+ packageManager: pm,
1890
+ });
1891
+ console.log(' ✅ AGENTS.md (canonical project contract)');
1892
+ console.log(' ✅ CLAUDE.md (Claude Code adapter)');
1893
+ console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot adapter)');
1894
+ console.log(' ✅ .github/instructions/ (path-specific instructions)');
1895
+ fs.writeFileSync(path.join(projectDir, '.mcp.json'), buildSwallowKitMcpProjectConfigSource(pm));
1896
+ console.log(' ✅ .mcp.json (project-scoped MCP bootstrap)');
1897
+ (0, workflows_1.writeWorkflowDocs)(projectDir, runCmd);
1898
+ console.log(' ✅ .swallowkit/workflows/ (agent-agnostic autonomous-loop runbooks)');
1899
+ const skillPaths = (0, workflows_1.writeAgentSkills)(projectDir, runCmd);
1900
+ console.log(' ✅ .github/skills/ (Agent Skills — agentskills.io open standard)');
1901
+ for (const skillPath of skillPaths) {
1902
+ console.log(` - ${skillPath}`);
1903
+ }
1904
+ console.log('\n✅ AI agent files created\n');
1306
1905
  }
1307
-
1308
- export async function POST(request: NextRequest) {
1309
- const body = await request.json();
1310
- return callFunction({
1311
- method: 'POST',
1312
- path: '/api/{model}',
1313
- body,
1314
- inputSchema: ModelSchema.omit({ id: true, createdAt: true, updatedAt: true }),
1315
- responseSchema: ModelSchema,
1316
- successStatus: 201,
1317
- });
1906
+ function buildFunctionsHostKeyBicepExpression() {
1907
+ return "listKeys('${resourceId('Microsoft.Web/sites', functionsAppName)}/host/default', '2023-12-01').functionKeys.default";
1318
1908
  }
1319
- \`\`\`
1320
-
1321
- ### 2. Zod Schemas Are the Single Source of Truth
1322
-
1323
- - All data models are defined **once** as Zod schemas in \`shared/models/\`.
1324
- - TypeScript types are derived with \`z.infer<typeof Schema>\` — never define types separately.
1325
- - The same schema is used in **all three layers**: frontend (validation), BFF (input/output validation), and Azure Functions (request/response validation + Cosmos DB documents).
1326
- - The shared package (\`@${projectName}/shared\`) is consumed by both Next.js and Azure Functions as a workspace dependency.
1327
-
1328
- Model definition pattern:
1329
-
1330
- \`\`\`typescript
1331
- // shared/models/{model}.ts
1332
- import { z } from 'zod/v4';
1333
-
1334
- export const Todo = z.object({
1335
- id: z.string(),
1336
- name: z.string().min(1),
1337
- // ... your fields
1338
- createdAt: z.string().optional(),
1339
- updatedAt: z.string().optional(),
1340
- });
1341
-
1342
- export type Todo = z.infer<typeof Todo>;
1343
- export const displayName = 'Todo';
1344
- \`\`\`
1345
-
1346
- Key rules:
1347
- - Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
1348
- - \`id\`, \`createdAt\`, and \`updatedAt\` are auto-managed by the backend. Mark them as \`optional()\` in the schema.
1349
- - Always re-export models from \`shared/index.ts\`.
1350
-
1351
- ### 3. Azure Functions Own All Business Logic and Data Access
1352
-
1353
- - All CRUD operations and business logic live in \`functions/src/\`.
1354
- - Use Azure Functions Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.
1355
- - Use the Cosmos DB SDK client directly **only** for delete operations (bindings do not support delete).
1356
- - Validate all data against Zod schemas before writing to Cosmos DB.
1357
- - The backend auto-generates \`id\` (UUID), \`createdAt\`, and \`updatedAt\` — never trust client-sent values for these fields.
1358
-
1359
- Azure Functions handler pattern:
1360
-
1361
- \`\`\`typescript
1362
- // functions/src/{model}.ts
1363
- import { app } from '@azure/functions';
1364
- import { ModelSchema } from '@${projectName}/shared';
1365
-
1366
- const containerName = 'Models'; // PascalCase + 's'
1367
-
1368
- app.http('{model}-get-all', {
1369
- methods: ['GET'],
1370
- route: '{model}',
1371
- authLevel: 'anonymous',
1372
- extraInputs: [{ type: 'cosmosDB', name: 'cosmosInput', containerName, ... }],
1373
- handler: async (request, context) => {
1374
- const documents = context.extraInputs.get('cosmosInput');
1375
- const validated = z.array(ModelSchema).parse(documents);
1376
- return { status: 200, jsonBody: validated };
1377
- },
1378
- });
1379
- \`\`\`
1380
-
1381
- ## Naming Conventions
1382
-
1383
- | Item | Convention | Example |
1384
- |------|-----------|---------|
1385
- | Model schema file | \`shared/models/{kebab-case}.ts\` | \`shared/models/todo.ts\` |
1386
- | Schema/type name | PascalCase (same name for both) | \`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\` |
1387
- | Functions handler file | \`functions/src/{kebab-case}.ts\` | \`functions/src/todo.ts\` |
1388
- | Functions handler name | \`{camelCase}-{operation}\` | \`todo-get-all\`, \`todo-create\` |
1389
- | API route path | \`/api/{camelCase}\` | \`/api/todo\`, \`/api/todo/{id}\` |
1390
- | BFF route file | \`app/api/{kebab-case}/route.ts\` | \`app/api/todo/route.ts\` |
1391
- | BFF detail route | \`app/api/{kebab-case}/[id]/route.ts\` | \`app/api/todo/[id]/route.ts\` |
1392
- | UI page directory | \`app/{kebab-case}/\` | \`app/todo/page.tsx\` |
1393
- | React component | PascalCase | \`TodoForm.tsx\` |
1394
- | Cosmos DB container | PascalCase + 's' | \`Todos\` |
1395
- | Cosmos DB partition key | \`/id\` | Always \`/id\` |
1396
- | Bicep container file | \`infra/containers/{kebab-case}-container.bicep\` | \`infra/containers/todo-container.bicep\` |
1397
-
1398
- ## Adding New Models (SwallowKit CLI Skills)
1399
-
1400
- Use the SwallowKit CLI — do **not** manually create model files or CRUD boilerplate.
1401
-
1402
- ### Skill: Create a new data model
1403
-
1404
- \`\`\`bash
1405
- npx swallowkit create-model <name>
1406
- # Multiple models at once:
1407
- npx swallowkit create-model user post comment
1408
- \`\`\`
1409
-
1410
- Creates \`shared/models/<name>.ts\` with a Zod schema template including \`id\`, \`createdAt\`, \`updatedAt\`.
1411
- Edit the generated file to add your domain-specific fields, then run scaffold.
1412
-
1413
- ### Skill: Generate full CRUD from a model
1414
-
1415
- \`\`\`bash
1416
- npx swallowkit scaffold shared/models/<name>.ts
1417
- \`\`\`
1418
-
1419
- Generates:
1420
- - Azure Functions handlers (\`functions/src/<name>.ts\`)
1421
- - BFF API routes (\`app/api/<name>/route.ts\`, \`app/api/<name>/[id]/route.ts\`)
1422
- - UI pages (\`app/<name>/page.tsx\`, detail, create, edit pages)
1423
- - Cosmos DB Bicep container config (\`infra/containers/<name>-container.bicep\`)
1424
-
1425
- ### Skill: Start development servers
1426
-
1427
- \`\`\`bash
1428
- npx swallowkit dev
1429
- \`\`\`
1430
-
1431
- Runs Next.js (http://localhost:3000) and Azure Functions (http://localhost:7071) concurrently.
1432
- Checks for Cosmos DB Emulator availability.
1433
-
1434
- ### Skill: Provision Azure resources
1435
-
1436
- \`\`\`bash
1437
- npx swallowkit provision --resource-group <name> --location <region>
1438
- \`\`\`
1439
-
1440
- Deploys Bicep infrastructure: Static Web Apps, Functions, Cosmos DB, Storage, Managed Identity.
1441
-
1442
- ### Typical workflow for "add a new feature/model"
1443
-
1444
- 1. \`npx swallowkit create-model <name>\`
1445
- 2. Edit \`shared/models/<name>.ts\` — add fields
1446
- 3. \`npx swallowkit scaffold shared/models/<name>.ts\`
1447
- 4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
1448
-
1449
- ## Do NOT
1450
-
1451
- - **Do not** put business logic or database calls in \`app/api/\` routes. They are BFF only.
1452
- - **Do not** define TypeScript interfaces/types separately from Zod schemas. Always derive types with \`z.infer<>\`.
1453
- - **Do not** manually duplicate model definitions across layers. Use the shared package.
1454
- - **Do not** manually create CRUD boilerplate. Use \`swallowkit scaffold\`.
1455
- - **Do not** hardcode Cosmos DB connection strings. Use Managed Identity (\`CosmosDBConnection__accountEndpoint\`) in production and emulator settings locally.
1456
- - **Do not** change the partition key strategy. All containers use \`/id\` as the partition key.
1457
-
1458
- ## Technology Stack
1459
-
1460
- - **Frontend**: Next.js (App Router), React, TypeScript, Tailwind CSS
1461
- - **BFF**: Next.js API Routes (proxy only)
1462
- - **Backend**: Azure Functions (TypeScript, Node.js)
1463
- - **Database**: Azure Cosmos DB (NoSQL)
1464
- - **Schema**: Zod (shared across all layers via workspace package)
1465
- - **Infrastructure**: Bicep (IaC)
1466
- - **Hosting**: Azure Static Web Apps (frontend) + Azure Functions Flex Consumption (backend)
1467
- - **Auth**: Azure Managed Identity (no connection strings in production)
1468
- - **Monitoring**: Application Insights
1469
- `;
1470
- fs.writeFileSync(path.join(projectDir, 'AGENTS.md'), agentsMd);
1471
- console.log(' ✅ AGENTS.md (Codex / generic agents)');
1472
- // ── 2. CLAUDE.md (Claude Code) ─────────────────────────────────────
1473
- const claudeMd = `# CLAUDE.md
1474
-
1475
- This file is for Claude Code. Read AGENTS.md in the project root for the full architecture, conventions, and rules.
1476
-
1477
- ## Quick Reference
1478
-
1479
- - **Architecture**: Next.js (frontend) → BFF (API routes, proxy only) → Azure Functions (backend) → Cosmos DB
1480
- - **Schema**: Zod schemas in \`shared/models/\` are the single source of truth. Never define types separately.
1481
- - **BFF rule**: \`app/api/\` routes must ONLY proxy to Azure Functions via \`callFunction()\`. No business logic.
1482
- - **Backend rule**: All business logic and Cosmos DB access lives in \`functions/src/\`.
1483
-
1484
- ## SwallowKit CLI Commands
1485
-
1486
- | Task | Command |
1487
- |------|---------|
1488
- | Create model | \`npx swallowkit create-model <name>\` |
1489
- | Generate CRUD | \`npx swallowkit scaffold shared/models/<name>.ts\` |
1490
- | Dev servers | \`npx swallowkit dev\` |
1491
- | Provision Azure | \`npx swallowkit provision --resource-group <rg> --location <region>\` |
1492
-
1493
- ## Workflow: Add a new model
1494
-
1495
- 1. \`npx swallowkit create-model <name>\`
1496
- 2. Edit \`shared/models/<name>.ts\` — add your fields
1497
- 3. \`npx swallowkit scaffold shared/models/<name>.ts\`
1498
- 4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
1499
- `;
1500
- fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), claudeMd);
1501
- console.log(' ✅ CLAUDE.md (Claude Code)');
1502
- // ── 3. .github/copilot-instructions.md (GitHub Copilot) ────────────
1503
- const ghDir = path.join(projectDir, '.github');
1504
- fs.mkdirSync(ghDir, { recursive: true });
1505
- const copilotInstructions = `# Copilot Instructions
1506
-
1507
- This project was generated by **SwallowKit**. See \`AGENTS.md\` in the project root for the full specification.
1508
-
1509
- ## Architecture (3-layer)
1510
-
1511
- \`\`\`
1512
- Frontend (Next.js App Router) → BFF (Next.js API Routes) → Backend (Azure Functions) → Cosmos DB
1513
- \`\`\`
1909
+ function buildStaticWebAppConfigBicepSource() {
1910
+ return `@description('Static Web App name')
1911
+ param staticWebAppName string
1514
1912
 
1515
- ## Key Rules
1913
+ @description('Functions App default hostname for backend API calls')
1914
+ param functionsDefaultHostname string
1516
1915
 
1517
- 1. **BFF is proxy only** — \`app/api/\` routes call Azure Functions via \`callFunction()\`. No business logic, no direct DB access.
1518
- 2. **Zod = single source of truth** — Models live in \`shared/models/\`. Types are derived with \`z.infer<>\`. Never define types separately.
1519
- 3. **Backend owns data** — All CRUD, business logic, and Cosmos DB access is in \`functions/src/\`.
1520
- 4. **Use the CLI** — Run \`npx swallowkit create-model <name>\` then \`npx swallowkit scaffold shared/models/<name>.ts\` to add models. Do not create boilerplate manually.
1916
+ @description('Functions App name used to resolve its host key after deployment')
1917
+ param functionsAppName string
1521
1918
 
1522
- ## Naming
1919
+ @description('Application Insights connection string for SWA')
1920
+ param appInsightsConnectionString string
1523
1921
 
1524
- - Schema/type: PascalCase, same name for both (\`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\`)
1525
- - Files: kebab-case (\`shared/models/todo.ts\`, \`functions/src/todo.ts\`)
1526
- - Cosmos DB containers: PascalCase + 's' (\`Todos\`), partition key always \`/id\`
1922
+ resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
1923
+ name: staticWebAppName
1924
+ }
1527
1925
 
1528
- ## Managed Fields
1926
+ resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
1927
+ parent: staticWebApp
1928
+ name: 'appsettings'
1929
+ properties: {
1930
+ APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
1931
+ ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
1932
+ BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
1933
+ BACKEND_FUNCTIONS_KEY: ${buildFunctionsHostKeyBicepExpression()}
1934
+ }
1935
+ }
1529
1936
 
1530
- \`id\`, \`createdAt\`, \`updatedAt\` are auto-managed by the backend. Define them as \`optional()\` in schemas. Never trust client-sent values.
1531
- `;
1532
- fs.writeFileSync(path.join(ghDir, 'copilot-instructions.md'), copilotInstructions);
1533
- console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot)');
1534
- // ── 4. .github/instructions/*.instructions.md (Copilot layer-specific) ──
1535
- const instructionsDir = path.join(ghDir, 'instructions');
1536
- fs.mkdirSync(instructionsDir, { recursive: true });
1537
- // 4a. shared/models — Zod schema layer
1538
- const sharedModelsInstructions = `---
1539
- applyTo: "shared/models/**"
1540
- ---
1541
-
1542
- # Shared Models — Zod Schema Rules
1543
-
1544
- Files in this directory are the **single source of truth** for data models across the entire application.
1545
-
1546
- ## Rules
1547
-
1548
- - Define Zod schemas using \`zod/v4\` (\`import { z } from 'zod/v4'\`).
1549
- - Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
1550
- \`\`\`typescript
1551
- export const Todo = z.object({ ... });
1552
- export type Todo = z.infer<typeof Todo>;
1553
- \`\`\`
1554
- - Always include \`id: z.string()\`, \`createdAt: z.string().optional()\`, \`updatedAt: z.string().optional()\`. These are managed by the backend.
1555
- - Export a \`displayName\` string constant for UI display.
1556
- - Re-export every model from \`shared/index.ts\`.
1557
- - For relationships, use **nested schemas** (import and embed the related schema), not ID references.
1558
- - After editing a model, run \`npx swallowkit scaffold shared/models/<name>.ts\` to regenerate CRUD code.
1937
+ output configName string = staticWebAppConfig.name
1559
1938
  `;
1560
- fs.writeFileSync(path.join(instructionsDir, 'shared-models.instructions.md'), sharedModelsInstructions);
1561
- // 4b. app/api — BFF layer
1562
- const bffInstructions = `---
1563
- applyTo: "app/api/**"
1564
- ---
1565
-
1566
- # BFF API Routes — Rules
1567
-
1568
- Files in \`app/api/\` are the **BFF (Backend for Frontend)** layer. They exist solely to proxy requests to Azure Functions.
1569
-
1570
- ## Rules
1571
-
1572
- - **Never** put business logic, database access, or direct Cosmos DB calls here.
1573
- - Use \`callFunction()\` from \`@/lib/api/call-function\` to forward requests to Azure Functions.
1574
- - You may validate input/output with Zod schemas before forwarding.
1575
- - Import schemas from \`@${projectName}/shared\`.
1576
-
1577
- ## Pattern
1578
-
1579
- \`\`\`typescript
1580
- import { callFunction } from '@/lib/api/call-function';
1581
- import { ModelSchema } from '@${projectName}/shared';
1582
- import { z } from 'zod/v4';
1583
-
1584
- export async function GET() {
1585
- return callFunction({
1586
- method: 'GET',
1587
- path: '/api/{model}',
1588
- responseSchema: z.array(ModelSchema),
1589
- });
1590
1939
  }
1591
- \`\`\`
1592
- `;
1593
- fs.writeFileSync(path.join(instructionsDir, 'bff-routes.instructions.md'), bffInstructions);
1594
- // 4c. functions — Azure Functions backend layer
1595
- const functionsInstructions = `---
1596
- applyTo: "functions/**"
1597
- ---
1598
-
1599
- # Azure Functions — Backend Rules
1600
-
1601
- Files in \`functions/src/\` contain all business logic and data access for this application.
1602
-
1603
- ## Rules
1604
-
1605
- - Use Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.
1606
- - Use the Cosmos DB SDK client directly **only** for delete operations.
1607
- - Validate all request data against Zod schemas from \`@${projectName}/shared\` before writing.
1608
- - Auto-generate \`id\` (UUID), \`createdAt\`, and \`updatedAt\` on the backend. Never trust client-sent values.
1609
- - Container names are PascalCase + 's' (e.g., \`Todos\`). Partition key is always \`/id\`.
1610
-
1611
- ## Handler Pattern
1612
-
1613
- \`\`\`typescript
1614
- import { app } from '@azure/functions';
1615
- import { ModelSchema } from '@${projectName}/shared';
1616
-
1617
- app.http('{model}-get-all', {
1618
- methods: ['GET'],
1619
- route: '{model}',
1620
- authLevel: 'anonymous',
1621
- extraInputs: [cosmosInput],
1622
- handler: async (request, context) => {
1623
- const documents = context.extraInputs.get(cosmosInput);
1624
- const validated = z.array(ModelSchema).parse(documents);
1625
- return { status: 200, jsonBody: validated };
1626
- },
1627
- });
1628
- \`\`\`
1629
- `;
1630
- fs.writeFileSync(path.join(instructionsDir, 'azure-functions.instructions.md'), functionsInstructions);
1631
- console.log(' ✅ .github/instructions/ (Copilot layer-specific instructions)');
1632
- console.log(' - shared-models.instructions.md');
1633
- console.log(' - bff-routes.instructions.md');
1634
- console.log(' - azure-functions.instructions.md');
1635
- console.log('\n✅ AI agent files created\n');
1636
- console.log(' Supported agents:');
1637
- console.log(' - OpenAI Codex → AGENTS.md');
1638
- console.log(' - Claude Code → CLAUDE.md (+ AGENTS.md)');
1639
- console.log(' - GitHub Copilot → .github/copilot-instructions.md');
1640
- console.log(' - GitHub Copilot (edit) → .github/instructions/*.instructions.md');
1641
- console.log('');
1642
- }
1643
- async function createInfrastructure(projectDir, projectName, azureConfig) {
1940
+ async function createInfrastructure(projectDir, projectName, azureConfig, backendLanguage) {
1644
1941
  console.log('📦 Creating infrastructure files (Bicep)...\n');
1645
1942
  const infraDir = path.join(projectDir, 'infra');
1646
1943
  const modulesDir = path.join(infraDir, 'modules');
1647
1944
  fs.mkdirSync(modulesDir, { recursive: true });
1648
1945
  const enableVNet = azureConfig.vnetOption !== 'none';
1946
+ const functionsRuntime = getFunctionsRuntimeConfig(backendLanguage);
1947
+ const staticWebAppSku = getStaticWebAppSku(azureConfig.swaPlan);
1649
1948
  // main.bicep
1650
1949
  const mainBicep = `targetScope = 'resourceGroup'
1651
1950
 
@@ -1658,6 +1957,13 @@ param location string = resourceGroup().location
1658
1957
  @description('Location for Static Web App (must be explicitly provided)')
1659
1958
  param swaLocation string
1660
1959
 
1960
+ @description('Static Web Apps hosting plan')
1961
+ @allowed([
1962
+ 'Free'
1963
+ 'Standard'
1964
+ ])
1965
+ param staticWebAppSku string = '${staticWebAppSku}'
1966
+
1661
1967
  @description('Cosmos DB mode')
1662
1968
  @allowed(['freetier', 'serverless'])
1663
1969
  param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
@@ -1700,7 +2006,7 @@ module staticWebApp 'modules/staticwebapp.bicep' = {
1700
2006
  params: {
1701
2007
  name: 'swa-\${projectName}'
1702
2008
  location: swaLocation
1703
- sku: 'Standard'
2009
+ sku: staticWebAppSku
1704
2010
  appInsightsConnectionString: appInsightsSwa.outputs.connectionString
1705
2011
  }
1706
2012
  }
@@ -1741,35 +2047,30 @@ module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enabl
1741
2047
  params: {
1742
2048
  name: 'pe-cosmos-\${projectName}'
1743
2049
  location: location
1744
- cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.id : cosmosDbServerless.outputs.id
1745
- cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
1746
- subnetId: vnet.outputs.privateEndpointSubnetId
1747
- vnetId: vnet.outputs.id
2050
+ cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.id : cosmosDbServerless!.outputs.id
2051
+ cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.accountName : cosmosDbServerless!.outputs.accountName
2052
+ subnetId: vnet!.outputs.privateEndpointSubnetId
2053
+ vnetId: vnet!.outputs.id
1748
2054
  }
1749
- dependsOn: [
1750
- cosmosDbFreeTier
1751
- cosmosDbServerless
1752
- vnet
1753
- ]
1754
2055
  }
1755
2056
 
1756
2057
  // Azure Functions (Flex Consumption) - Deploy AFTER Cosmos DB
1757
2058
  module functionsFlex 'modules/functions-flex.bicep' = {
1758
2059
  name: 'functionsApp'
1759
- params: {
1760
- name: 'func-\${projectName}'
1761
- location: location
1762
- storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
1763
- appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
1764
- swaDefaultHostname: staticWebApp.outputs.defaultHostname
1765
- cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
1766
- cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
1767
- enableVNet: enableVNet
1768
- vnetSubnetId: enableVNet ? vnet.outputs.functionsSubnetId : ''
2060
+ params: {
2061
+ name: 'func-\${projectName}'
2062
+ location: location
2063
+ storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
2064
+ appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
2065
+ swaDefaultHostname: staticWebApp.outputs.defaultHostname
2066
+ cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.endpoint : cosmosDbServerless!.outputs.endpoint
2067
+ cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.databaseName : cosmosDbServerless!.outputs.databaseName
2068
+ functionsRuntimeName: '${functionsRuntime.name}'
2069
+ functionsRuntimeVersion: '${functionsRuntime.version}'
2070
+ enableVNet: enableVNet
2071
+ vnetSubnetId: enableVNet ? vnet!.outputs.functionsSubnetId : ''
1769
2072
  }
1770
2073
  dependsOn: [
1771
- cosmosDbFreeTier
1772
- cosmosDbServerless
1773
2074
  cosmosPrivateEndpoint
1774
2075
  ]
1775
2076
  }
@@ -1778,23 +2079,17 @@ module functionsFlex 'modules/functions-flex.bicep' = {
1778
2079
  module cosmosDbRoleAssignmentFreeTier 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'freetier') {
1779
2080
  name: 'cosmosDbRoleAssignment'
1780
2081
  params: {
1781
- cosmosAccountName: cosmosDbFreeTier.outputs.accountName
2082
+ cosmosAccountName: cosmosDbFreeTier!.outputs.accountName
1782
2083
  functionsPrincipalId: functionsFlex.outputs.principalId
1783
2084
  }
1784
- dependsOn: [
1785
- functionsFlex
1786
- ]
1787
2085
  }
1788
2086
 
1789
2087
  module cosmosDbRoleAssignmentServerless 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'serverless') {
1790
2088
  name: 'cosmosDbRoleAssignment'
1791
2089
  params: {
1792
- cosmosAccountName: cosmosDbServerless.outputs.accountName
2090
+ cosmosAccountName: cosmosDbServerless!.outputs.accountName
1793
2091
  functionsPrincipalId: functionsFlex.outputs.principalId
1794
2092
  }
1795
- dependsOn: [
1796
- functionsFlex
1797
- ]
1798
2093
  }
1799
2094
 
1800
2095
  // Update SWA config with Functions hostname (after Functions deployment)
@@ -1803,20 +2098,18 @@ module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
1803
2098
  params: {
1804
2099
  staticWebAppName: staticWebApp.outputs.name
1805
2100
  functionsDefaultHostname: functionsFlex.outputs.defaultHostname
2101
+ functionsAppName: functionsFlex.outputs.name
1806
2102
  appInsightsConnectionString: appInsightsSwa.outputs.connectionString
1807
2103
  }
1808
- dependsOn: [
1809
- functionsFlex
1810
- ]
1811
2104
  }
1812
2105
 
1813
2106
  output staticWebAppName string = staticWebApp.outputs.name
1814
2107
  output staticWebAppUrl string = staticWebApp.outputs.defaultHostname
1815
2108
  output functionsAppName string = functionsFlex.outputs.name
1816
2109
  output functionsAppUrl string = functionsFlex.outputs.defaultHostname
1817
- output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
1818
- output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
1819
- output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
2110
+ output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.accountName : cosmosDbServerless!.outputs.accountName
2111
+ output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.endpoint : cosmosDbServerless!.outputs.endpoint
2112
+ output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.databaseName : cosmosDbServerless!.outputs.databaseName
1820
2113
  output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
1821
2114
  output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
1822
2115
  output appInsightsSwaName string = appInsightsSwa.outputs.name
@@ -1824,7 +2117,7 @@ output appInsightsSwaConnectionString string = appInsightsSwa.outputs.connection
1824
2117
  output appInsightsFunctionsName string = appInsightsFunctions.outputs.name
1825
2118
  output appInsightsFunctionsConnectionString string = appInsightsFunctions.outputs.connectionString
1826
2119
  output vnetEnabled bool = enableVNet
1827
- output vnetName string = enableVNet ? vnet.outputs.name : ''
2120
+ output vnetName string = enableVNet ? vnet!.outputs.name : ''
1828
2121
  `;
1829
2122
  fs.writeFileSync(path.join(infraDir, 'main.bicep'), mainBicep);
1830
2123
  // main.parameters.json
@@ -1838,6 +2131,9 @@ output vnetName string = enableVNet ? vnet.outputs.name : ''
1838
2131
  "cosmosDbMode": {
1839
2132
  "value": "${azureConfig.cosmosDbMode}"
1840
2133
  },
2134
+ "staticWebAppSku": {
2135
+ "value": "${staticWebAppSku}"
2136
+ },
1841
2137
  "enableVNet": {
1842
2138
  "value": ${enableVNet}
1843
2139
  }
@@ -1892,31 +2188,7 @@ output defaultHostname string = staticWebApp.properties.defaultHostname
1892
2188
  `;
1893
2189
  fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
1894
2190
  // modules/staticwebapp-config.bicep (for updating config after Functions deployment)
1895
- const staticWebAppConfigBicep = `@description('Static Web App name')
1896
- param staticWebAppName string
1897
-
1898
- @description('Functions App default hostname for backend API calls')
1899
- param functionsDefaultHostname string
1900
-
1901
- @description('Application Insights connection string for SWA')
1902
- param appInsightsConnectionString string
1903
-
1904
- resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
1905
- name: staticWebAppName
1906
- }
1907
-
1908
- resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
1909
- parent: staticWebApp
1910
- name: 'appsettings'
1911
- properties: {
1912
- APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
1913
- ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
1914
- BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
1915
- }
1916
- }
1917
-
1918
- output configName string = staticWebAppConfig.name
1919
- `;
2191
+ const staticWebAppConfigBicep = buildStaticWebAppConfigBicepSource();
1920
2192
  fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
1921
2193
  // modules/loganalytics.bicep (Shared Log Analytics Workspace)
1922
2194
  const logAnalyticsBicep = `@description('Log Analytics workspace name')
@@ -1996,6 +2268,12 @@ param enableVNet bool = false
1996
2268
  @description('VNet subnet ID for Functions (required if enableVNet is true)')
1997
2269
  param vnetSubnetId string = ''
1998
2270
 
2271
+ @description('Functions runtime name')
2272
+ param functionsRuntimeName string
2273
+
2274
+ @description('Functions runtime version')
2275
+ param functionsRuntimeVersion string
2276
+
1999
2277
  // Storage Account for Functions
2000
2278
  resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
2001
2279
  name: storageAccountName
@@ -2066,8 +2344,8 @@ resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
2066
2344
  instanceMemoryMB: 2048
2067
2345
  }
2068
2346
  runtime: {
2069
- name: 'node'
2070
- version: '22'
2347
+ name: functionsRuntimeName
2348
+ version: functionsRuntimeVersion
2071
2349
  }
2072
2350
  }
2073
2351
  siteConfig: {
@@ -2129,63 +2407,7 @@ output principalId string = functionApp.identity.principalId
2129
2407
  `;
2130
2408
  fs.writeFileSync(path.join(modulesDir, 'functions-flex.bicep'), functionsFlexBicep);
2131
2409
  // modules/cosmosdb-freetier.bicep (Free Tier)
2132
- const cosmosDbFreeTierBicep = `@description('Cosmos DB account name')
2133
- param accountName string
2134
-
2135
- @description('Database name')
2136
- param databaseName string
2137
-
2138
- @description('Location for Cosmos DB')
2139
- param location string
2140
-
2141
- @description('Public network access')
2142
- @allowed(['Enabled', 'Disabled'])
2143
- param publicNetworkAccess string = 'Enabled'
2144
-
2145
- // Cosmos DB Account (Free Tier)
2146
- resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
2147
- name: accountName
2148
- location: location
2149
- kind: 'GlobalDocumentDB'
2150
- properties: {
2151
- databaseAccountOfferType: 'Standard'
2152
- enableAutomaticFailover: false
2153
- enableFreeTier: true
2154
- publicNetworkAccess: publicNetworkAccess
2155
- disableLocalAuth: true
2156
- consistencyPolicy: {
2157
- defaultConsistencyLevel: 'Session'
2158
- }
2159
- locations: [
2160
- {
2161
- locationName: location
2162
- failoverPriority: 0
2163
- isZoneRedundant: false
2164
- }
2165
- ]
2166
- disableKeyBasedMetadataWriteAccess: true
2167
- }
2168
- }
2169
-
2170
- // Cosmos DB Database
2171
- resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
2172
- parent: cosmosAccount
2173
- name: databaseName
2174
- properties: {
2175
- resource: {
2176
- id: databaseName
2177
- }
2178
- options: {
2179
- throughput: 1000
2180
- }
2181
- }
2182
- }
2183
-
2184
- output id string = cosmosAccount.id
2185
- output accountName string = cosmosAccount.name
2186
- output endpoint string = cosmosAccount.properties.documentEndpoint
2187
- output databaseName string = database.name
2188
- `;
2410
+ const cosmosDbFreeTierBicep = buildCosmosDbFreeTierBicepSource();
2189
2411
  fs.writeFileSync(path.join(modulesDir, 'cosmosdb-freetier.bicep'), cosmosDbFreeTierBicep);
2190
2412
  // modules/cosmosdb-serverless.bicep (Serverless)
2191
2413
  const cosmosDbServerlessBicep = `@description('Cosmos DB account name')
@@ -2275,10 +2497,11 @@ resource roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignment
2275
2497
  output roleAssignmentId string = roleAssignment.id
2276
2498
  `;
2277
2499
  fs.writeFileSync(path.join(modulesDir, 'cosmosdb-role-assignment.bicep'), cosmosDbRoleAssignmentBicep);
2278
- // VNet modules (only generate if VNet is enabled)
2279
- if (enableVNet) {
2280
- // modules/vnet.bicep
2281
- const vnetBicep = `@description('VNet name')
2500
+ // Bicep resolves module files at compile time even when their deployment condition is false,
2501
+ // so these definitions must always be generated. main.bicep still controls deployment with
2502
+ // `if (enableVNet)`.
2503
+ // modules/vnet.bicep
2504
+ const vnetBicep = `@description('VNet name')
2282
2505
  param name string
2283
2506
 
2284
2507
  @description('Location for VNet')
@@ -2324,9 +2547,9 @@ output name string = vnet.name
2324
2547
  output functionsSubnetId string = vnet.properties.subnets[0].id
2325
2548
  output privateEndpointSubnetId string = vnet.properties.subnets[1].id
2326
2549
  `;
2327
- fs.writeFileSync(path.join(modulesDir, 'vnet.bicep'), vnetBicep);
2328
- // modules/private-endpoint-cosmos.bicep
2329
- const cosmosPrivateEndpointBicep = `@description('Private endpoint name')
2550
+ fs.writeFileSync(path.join(modulesDir, 'vnet.bicep'), vnetBicep);
2551
+ // modules/private-endpoint-cosmos.bicep
2552
+ const cosmosPrivateEndpointBicep = `@description('Private endpoint name')
2330
2553
  param name string
2331
2554
 
2332
2555
  @description('Location')
@@ -2404,73 +2627,83 @@ resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneG
2404
2627
  output privateEndpointId string = privateEndpoint.id
2405
2628
  output privateDnsZoneId string = privateDnsZone.id
2406
2629
  `;
2407
- fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
2408
- console.log('✅ VNet modules created\n');
2409
- }
2630
+ fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
2631
+ console.log('✅ VNet module definitions created\n');
2410
2632
  console.log('✅ Infrastructure files created\n');
2411
2633
  }
2412
- async function createGitHubActionsWorkflows(projectDir, azureConfig, pm) {
2413
- console.log('📦 Creating GitHub Actions workflows...\n');
2634
+ function getGitHubFunctionsWorkflow(pm, backendLanguage, projectPm = pm) {
2414
2635
  const pmCmd = (0, package_manager_1.getCommands)(pm);
2415
2636
  const pnpmSetupStep = (0, package_manager_1.getCiSetupStep)(pm);
2416
- const workflowsDir = path.join(projectDir, '.github', 'workflows');
2417
- fs.mkdirSync(workflowsDir, { recursive: true });
2418
- // deploy-swa.yml
2419
- const swaWorkflow = `name: Deploy Static Web App
2637
+ const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
2638
+ const commonSetup = ` - uses: actions/checkout@v4
2639
+
2640
+ - name: Setup Node.js
2641
+ uses: actions/setup-node@v4
2642
+ with:
2643
+ node-version: '22'
2644
+ ${pnpmSetupStep ? `\n${pnpmSetupStep}\n` : ''}
2645
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
2646
+ - name: Install dependencies
2647
+ run: |
2648
+ ${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
2649
+
2650
+ - name: Build shared package
2651
+ run: |
2652
+ ${pmCmd.runFilter('shared')} build
2653
+ `;
2654
+ if (backendLanguage === 'typescript') {
2655
+ return `name: Deploy Azure Functions
2420
2656
 
2421
2657
  on:
2422
2658
  push:
2423
2659
  branches:
2424
2660
  - main
2425
2661
  paths:
2426
- - 'app/**'
2427
- - 'components/**'
2428
- - 'lib/**'
2662
+ - 'functions/**'
2429
2663
  - 'shared/**'
2430
- - 'public/**'
2431
- - 'package.json'
2432
- - 'next.config.js'
2433
- - 'next.config.ts'
2434
- workflow_dispatch:
2435
2664
  pull_request:
2436
2665
  branches:
2437
2666
  - main
2438
2667
  paths:
2439
- - 'app/**'
2440
- - 'components/**'
2441
- - 'lib/**'
2668
+ - 'functions/**'
2442
2669
  - 'shared/**'
2443
- - 'public/**'
2444
- - 'package.json'
2445
- - 'next.config.js'
2446
- - 'next.config.ts'
2670
+ workflow_dispatch:
2447
2671
 
2448
2672
  jobs:
2449
2673
  build-and-deploy:
2450
2674
  runs-on: ubuntu-latest
2451
- name: Build and Deploy Static Web App
2675
+ name: Build and Deploy Functions
2452
2676
 
2453
2677
  steps:
2454
- - uses: actions/checkout@v4
2455
- with:
2456
- submodules: true
2678
+ ${commonSetup} - name: Build Functions
2679
+ run: |
2680
+ ${pmCmd.runFilter('functions')} build
2457
2681
 
2458
- - name: Deploy to Azure Static Web Apps
2682
+ - name: Prepare functions for deployment
2683
+ run: |
2684
+ SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
2685
+ mkdir -p /tmp/fn-deps
2686
+ node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
2687
+ cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
2688
+ rm -rf ./functions/node_modules
2689
+ mv /tmp/fn-deps/node_modules ./functions/node_modules
2690
+ SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
2691
+ mkdir -p "$SHARED_DEST"
2692
+ cp -r ./shared/dist "$SHARED_DEST/dist"
2693
+ cp ./shared/package.json "$SHARED_DEST/package.json"
2694
+
2695
+ - name: Deploy to Azure Functions
2459
2696
  if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
2460
- uses: Azure/static-web-apps-deploy@v1
2697
+ uses: Azure/functions-action@v1
2461
2698
  with:
2462
- azure_static_web_apps_api_token: \${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
2463
- repo_token: \${{ secrets.GITHUB_TOKEN }}
2464
- action: 'upload'
2465
- app_location: '/'
2466
- api_location: ''
2467
- output_location: ''
2468
- env:
2469
- NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
2699
+ app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
2700
+ package: './functions'
2701
+ publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
2702
+ sku: flexconsumption
2470
2703
  `;
2471
- fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
2472
- // deploy-functions.yml
2473
- const functionsWorkflow = `name: Deploy Azure Functions
2704
+ }
2705
+ if (backendLanguage === 'csharp') {
2706
+ return `name: Deploy Azure Functions
2474
2707
 
2475
2708
  on:
2476
2709
  push:
@@ -2493,37 +2726,57 @@ jobs:
2493
2726
  name: Build and Deploy Functions
2494
2727
 
2495
2728
  steps:
2496
- - uses: actions/checkout@v4
2497
-
2498
- - name: Setup Node.js
2499
- uses: actions/setup-node@v4
2729
+ ${commonSetup} - name: Setup .NET
2730
+ uses: actions/setup-dotnet@v4
2500
2731
  with:
2501
- node-version: '22'
2502
- ${pnpmSetupStep ? `\n${pnpmSetupStep}\n` : ''}
2503
- - name: Install dependencies
2504
- run: |
2505
- ${pmCmd.ci}
2506
-
2507
- - name: Build shared package
2508
- run: |
2509
- ${pmCmd.runFilter('shared')} build
2510
-
2511
- - name: Build Functions
2732
+ dotnet-version: '10.0.x'
2733
+
2734
+ - name: Publish Functions
2512
2735
  run: |
2513
- ${pmCmd.runFilter('functions')} build
2736
+ dotnet publish ./functions -c Release -o ./functions/publish
2514
2737
 
2515
- - name: Prepare functions for deployment
2738
+ - name: Deploy to Azure Functions
2739
+ if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
2740
+ uses: Azure/functions-action@v1
2741
+ with:
2742
+ app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
2743
+ package: './functions/publish'
2744
+ publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
2745
+ sku: flexconsumption
2746
+ `;
2747
+ }
2748
+ return `name: Deploy Azure Functions
2749
+
2750
+ on:
2751
+ push:
2752
+ branches:
2753
+ - main
2754
+ paths:
2755
+ - 'functions/**'
2756
+ - 'shared/**'
2757
+ pull_request:
2758
+ branches:
2759
+ - main
2760
+ paths:
2761
+ - 'functions/**'
2762
+ - 'shared/**'
2763
+ workflow_dispatch:
2764
+
2765
+ jobs:
2766
+ build-and-deploy:
2767
+ runs-on: ubuntu-latest
2768
+ name: Build and Deploy Functions
2769
+
2770
+ steps:
2771
+ ${commonSetup} - name: Setup Python
2772
+ uses: actions/setup-python@v5
2773
+ with:
2774
+ python-version: '3.11'
2775
+
2776
+ - name: Install Functions dependencies
2516
2777
  run: |
2517
- SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
2518
- mkdir -p /tmp/fn-deps
2519
- node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
2520
- cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
2521
- rm -rf ./functions/node_modules
2522
- mv /tmp/fn-deps/node_modules ./functions/node_modules
2523
- SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
2524
- mkdir -p "$SHARED_DEST"
2525
- cp -r ./shared/dist "$SHARED_DEST/dist"
2526
- cp ./shared/package.json "$SHARED_DEST/package.json"
2778
+ python -m pip install --upgrade pip
2779
+ python -m pip install -r ./functions/requirements.txt --target "./functions/.python_packages/lib/site-packages"
2527
2780
 
2528
2781
  - name: Deploy to Azure Functions
2529
2782
  if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
@@ -2534,29 +2787,34 @@ ${pnpmSetupStep ? `\n${pnpmSetupStep}\n` : ''}
2534
2787
  publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
2535
2788
  sku: flexconsumption
2536
2789
  `;
2537
- fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
2538
- console.log('✅ GitHub Actions workflows created\n');
2539
2790
  }
2540
- async function createAzurePipelines(projectDir, pm) {
2541
- console.log('📦 Creating Azure Pipelines...\n');
2791
+ function getAzureFunctionsPipeline(pm, backendLanguage, projectPm = pm) {
2542
2792
  const pmCmd = (0, package_manager_1.getCommands)(pm);
2543
2793
  const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
2544
- const pipelinesDir = path.join(projectDir, 'pipelines');
2545
- fs.mkdirSync(pipelinesDir, { recursive: true });
2546
- // swa.yml
2547
- const swaPipeline = `trigger:
2794
+ const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
2795
+ const commonSetup = ` - task: NodeTool@0
2796
+ inputs:
2797
+ versionSpec: '22.x'
2798
+ displayName: 'Install Node.js'
2799
+ ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
2800
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
2801
+ - script: |
2802
+ ${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
2803
+ displayName: 'Install workspace dependencies'
2804
+
2805
+ - script: |
2806
+ ${pmCmd.runFilter('shared')} build
2807
+ displayName: 'Build shared package'
2808
+ `;
2809
+ if (backendLanguage === 'typescript') {
2810
+ return `trigger:
2548
2811
  branches:
2549
2812
  include:
2550
2813
  - main
2551
2814
  paths:
2552
2815
  include:
2553
- - app/**
2554
- - components/**
2555
- - lib/**
2816
+ - functions/**
2556
2817
  - shared/**
2557
- - public/**
2558
- - package.json
2559
- - next.config.js
2560
2818
 
2561
2819
  pr:
2562
2820
  branches:
@@ -2564,13 +2822,8 @@ pr:
2564
2822
  - main
2565
2823
  paths:
2566
2824
  include:
2567
- - app/**
2568
- - components/**
2569
- - lib/**
2825
+ - functions/**
2570
2826
  - shared/**
2571
- - public/**
2572
- - package.json
2573
- - next.config.js
2574
2827
 
2575
2828
  pool:
2576
2829
  vmImage: 'ubuntu-latest'
@@ -2579,33 +2832,51 @@ variables:
2579
2832
  - group: azure-deployment
2580
2833
 
2581
2834
  steps:
2582
- - task: NodeTool@0
2583
- inputs:
2584
- versionSpec: '22.x'
2585
- displayName: 'Install Node.js'
2586
- ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
2587
- - script: |
2588
- ${pmCmd.ci}
2589
- displayName: 'Install dependencies'
2835
+ ${commonSetup} - script: |
2836
+ ${pmCmd.runFilter('functions')} build
2837
+ displayName: 'Build Functions'
2590
2838
 
2591
2839
  - script: |
2592
- ${pmCmd.run} build
2593
- env:
2594
- NODE_ENV: production
2595
- displayName: 'Build Next.js app'
2840
+ SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
2841
+ mkdir -p /tmp/fn-deps
2842
+ node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
2843
+ cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
2844
+ rm -rf ./functions/node_modules
2845
+ mv /tmp/fn-deps/node_modules ./functions/node_modules
2846
+ SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
2847
+ mkdir -p "$SHARED_DEST"
2848
+ cp -r ./shared/dist "$SHARED_DEST/dist"
2849
+ cp ./shared/package.json "$SHARED_DEST/package.json"
2850
+ displayName: 'Prepare functions for deployment'
2596
2851
 
2597
- - task: AzureStaticWebApp@0
2852
+ - task: ArchiveFiles@2
2598
2853
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2599
2854
  inputs:
2600
- app_location: '.'
2601
- output_location: '.next/standalone'
2602
- skip_app_build: true
2603
- azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
2604
- displayName: 'Deploy to Azure Static Web Apps'
2855
+ rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions'
2856
+ includeRootFolder: false
2857
+ archiveType: 'zip'
2858
+ archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
2859
+ displayName: 'Archive Functions'
2860
+
2861
+ - task: PublishBuildArtifacts@1
2862
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2863
+ inputs:
2864
+ PathtoPublish: '$(Build.ArtifactStagingDirectory)/functions.zip'
2865
+ ArtifactName: 'functions'
2866
+ displayName: 'Publish Functions artifact'
2867
+
2868
+ - task: AzureFunctionApp@2
2869
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2870
+ inputs:
2871
+ azureSubscription: '$(AZURE_SUBSCRIPTION)'
2872
+ appType: 'functionAppLinux'
2873
+ appName: '$(AZURE_FUNCTIONAPP_NAME)'
2874
+ package: '$(Build.ArtifactStagingDirectory)/functions.zip'
2875
+ displayName: 'Deploy to Azure Functions'
2605
2876
  `;
2606
- fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
2607
- // functions.yml
2608
- const functionsPipeline = `trigger:
2877
+ }
2878
+ if (backendLanguage === 'csharp') {
2879
+ return `trigger:
2609
2880
  branches:
2610
2881
  include:
2611
2882
  - main
@@ -2630,35 +2901,68 @@ variables:
2630
2901
  - group: azure-deployment
2631
2902
 
2632
2903
  steps:
2633
- - task: NodeTool@0
2904
+ ${commonSetup} - task: UseDotNet@2
2634
2905
  inputs:
2635
- versionSpec: '22.x'
2636
- displayName: 'Install Node.js'
2637
- ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
2638
- - script: |
2639
- ${pmCmd.ci}
2640
- displayName: 'Install workspace dependencies'
2906
+ version: '10.0.x'
2907
+ displayName: 'Install .NET SDK'
2641
2908
 
2642
2909
  - script: |
2643
- ${pmCmd.runFilter('shared')} build
2644
- displayName: 'Build shared package'
2910
+ dotnet publish ./functions -c Release -o ./functions/publish
2911
+ displayName: 'Publish Functions'
2645
2912
 
2646
- - script: |
2647
- ${pmCmd.runFilter('functions')} build
2648
- displayName: 'Build Functions'
2913
+ - task: ArchiveFiles@2
2914
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2915
+ inputs:
2916
+ rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions/publish'
2917
+ includeRootFolder: false
2918
+ archiveType: 'zip'
2919
+ archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
2920
+ displayName: 'Archive Functions'
2921
+
2922
+ - task: AzureFunctionApp@2
2923
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2924
+ inputs:
2925
+ azureSubscription: '$(AZURE_SUBSCRIPTION)'
2926
+ appType: 'functionAppLinux'
2927
+ appName: '$(AZURE_FUNCTIONAPP_NAME)'
2928
+ package: '$(Build.ArtifactStagingDirectory)/functions.zip'
2929
+ displayName: 'Deploy to Azure Functions'
2930
+ `;
2931
+ }
2932
+ return `trigger:
2933
+ branches:
2934
+ include:
2935
+ - main
2936
+ paths:
2937
+ include:
2938
+ - functions/**
2939
+ - shared/**
2940
+
2941
+ pr:
2942
+ branches:
2943
+ include:
2944
+ - main
2945
+ paths:
2946
+ include:
2947
+ - functions/**
2948
+ - shared/**
2949
+
2950
+ pool:
2951
+ vmImage: 'ubuntu-latest'
2952
+
2953
+ variables:
2954
+ - group: azure-deployment
2955
+
2956
+ steps:
2957
+ ${commonSetup} - task: UsePythonVersion@0
2958
+ inputs:
2959
+ versionSpec: '3.11'
2960
+ displayName: 'Install Python'
2649
2961
 
2650
2962
  - script: |
2651
- SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
2652
- mkdir -p /tmp/fn-deps
2653
- node -e "const p=JSON.parse(require('fs').readFileSync('./functions/package.json','utf8'));Object.keys(p.dependencies).filter(k=>k.endsWith('/shared')).forEach(k=>delete p.dependencies[k]);require('fs').writeFileSync('/tmp/fn-deps/package.json',JSON.stringify(p,null,2));"
2654
- cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
2655
- rm -rf ./functions/node_modules
2656
- mv /tmp/fn-deps/node_modules ./functions/node_modules
2657
- SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
2658
- mkdir -p "$SHARED_DEST"
2659
- cp -r ./shared/dist "$SHARED_DEST/dist"
2660
- cp ./shared/package.json "$SHARED_DEST/package.json"
2661
- displayName: 'Prepare functions for deployment'
2963
+ python -m pip install --upgrade pip
2964
+ python -m pip install -r ./functions/requirements.txt --target "./functions/.python_packages/lib/site-packages"
2965
+ displayName: 'Install Functions dependencies'
2662
2966
 
2663
2967
  - task: ArchiveFiles@2
2664
2968
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
@@ -2669,13 +2973,6 @@ ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
2669
2973
  archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
2670
2974
  displayName: 'Archive Functions'
2671
2975
 
2672
- - task: PublishBuildArtifacts@1
2673
- condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2674
- inputs:
2675
- PathtoPublish: '$(Build.ArtifactStagingDirectory)/functions.zip'
2676
- ArtifactName: 'functions'
2677
- displayName: 'Publish Functions artifact'
2678
-
2679
2976
  - task: AzureFunctionApp@2
2680
2977
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2681
2978
  inputs:
@@ -2685,6 +2982,264 @@ ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
2685
2982
  package: '$(Build.ArtifactStagingDirectory)/functions.zip'
2686
2983
  displayName: 'Deploy to Azure Functions'
2687
2984
  `;
2985
+ }
2986
+ function buildAzureSwaPipeline(pm, projectPm = pm) {
2987
+ const pmCmd = (0, package_manager_1.getCommands)(pm);
2988
+ const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
2989
+ const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
2990
+ return `trigger:
2991
+ branches:
2992
+ include:
2993
+ - main
2994
+ paths:
2995
+ include:
2996
+ - app/**
2997
+ - components/**
2998
+ - lib/**
2999
+ - shared/**
3000
+ - public/**
3001
+ - package.json
3002
+ - next.config.js
3003
+
3004
+ pr:
3005
+ branches:
3006
+ include:
3007
+ - main
3008
+ paths:
3009
+ include:
3010
+ - app/**
3011
+ - components/**
3012
+ - lib/**
3013
+ - shared/**
3014
+ - public/**
3015
+ - package.json
3016
+ - next.config.js
3017
+
3018
+ pool:
3019
+ vmImage: 'ubuntu-latest'
3020
+
3021
+ variables:
3022
+ - group: azure-deployment
3023
+
3024
+ steps:
3025
+ - task: NodeTool@0
3026
+ inputs:
3027
+ versionSpec: '22.x'
3028
+ displayName: 'Install Node.js'
3029
+ ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
3030
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
3031
+ - script: |
3032
+ ${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
3033
+ displayName: 'Install dependencies'
3034
+
3035
+ - script: |
3036
+ ${pmCmd.run} build
3037
+ env:
3038
+ NODE_ENV: production
3039
+ displayName: 'Build Next.js app'
3040
+
3041
+ - task: AzureStaticWebApp@0
3042
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
3043
+ inputs:
3044
+ app_location: '.'
3045
+ output_location: '.next/standalone'
3046
+ skip_app_build: true
3047
+ azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
3048
+ displayName: 'Deploy to Azure Static Web Apps'
3049
+ `;
3050
+ }
3051
+ function getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm) {
3052
+ if (pm !== 'npm' || projectPm !== 'pnpm') {
3053
+ return '';
3054
+ }
3055
+ return ` - script: |
3056
+ node <<'NODE'
3057
+ const fs = require('fs');
3058
+
3059
+ function readJson(path) {
3060
+ return JSON.parse(fs.readFileSync(path, 'utf8'));
3061
+ }
3062
+
3063
+ function writeJson(path, value) {
3064
+ fs.writeFileSync(path, JSON.stringify(value, null, 2) + '\\n');
3065
+ }
3066
+
3067
+ const sharedPackagePath = 'shared/package.json';
3068
+ const sharedPackageName = fs.existsSync(sharedPackagePath) ? readJson(sharedPackagePath).name : undefined;
3069
+
3070
+ function replaceSharedWorkspaceDep(pkg, replacement) {
3071
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
3072
+ if (!pkg[section]) continue;
3073
+
3074
+ for (const [name, version] of Object.entries(pkg[section])) {
3075
+ if (typeof version === 'string' && version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
3076
+ pkg[section][name] = replacement;
3077
+ }
3078
+ }
3079
+ }
3080
+ }
3081
+
3082
+ const root = readJson('package.json');
3083
+ root.workspaces = ['shared', 'functions'];
3084
+ replaceSharedWorkspaceDep(root, 'file:shared');
3085
+ root.scripts = root.scripts || {};
3086
+ root.scripts.build = ${JSON.stringify((0, package_manager_1.getBuildScript)('npm'))};
3087
+ writeJson('package.json', root);
3088
+
3089
+ if (fs.existsSync('functions/package.json')) {
3090
+ const functionsPkg = readJson('functions/package.json');
3091
+ replaceSharedWorkspaceDep(functionsPkg, 'file:../shared');
3092
+ if (functionsPkg.scripts && functionsPkg.scripts.prestart) {
3093
+ functionsPkg.scripts.prestart = 'npm run build';
3094
+ }
3095
+ writeJson('functions/package.json', functionsPkg);
3096
+ }
3097
+ NODE
3098
+
3099
+ npm install --package-lock-only --ignore-scripts
3100
+ displayName: 'Normalize pnpm workspace for npm CI'`;
3101
+ }
3102
+ function getGitHubNpmWorkspaceNormalizationStep(pm, projectPm) {
3103
+ if (pm !== 'npm' || projectPm !== 'pnpm') {
3104
+ return '';
3105
+ }
3106
+ return ` - name: Normalize pnpm workspace for npm CI
3107
+ shell: bash
3108
+ run: |
3109
+ node <<'NODE'
3110
+ const fs = require('fs');
3111
+
3112
+ function readJson(path) {
3113
+ return JSON.parse(fs.readFileSync(path, 'utf8'));
3114
+ }
3115
+
3116
+ function writeJson(path, value) {
3117
+ fs.writeFileSync(path, JSON.stringify(value, null, 2) + '\\n');
3118
+ }
3119
+
3120
+ const sharedPackagePath = 'shared/package.json';
3121
+ const sharedPackageName = fs.existsSync(sharedPackagePath) ? readJson(sharedPackagePath).name : undefined;
3122
+
3123
+ function replaceSharedWorkspaceDep(pkg, replacement) {
3124
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
3125
+ if (!pkg[section]) continue;
3126
+
3127
+ for (const [name, version] of Object.entries(pkg[section])) {
3128
+ if (typeof version === 'string' && version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
3129
+ pkg[section][name] = replacement;
3130
+ }
3131
+ }
3132
+ }
3133
+ }
3134
+
3135
+ const root = readJson('package.json');
3136
+ root.workspaces = ['shared', 'functions'];
3137
+ replaceSharedWorkspaceDep(root, 'file:shared');
3138
+ root.scripts = root.scripts || {};
3139
+ root.scripts.build = ${JSON.stringify((0, package_manager_1.getBuildScript)('npm'))};
3140
+ writeJson('package.json', root);
3141
+
3142
+ if (fs.existsSync('functions/package.json')) {
3143
+ const functionsPkg = readJson('functions/package.json');
3144
+ replaceSharedWorkspaceDep(functionsPkg, 'file:../shared');
3145
+ if (functionsPkg.scripts && functionsPkg.scripts.prestart) {
3146
+ functionsPkg.scripts.prestart = 'npm run build';
3147
+ }
3148
+ writeJson('functions/package.json', functionsPkg);
3149
+ }
3150
+ NODE
3151
+
3152
+ npm install --package-lock-only --ignore-scripts
3153
+ `;
3154
+ }
3155
+ async function createGitHubActionsWorkflows(projectDir, azureConfig, pm, backendLanguage) {
3156
+ console.log('📦 Creating GitHub Actions workflows...\n');
3157
+ const workflowsDir = path.join(projectDir, '.github', 'workflows');
3158
+ fs.mkdirSync(workflowsDir, { recursive: true });
3159
+ const actionsPm = 'npm';
3160
+ const swaWorkflow = buildGitHubSwaWorkflow(actionsPm, pm);
3161
+ fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
3162
+ // deploy-functions.yml
3163
+ const functionsWorkflow = getGitHubFunctionsWorkflow(actionsPm, backendLanguage, pm);
3164
+ fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
3165
+ console.log('✅ GitHub Actions workflows created\n');
3166
+ }
3167
+ function buildGitHubSwaWorkflow(pm, projectPm = pm) {
3168
+ const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
3169
+ return `name: Deploy Static Web App
3170
+
3171
+ on:
3172
+ push:
3173
+ branches:
3174
+ - main
3175
+ paths:
3176
+ - 'app/**'
3177
+ - 'components/**'
3178
+ - 'lib/**'
3179
+ - 'shared/**'
3180
+ - 'public/**'
3181
+ - 'package.json'
3182
+ - 'next.config.js'
3183
+ - 'next.config.ts'
3184
+ workflow_dispatch:
3185
+ pull_request:
3186
+ branches:
3187
+ - main
3188
+ paths:
3189
+ - 'app/**'
3190
+ - 'components/**'
3191
+ - 'lib/**'
3192
+ - 'shared/**'
3193
+ - 'public/**'
3194
+ - 'package.json'
3195
+ - 'next.config.js'
3196
+ - 'next.config.ts'
3197
+
3198
+ jobs:
3199
+ build-and-deploy:
3200
+ runs-on: ubuntu-latest
3201
+ name: Build and Deploy Static Web App
3202
+
3203
+ steps:
3204
+ - uses: actions/checkout@v4
3205
+ with:
3206
+ submodules: true
3207
+
3208
+ - name: Setup Node.js
3209
+ uses: actions/setup-node@v4
3210
+ with:
3211
+ node-version: '22'
3212
+ ${(0, package_manager_1.getCiSetupStep)(pm) ? `\n${(0, package_manager_1.getCiSetupStep)(pm)}\n` : ''}
3213
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
3214
+ - name: Install and build app
3215
+ run: |
3216
+ ${(0, package_manager_1.getSwaAppBuildCommand)(pm, projectPm)}
3217
+
3218
+ - name: Deploy to Azure Static Web Apps
3219
+ if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
3220
+ uses: Azure/static-web-apps-deploy@v1
3221
+ with:
3222
+ azure_static_web_apps_api_token: \${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
3223
+ repo_token: \${{ secrets.GITHUB_TOKEN }}
3224
+ action: 'upload'
3225
+ app_location: '/'
3226
+ api_location: ''
3227
+ output_location: ''
3228
+ app_build_command: '${(0, package_manager_1.getSwaAppBuildCommand)(pm, projectPm)}'
3229
+ env:
3230
+ NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
3231
+ `;
3232
+ }
3233
+ async function createAzurePipelines(projectDir, pm, backendLanguage) {
3234
+ console.log('📦 Creating Azure Pipelines...\n');
3235
+ const pipelinesPm = 'npm';
3236
+ const pipelinesDir = path.join(projectDir, 'pipelines');
3237
+ fs.mkdirSync(pipelinesDir, { recursive: true });
3238
+ // swa.yml
3239
+ const swaPipeline = buildAzureSwaPipeline(pipelinesPm, pm);
3240
+ fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
3241
+ // functions.yml
3242
+ const functionsPipeline = getAzureFunctionsPipeline(pipelinesPm, backendLanguage, pm);
2688
3243
  fs.writeFileSync(path.join(pipelinesDir, 'functions.yml'), functionsPipeline);
2689
3244
  console.log('✅ Azure Pipelines created\n');
2690
3245
  }