swallowkit 1.0.0-beta.6 → 1.0.0-beta.61

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 +87 -175
  2. package/README.md +87 -176
  3. package/dist/__tests__/fixtures.d.ts +12 -0
  4. package/dist/__tests__/fixtures.d.ts.map +1 -1
  5. package/dist/__tests__/fixtures.js +101 -0
  6. package/dist/__tests__/fixtures.js.map +1 -1
  7. package/dist/cli/commands/add-auth.d.ts +15 -0
  8. package/dist/cli/commands/add-auth.d.ts.map +1 -0
  9. package/dist/cli/commands/add-auth.js +790 -0
  10. package/dist/cli/commands/add-auth.js.map +1 -0
  11. package/dist/cli/commands/add-connector.d.ts +20 -0
  12. package/dist/cli/commands/add-connector.d.ts.map +1 -0
  13. package/dist/cli/commands/add-connector.js +164 -0
  14. package/dist/cli/commands/add-connector.js.map +1 -0
  15. package/dist/cli/commands/cosmos-worktree-isolation.d.ts +25 -0
  16. package/dist/cli/commands/cosmos-worktree-isolation.d.ts.map +1 -0
  17. package/dist/cli/commands/cosmos-worktree-isolation.js +124 -0
  18. package/dist/cli/commands/cosmos-worktree-isolation.js.map +1 -0
  19. package/dist/cli/commands/create-model.d.ts +1 -4
  20. package/dist/cli/commands/create-model.d.ts.map +1 -1
  21. package/dist/cli/commands/create-model.js +20 -82
  22. package/dist/cli/commands/create-model.js.map +1 -1
  23. package/dist/cli/commands/dev-seeds.d.ts +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 +74 -0
  36. package/dist/cli/commands/init.d.ts.map +1 -1
  37. package/dist/cli/commands/init.js +1376 -930
  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 +335 -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 +160 -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 +230 -79
  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 +9 -12
  192. package/dist/utils/package-manager.d.ts.map +1 -1
  193. package/dist/utils/package-manager.js +51 -22
  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 +16 -17
  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 +459 -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 +363 -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 +142 -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 +244 -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 +159 -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 +3414 -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 +352 -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 +504 -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 +781 -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 +251 -0
  295. package/src/utils/python-uv.ts +96 -0
  296. package/src/version.ts +13 -0
@@ -36,12 +36,159 @@ 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.injectSwallowKitNextConfig = injectSwallowKitNextConfig;
46
+ exports.buildCSharpFunctionsProgramSource = buildCSharpFunctionsProgramSource;
47
+ exports.buildCSharpFunctionsProjectSource = buildCSharpFunctionsProjectSource;
48
+ exports.buildSwallowKitConfigSource = buildSwallowKitConfigSource;
49
+ exports.buildGeneratedProjectDependencies = buildGeneratedProjectDependencies;
50
+ exports.buildGeneratedProjectDevDependencies = buildGeneratedProjectDevDependencies;
51
+ exports.buildSwallowKitMcpProjectConfigSource = buildSwallowKitMcpProjectConfigSource;
52
+ exports.buildTypeScriptFunctionsPackageJson = buildTypeScriptFunctionsPackageJson;
53
+ exports.buildFunctionsHostKeyBicepExpression = buildFunctionsHostKeyBicepExpression;
54
+ exports.buildStaticWebAppConfigBicepSource = buildStaticWebAppConfigBicepSource;
55
+ exports.createInfrastructure = createInfrastructure;
56
+ exports.getGitHubFunctionsWorkflow = getGitHubFunctionsWorkflow;
57
+ exports.getAzureFunctionsPipeline = getAzureFunctionsPipeline;
58
+ exports.buildAzureSwaPipeline = buildAzureSwaPipeline;
59
+ exports.buildGitHubSwaWorkflow = buildGitHubSwaWorkflow;
40
60
  const fs = __importStar(require("fs"));
41
61
  const path = __importStar(require("path"));
42
62
  const child_process_1 = require("child_process");
43
63
  const prompts_1 = __importDefault(require("prompts"));
44
64
  const package_manager_1 = require("../../utils/package-manager");
65
+ const manifest_1 = require("../../core/project/manifest");
66
+ const workflows_1 = require("../../core/project/workflows");
67
+ const agent_instructions_1 = require("../../core/project/agent-instructions");
68
+ const native_schema_generator_1 = require("../../core/scaffold/native-schema-generator");
69
+ const python_uv_1 = require("../../utils/python-uv");
70
+ function getStaticWebAppSku(plan) {
71
+ return plan === 'free' ? 'Free' : 'Standard';
72
+ }
73
+ const BACKEND_LANGUAGE_CHOICES = [
74
+ { title: "TypeScript", value: "typescript" },
75
+ { title: "C#", value: "csharp" },
76
+ { title: "Python", value: "python" },
77
+ ];
78
+ function usesNodeFunctionsProject(backendLanguage) {
79
+ return backendLanguage === "typescript";
80
+ }
81
+ function getBackendLanguageLabel(backendLanguage) {
82
+ return BACKEND_LANGUAGE_CHOICES.find((choice) => choice.value === backendLanguage)?.title || backendLanguage;
83
+ }
84
+ function buildCosmosDbFreeTierBicepSource() {
85
+ return `@description('Cosmos DB account name')
86
+ param accountName string
87
+
88
+ @description('Database name')
89
+ param databaseName string
90
+
91
+ @description('Location for Cosmos DB')
92
+ param location string
93
+
94
+ @description('Public network access')
95
+ @allowed(['Enabled', 'Disabled'])
96
+ param publicNetworkAccess string = 'Enabled'
97
+
98
+ // Cosmos DB Account (Free Tier)
99
+ resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
100
+ name: accountName
101
+ location: location
102
+ kind: 'GlobalDocumentDB'
103
+ properties: {
104
+ databaseAccountOfferType: 'Standard'
105
+ enableAutomaticFailover: false
106
+ enableFreeTier: true
107
+ publicNetworkAccess: publicNetworkAccess
108
+ disableLocalAuth: true
109
+ consistencyPolicy: {
110
+ defaultConsistencyLevel: 'Session'
111
+ }
112
+ locations: [
113
+ {
114
+ locationName: location
115
+ failoverPriority: 0
116
+ isZoneRedundant: false
117
+ }
118
+ ]
119
+ disableKeyBasedMetadataWriteAccess: true
120
+ }
121
+ }
122
+
123
+ // Cosmos DB Database
124
+ resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
125
+ parent: cosmosAccount
126
+ name: databaseName
127
+ properties: {
128
+ resource: {
129
+ id: databaseName
130
+ }
131
+ options: {
132
+ throughput: 1000
133
+ }
134
+ }
135
+ }
136
+
137
+ output id string = cosmosAccount.id
138
+ output accountName string = cosmosAccount.name
139
+ output endpoint string = cosmosAccount.properties.documentEndpoint
140
+ output databaseName string = database.name
141
+ `;
142
+ }
143
+ function getFunctionsWorkerRuntime(backendLanguage) {
144
+ if (backendLanguage === "csharp") {
145
+ return "dotnet-isolated";
146
+ }
147
+ if (backendLanguage === "python") {
148
+ return "python";
149
+ }
150
+ return "node";
151
+ }
152
+ function getFunctionsRuntimeConfig(backendLanguage) {
153
+ if (backendLanguage === "csharp") {
154
+ return { name: "dotnet-isolated", version: "10.0" };
155
+ }
156
+ if (backendLanguage === "python") {
157
+ return { name: "python", version: "3.11" };
158
+ }
159
+ return { name: "node", version: "22" };
160
+ }
161
+ function buildSharedTsConfig() {
162
+ return {
163
+ compilerOptions: {
164
+ target: "ES2020",
165
+ module: "Node16",
166
+ moduleResolution: "node16",
167
+ lib: ["ES2020"],
168
+ outDir: "dist",
169
+ rootDir: ".",
170
+ declaration: true,
171
+ declarationMap: true,
172
+ sourceMap: true,
173
+ strict: true,
174
+ esModuleInterop: true,
175
+ skipLibCheck: true,
176
+ forceConsistentCasingInFileNames: true,
177
+ },
178
+ include: ["index.ts", "models/**/*"],
179
+ exclude: ["node_modules", "dist"],
180
+ };
181
+ }
182
+ async function promptBackendLanguage() {
183
+ const response = await (0, prompts_1.default)({
184
+ type: "select",
185
+ name: "backendLanguage",
186
+ message: "Azure Functions backend language:",
187
+ choices: BACKEND_LANGUAGE_CHOICES,
188
+ initial: 0,
189
+ });
190
+ return response.backendLanguage || "typescript";
191
+ }
45
192
  async function promptCiCd() {
46
193
  const response = await (0, prompts_1.default)({
47
194
  type: 'select',
@@ -56,40 +203,72 @@ async function promptCiCd() {
56
203
  });
57
204
  return response.cicd || 'skip';
58
205
  }
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
- });
206
+ async function promptAzureConfig(options) {
207
+ let swaPlan = options.swaPlan;
208
+ if (!swaPlan) {
209
+ const response = await (0, prompts_1.default)({
210
+ type: 'select',
211
+ name: 'plan',
212
+ message: 'Azure Static Web Apps plan:',
213
+ choices: [
214
+ { title: 'Standard (production, SLA and advanced auth/networking features)', value: 'standard' },
215
+ { title: 'Free (personal projects, no SLA and lower quotas)', value: 'free' }
216
+ ],
217
+ initial: 0
218
+ });
219
+ swaPlan = response.plan || 'standard';
220
+ }
221
+ let cosmosDbMode = options.cosmosDbMode;
222
+ if (!cosmosDbMode) {
223
+ const response = await (0, prompts_1.default)({
224
+ type: 'select',
225
+ name: 'mode',
226
+ message: 'Cosmos DB mode (affects cost):',
227
+ choices: [
228
+ { title: 'Free Tier (1000 RU/s free, best for first project)', value: 'freetier' },
229
+ { title: 'Serverless (pay-per-use, flexible)', value: 'serverless' }
230
+ ],
231
+ initial: 0
232
+ });
233
+ cosmosDbMode = response.mode || 'freetier';
234
+ }
235
+ let vnetOption = options.vnet;
236
+ if (!vnetOption) {
237
+ const response = await (0, prompts_1.default)({
238
+ type: 'select',
239
+ name: 'vnet',
240
+ message: 'Network security:',
241
+ choices: [
242
+ { title: 'VNet Integration (recommended) - Cosmos DB via Private Endpoint', value: 'outbound' },
243
+ { title: 'None - Public endpoints, simplest but least secure', value: 'none' }
244
+ ],
245
+ initial: 0
246
+ });
247
+ vnetOption = response.vnet || 'outbound';
248
+ }
80
249
  return {
81
- cosmosDbMode: cosmosResponse.mode || 'freetier',
82
- vnetOption: vnetResponse.vnet || 'outbound'
250
+ cosmosDbMode: cosmosDbMode || 'freetier',
251
+ vnetOption: vnetOption || 'outbound',
252
+ swaPlan: swaPlan || 'standard'
83
253
  };
84
254
  }
85
255
  const VALID_CICD = ['github', 'azure', 'skip'];
256
+ const VALID_BACKEND_LANGUAGE = ['typescript', 'csharp', 'python'];
86
257
  const VALID_COSMOS_DB_MODE = ['freetier', 'serverless'];
87
258
  const VALID_VNET = ['none', 'outbound'];
259
+ const VALID_SWA_PLAN = ['free', 'standard'];
88
260
  function validateInitFlags(options) {
261
+ if (options.packageManager && !['npm', 'pnpm'].includes(options.packageManager)) {
262
+ throw new Error('Invalid --package-manager: expected npm or pnpm');
263
+ }
89
264
  if (options.cicd && !VALID_CICD.includes(options.cicd)) {
90
265
  console.error(`❌ Invalid --cicd value: "${options.cicd}". Must be: ${VALID_CICD.join(', ')}`);
91
266
  process.exit(1);
92
267
  }
268
+ if (options.backendLanguage && !VALID_BACKEND_LANGUAGE.includes(options.backendLanguage)) {
269
+ console.error(`❌ Invalid --backend-language value: "${options.backendLanguage}". Must be: ${VALID_BACKEND_LANGUAGE.join(', ')}`);
270
+ process.exit(1);
271
+ }
93
272
  if (options.cosmosDbMode && !VALID_COSMOS_DB_MODE.includes(options.cosmosDbMode)) {
94
273
  console.error(`❌ Invalid --cosmos-db-mode value: "${options.cosmosDbMode}". Must be: ${VALID_COSMOS_DB_MODE.join(', ')}`);
95
274
  process.exit(1);
@@ -98,6 +277,10 @@ function validateInitFlags(options) {
98
277
  console.error(`❌ Invalid --vnet value: "${options.vnet}". Must be: ${VALID_VNET.join(', ')}`);
99
278
  process.exit(1);
100
279
  }
280
+ if (options.swaPlan && !VALID_SWA_PLAN.includes(options.swaPlan)) {
281
+ console.error(`❌ Invalid --swa-plan value: "${options.swaPlan}". Must be: ${VALID_SWA_PLAN.join(', ')}`);
282
+ process.exit(1);
283
+ }
101
284
  }
102
285
  async function initCommand(options) {
103
286
  // Validate flag values before doing anything
@@ -105,7 +288,7 @@ async function initCommand(options) {
105
288
  console.log(`🚀 Initializing SwallowKit project: ${options.name}`);
106
289
  console.log(`📋 Template: ${options.template}`);
107
290
  // Detect package manager from invocation context (npx → npm, pnpm dlx → pnpm)
108
- const pm = (0, package_manager_1.detectFromUserAgent)();
291
+ const pm = options.packageManager || (0, package_manager_1.detectFromUserAgent)();
109
292
  const pmCmd = (0, package_manager_1.getCommands)(pm);
110
293
  console.log(`📦 Package manager: ${pm}`);
111
294
  const projectDir = path.join(process.cwd(), options.name);
@@ -115,26 +298,46 @@ async function initCommand(options) {
115
298
  console.error(`❌ Directory "${options.name}" already exists.`);
116
299
  process.exit(1);
117
300
  }
301
+ // 非対話環境でプロンプトに入るとハングするため、不足フラグを明示エラーで案内する
302
+ const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true && !process.env.CI;
303
+ if (!interactive) {
304
+ const missing = [];
305
+ if (!options.cicd)
306
+ missing.push('--cicd <github|azure|skip>');
307
+ if (!options.backendLanguage)
308
+ missing.push('--backend-language <typescript|csharp|python>');
309
+ if (!options.cosmosDbMode)
310
+ missing.push('--cosmos-db-mode <freetier|serverless>');
311
+ if (!options.vnet)
312
+ missing.push('--vnet <outbound|none>');
313
+ if (!options.swaPlan)
314
+ missing.push('--swa-plan <free|standard>');
315
+ if (missing.length > 0) {
316
+ console.error(`❌ Non-interactive environment detected. Pass all configuration flags to run init without prompts:\n` +
317
+ missing.map((flag) => ` ${flag}`).join('\n'));
318
+ process.exit(1);
319
+ }
320
+ }
118
321
  // Use flag values if provided, otherwise prompt interactively
119
322
  const cicdProvider = options.cicd || await promptCiCd();
120
- const azureConfig = (options.cosmosDbMode && options.vnet)
121
- ? { cosmosDbMode: options.cosmosDbMode, vnetOption: options.vnet }
122
- : await promptAzureConfig();
323
+ const backendLanguage = options.backendLanguage || await promptBackendLanguage();
324
+ const azureConfig = await promptAzureConfig(options);
123
325
  // Create Next.js project with create-next-app
124
326
  await createNextJsProject(options.name, pm);
125
327
  // Upgrade Next.js to specified version (or latest) to avoid cached old versions
126
328
  await upgradeNextJs(projectDir, options.nextVersion || 'latest', pm);
127
329
  // Add SwallowKit specific files
128
- await addSwallowKitFiles(projectDir, options, cicdProvider, azureConfig, pm);
330
+ await addSwallowKitFiles(projectDir, options, cicdProvider, azureConfig, pm, backendLanguage);
129
331
  // Create infrastructure files (Bicep)
130
- await createInfrastructure(projectDir, options.name, azureConfig);
332
+ await createInfrastructure(projectDir, options.name, azureConfig, backendLanguage);
131
333
  // Create CI/CD files based on choice
132
334
  if (cicdProvider === 'github') {
133
- await createGitHubActionsWorkflows(projectDir, azureConfig, pm);
335
+ await createGitHubActionsWorkflows(projectDir, azureConfig, pm, backendLanguage);
134
336
  }
135
337
  else if (cicdProvider === 'azure') {
136
- await createAzurePipelines(projectDir, pm);
338
+ await createAzurePipelines(projectDir, pm, backendLanguage);
137
339
  }
340
+ await (0, manifest_1.syncProjectManifest)(projectDir);
138
341
  // Initialize Git repository and create initial commit
139
342
  try {
140
343
  // Try git init with -b main (Git 2.28+), fallback to git init
@@ -178,11 +381,11 @@ async function initCommand(options) {
178
381
  console.log(`\n✅ Project "${options.name}" created successfully!`);
179
382
  console.log("\n📝 Next steps:");
180
383
  console.log(` cd ${options.name}`);
181
- 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`);
183
- console.log(` ${pmCmd.dlx} swallowkit dev # Start development servers`);
384
+ console.log(` ${pmCmd.exec} swallowkit create-model <name> # Create your first model`);
385
+ console.log(` ${pmCmd.exec} swallowkit scaffold shared/models/<name>.ts # Generate CRUD code`);
386
+ console.log(` ${pmCmd.exec} swallowkit dev # Start development servers`);
184
387
  console.log("\n🚀 Deploy to Azure:");
185
- console.log(` ${pmCmd.dlx} swallowkit provision --resource-group <name>`);
388
+ console.log(` ${pmCmd.exec} swallowkit provision --resource-group <name>`);
186
389
  if (cicdProvider !== 'skip') {
187
390
  console.log(" Configure CI/CD secrets and push to repository");
188
391
  }
@@ -204,7 +407,7 @@ async function createNextJsProject(projectName, pm) {
204
407
  // for npm use "npx create-next-app@latest ..."
205
408
  const baseArgs = pm === 'pnpm'
206
409
  ? ['dlx', 'create-next-app@latest']
207
- : ['create-next-app@latest'];
410
+ : ['--yes', '--package', 'create-next-app@latest', '--', 'create-next-app'];
208
411
  const args = [
209
412
  ...baseArgs,
210
413
  projectName,
@@ -213,6 +416,7 @@ async function createNextJsProject(projectName, pm) {
213
416
  '--app',
214
417
  '--no-src',
215
418
  '--disable-git',
419
+ '--skip-install',
216
420
  '--import-alias',
217
421
  '@/*',
218
422
  ...(pmCmd.createNextAppFlag ? [pmCmd.createNextAppFlag] : []),
@@ -238,92 +442,298 @@ async function createNextJsProject(projectName, pm) {
238
442
  });
239
443
  }
240
444
  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,
251
- });
252
- child.on('close', (code) => {
253
- if (code !== 0) {
254
- reject(new Error(`${pm} add next@${version} exited with code ${code}`));
445
+ console.log(`\n📦 Installing Next.js ${version} (to ensure latest security patches)...\n`);
446
+ // pnpm: pnpm add next@... ; npm: npm install next@...
447
+ const args = pm === 'pnpm'
448
+ ? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact', '--lockfile-only']
449
+ : ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact', '--package-lock-only'];
450
+ await runPackageManagerCommand(pm, args, projectDir, `${pm} add next@${version}`);
451
+ console.log(`\n✅ Next.js ${version} installed\n`);
452
+ }
453
+ async function installDependencies(projectDir, pm = 'pnpm') {
454
+ console.log('\n📦 Installing dependencies...\n');
455
+ await runPackageManagerCommand(pm, pm === 'pnpm' ? ['install', '--no-frozen-lockfile'] : ['install'], projectDir, `${pm} install`);
456
+ console.log('\n✅ Dependencies installed\n');
457
+ }
458
+ async function withNpmLockfileSafeManifests(projectDir, action) {
459
+ const packageJsonPath = path.join(projectDir, 'package.json');
460
+ const functionsPackageJsonPath = path.join(projectDir, 'functions', 'package.json');
461
+ const sharedPackageJsonPath = path.join(projectDir, 'shared', 'package.json');
462
+ const originalFiles = new Map();
463
+ const hiddenNodeModules = new Map();
464
+ function rememberOriginal(filePath) {
465
+ if (!originalFiles.has(filePath)) {
466
+ originalFiles.set(filePath, fs.readFileSync(filePath, 'utf-8'));
467
+ }
468
+ }
469
+ function readJson(filePath) {
470
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
471
+ }
472
+ function writeJson(filePath, value) {
473
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
474
+ }
475
+ const sharedPackageName = fs.existsSync(sharedPackageJsonPath)
476
+ ? readJson(sharedPackageJsonPath).name
477
+ : undefined;
478
+ function replaceSharedWorkspaceDependency(packageJson, replacement) {
479
+ let changed = false;
480
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
481
+ const dependencies = packageJson[section];
482
+ if (!dependencies)
483
+ continue;
484
+ for (const [name, version] of Object.entries(dependencies)) {
485
+ if (version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
486
+ dependencies[name] = replacement;
487
+ changed = true;
488
+ }
255
489
  }
256
- else {
257
- console.log(`\n✅ Next.js ${version} installed\n`);
258
- resolve();
490
+ }
491
+ return changed;
492
+ }
493
+ function normalizePackageJson(filePath, replacement) {
494
+ if (!fs.existsSync(filePath))
495
+ return;
496
+ const packageJson = readJson(filePath);
497
+ if (!replaceSharedWorkspaceDependency(packageJson, replacement))
498
+ return;
499
+ rememberOriginal(filePath);
500
+ writeJson(filePath, packageJson);
501
+ }
502
+ function hideNodeModules(directory) {
503
+ const nodeModulesPath = path.join(directory, 'node_modules');
504
+ if (!fs.existsSync(nodeModulesPath))
505
+ return;
506
+ const hiddenPath = path.join(directory, `.node_modules.swallowkit-npm-lockfile-${process.pid}`);
507
+ fs.renameSync(nodeModulesPath, hiddenPath);
508
+ hiddenNodeModules.set(nodeModulesPath, hiddenPath);
509
+ }
510
+ try {
511
+ normalizePackageJson(packageJsonPath, 'file:shared');
512
+ normalizePackageJson(functionsPackageJsonPath, 'file:../shared');
513
+ hideNodeModules(projectDir);
514
+ hideNodeModules(path.join(projectDir, 'shared'));
515
+ hideNodeModules(path.join(projectDir, 'functions'));
516
+ await action();
517
+ }
518
+ finally {
519
+ for (const [nodeModulesPath, hiddenPath] of hiddenNodeModules) {
520
+ if (fs.existsSync(nodeModulesPath)) {
521
+ fs.rmSync(nodeModulesPath, { recursive: true, force: true });
259
522
  }
260
- });
261
- child.on('error', (error) => {
262
- reject(error);
263
- });
264
- });
523
+ fs.renameSync(hiddenPath, nodeModulesPath);
524
+ }
525
+ for (const [filePath, content] of originalFiles) {
526
+ fs.writeFileSync(filePath, content);
527
+ }
528
+ }
265
529
  }
266
- async function installDependencies(projectDir, pm = 'pnpm') {
530
+ /** Run installs without changing the project security policy. */
531
+ async function runPackageManagerCommand(pm, args, projectDir, label) {
532
+ const { code, output } = await spawnAndCapture(pm, args, projectDir, pm === 'pnpm');
533
+ if (code === 0)
534
+ return;
535
+ if (pm === 'pnpm') {
536
+ if (parseIgnoredBuilds(output).length > 0) {
537
+ throw new Error(`${label} exited with code ${code}: pnpm still reports unapproved build scripts. ` +
538
+ 'Review the named dependencies and project security policy before allowing any scripts. SwallowKit does not auto-approve them.');
539
+ }
540
+ }
541
+ throw new Error(`${label} exited with code ${code}`);
542
+ }
543
+ /**
544
+ * Spawn a child process inheriting stdin (so interactive prompts still work)
545
+ * while either inheriting or capturing+teeing stdout/stderr.
546
+ */
547
+ function spawnAndCapture(pm, args, projectDir, capture) {
267
548
  return new Promise((resolve, reject) => {
268
- console.log('\n📦 Installing dependencies...\n');
269
- const child = (0, child_process_1.spawn)(pm, ['install'], {
549
+ const stdio = capture
550
+ ? ['inherit', 'pipe', 'pipe']
551
+ : ['inherit', 'inherit', 'inherit'];
552
+ const child = (0, child_process_1.spawn)(pm, args, {
270
553
  cwd: projectDir,
271
- stdio: 'inherit',
554
+ stdio,
272
555
  shell: true,
273
556
  });
274
- 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');
280
- resolve();
281
- }
282
- });
283
- child.on('error', (error) => {
284
- reject(error);
285
- });
557
+ let combined = '';
558
+ if (capture) {
559
+ child.stdout?.on('data', (chunk) => {
560
+ process.stdout.write(chunk);
561
+ combined += chunk.toString('utf8');
562
+ });
563
+ child.stderr?.on('data', (chunk) => {
564
+ process.stderr.write(chunk);
565
+ combined += chunk.toString('utf8');
566
+ });
567
+ }
568
+ child.on('close', (code) => resolve({ code: code ?? 0, output: combined }));
569
+ child.on('error', reject);
286
570
  });
287
571
  }
288
- async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig, pm) {
572
+ /**
573
+ * Parse pnpm's `Ignored build scripts: foo@1.0.0, bar@2.0.0` warning/error and
574
+ * return the bare package names (without versions), de-duplicated.
575
+ */
576
+ function parseIgnoredBuilds(output) {
577
+ const match = output.match(/Ignored build scripts:\s*([^\n\r]+)/i);
578
+ if (!match)
579
+ return [];
580
+ const list = match[1]
581
+ .split(',')
582
+ .map((entry) => entry.trim())
583
+ // strip version suffix: `sharp@0.34.5` -> `sharp`, `@scope/pkg@1.0.0` -> `@scope/pkg`
584
+ .map((entry) => entry.replace(/@[^@]+$/, ''))
585
+ .filter((name) => name.length > 0);
586
+ return Array.from(new Set(list));
587
+ }
588
+ function injectSwallowKitNextConfig(nextConfigContent, projectName) {
589
+ 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`);
590
+ }
591
+ function buildCSharpFunctionsProgramSource() {
592
+ return `using Microsoft.Azure.Functions.Worker;
593
+ using Microsoft.Extensions.DependencyInjection;
594
+ using Microsoft.Extensions.Hosting;
595
+ using Microsoft.Azure.Functions.Worker.ApplicationInsights;
596
+
597
+ var host = new HostBuilder()
598
+ .ConfigureFunctionsWorkerDefaults()
599
+ .ConfigureServices(services =>
600
+ {
601
+ services.AddApplicationInsightsTelemetryWorkerService();
602
+ services.ConfigureFunctionsApplicationInsights();
603
+ })
604
+ .Build();
605
+
606
+ host.Run();
607
+ `;
608
+ }
609
+ function buildCSharpFunctionsProjectSource() {
610
+ return `<Project Sdk="Microsoft.NET.Sdk">
611
+ <PropertyGroup>
612
+ <TargetFramework>net10.0</TargetFramework>
613
+ <AzureFunctionsVersion>v4</AzureFunctionsVersion>
614
+ <OutputType>Exe</OutputType>
615
+ <ImplicitUsings>enable</ImplicitUsings>
616
+ <Nullable>enable</Nullable>
617
+ </PropertyGroup>
618
+ <ItemGroup>
619
+ <Compile Remove="generated\\**\\bin\\**\\*.cs;generated\\**\\obj\\**\\*.cs" />
620
+ <EmbeddedResource Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />
621
+ <None Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />
622
+ </ItemGroup>
623
+ <ItemGroup>
624
+ <PackageReference Include="Microsoft.Azure.Cosmos" Version="3.47.0" />
625
+ <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
626
+ <PackageReference Include="Azure.Identity" Version="1.17.0" />
627
+ <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" />
628
+ <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
629
+ <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" OutputItemType="Analyzer" />
630
+ <PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" />
631
+ <PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
632
+ </ItemGroup>
633
+ </Project>
634
+ `;
635
+ }
636
+ function buildSwallowKitConfigSource(backendLanguage) {
637
+ return `module.exports = {
638
+ backend: {
639
+ language: '${backendLanguage}',
640
+ },
641
+ functions: {
642
+ baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
643
+ },
644
+ deployment: {
645
+ resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
646
+ swaName: process.env.AZURE_SWA_NAME || '',
647
+ },
648
+ }
649
+ `;
650
+ }
651
+ function buildGeneratedProjectDependencies(projectName, pm = 'pnpm') {
652
+ return {
653
+ '@azure/cosmos': '^4.0.0',
654
+ 'applicationinsights': '^3.3.0',
655
+ 'zod': '^4.0.0',
656
+ [`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:shared',
657
+ };
658
+ }
659
+ function getSwallowKitPackageMetadata() {
660
+ const packageJsonPath = path.resolve(__dirname, "..", "..", "..", "package.json");
661
+ try {
662
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
663
+ return {
664
+ name: packageJson.name || "swallowkit",
665
+ version: packageJson.version || "latest",
666
+ };
667
+ }
668
+ catch {
669
+ // Fall back to the published package name when package metadata is unavailable.
670
+ }
671
+ return {
672
+ name: "swallowkit",
673
+ version: "latest",
674
+ };
675
+ }
676
+ function buildGeneratedProjectDevDependencies() {
677
+ const { name, version } = getSwallowKitPackageMetadata();
678
+ return {
679
+ [name]: version,
680
+ };
681
+ }
682
+ function buildSwallowKitMcpProjectConfigSource(pm = 'pnpm') {
683
+ const { name } = getSwallowKitPackageMetadata();
684
+ return JSON.stringify({
685
+ mcpServers: {
686
+ swallowkit: {
687
+ command: pm === 'pnpm' ? 'pnpm' : 'npx',
688
+ args: pm === 'pnpm'
689
+ ? ["dlx", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"]
690
+ : ["--yes", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"],
691
+ cwd: ".",
692
+ env: {
693
+ SWALLOWKIT_MCP_VERSION: "latest",
694
+ },
695
+ },
696
+ },
697
+ }, null, 2);
698
+ }
699
+ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig, pm, backendLanguage) {
289
700
  console.log('📦 Adding SwallowKit files...\n');
290
701
  const projectName = options.name;
291
- // 1. Update package.json to add swallowkit and @azure/cosmos dependencies
702
+ // 1. Update package.json to add runtime dependencies for generated projects
292
703
  const packageJsonPath = path.join(projectDir, 'package.json');
293
704
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
294
- // Add SwallowKit dependencies (Next.js version already upgraded by upgradeNextJs)
295
- // zod is in the shared workspace package, not here
705
+ // Root BFF/auth code imports Zod directly; declare it as well as the shared package.
296
706
  packageJson.dependencies = {
297
707
  ...packageJson.dependencies,
298
- 'swallowkit': 'latest',
299
- '@azure/cosmos': '^4.0.0',
300
- 'applicationinsights': '^3.3.0',
301
- [`@${projectName}/shared`]: '*',
708
+ ...buildGeneratedProjectDependencies(projectName, pm),
709
+ };
710
+ packageJson.devDependencies = {
711
+ ...packageJson.devDependencies,
712
+ ...buildGeneratedProjectDevDependencies(),
302
713
  };
303
714
  packageJson.scripts = {
304
715
  ...packageJson.scripts,
305
716
  'build': (0, package_manager_1.getBuildScript)(pm),
306
717
  'start': 'next start',
307
- 'functions:start': (0, package_manager_1.getFunctionsStartScript)(pm),
718
+ 'functions:start': (0, package_manager_1.getFunctionsStartScript)(pm, backendLanguage),
308
719
  };
309
- if (pm === 'pnpm') {
310
- packageJson.packageManager = 'pnpm@latest';
311
- }
312
720
  packageJson.engines = {
313
- node: '20.x',
721
+ node: '>=22.14.0',
722
+ ...(pm === 'pnpm' ? { pnpm: '>=11 <13' } : {}),
314
723
  };
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
724
+ packageJson.devEngines = {
725
+ packageManager: { name: pm, version: pm === 'pnpm' ? '>=11 <13' : '>=10', onFail: 'error' },
726
+ };
727
+ delete packageJson.packageManager;
728
+ // Keep package.json workspaces for npm-based CI/CD even when local development uses pnpm.
729
+ const workspacePackages = usesNodeFunctionsProject(backendLanguage) ? ['shared', 'functions'] : ['shared'];
730
+ const wsConfig = (0, package_manager_1.getWorkspaceConfig)(pm, workspacePackages);
731
+ packageJson.workspaces = workspacePackages;
732
+ if (wsConfig.type !== 'file') {
323
733
  packageJson.workspaces = wsConfig.value;
324
734
  }
325
735
  fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
326
- // Create workspace config file if needed (pnpm-workspace.yaml)
736
+ // Create pnpm-workspace.yaml for local pnpm development when needed.
327
737
  if (wsConfig.type === 'file') {
328
738
  fs.writeFileSync(path.join(projectDir, wsConfig.filename), wsConfig.content);
329
739
  }
@@ -336,11 +746,9 @@ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig,
336
746
  }
337
747
  if (fs.existsSync(nextConfigPath)) {
338
748
  let nextConfigContent = fs.readFileSync(nextConfigPath, 'utf-8');
339
- // Add output: 'standalone', experimental.turbopackUseSystemTlsCerts, and serverExternalPackages
749
+ // Add output, transpiled workspace package, and server externals for standalone deployment
340
750
  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`);
751
+ nextConfigContent = injectSwallowKitNextConfig(nextConfigContent, projectName);
344
752
  fs.writeFileSync(nextConfigPath, nextConfigContent);
345
753
  }
346
754
  }
@@ -360,17 +768,7 @@ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig,
360
768
  fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
361
769
  }
362
770
  // 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
- `;
771
+ const swallowkitConfig = buildSwallowKitConfigSource(backendLanguage);
374
772
  fs.writeFileSync(path.join(projectDir, 'swallowkit.config.js'), swallowkitConfig);
375
773
  // 4. Create shared workspace package for Zod models (Single Source of Truth)
376
774
  await createSharedPackage(projectDir, projectName);
@@ -382,7 +780,14 @@ module.exports = {
382
780
  // Create backend utility for calling Azure Functions
383
781
  const backendUtilContent = `// Get Functions base URL at runtime (not at build time)
384
782
  function getFunctionsBaseUrl(): string {
385
- return process.env.BACKEND_FUNCTIONS_BASE_URL || 'http://localhost:7071';
783
+ return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
784
+ }
785
+
786
+ function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string> {
787
+ const key = process.env.BACKEND_FUNCTIONS_KEY;
788
+ if (key) return { 'x-functions-key': key };
789
+ if (functionsBaseUrl.startsWith('http://localhost:') || functionsBaseUrl.startsWith('http://127.0.0.1:')) return {};
790
+ throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
386
791
  }
387
792
 
388
793
  /**
@@ -392,10 +797,11 @@ function getFunctionsBaseUrl(): string {
392
797
  async function request<T>(
393
798
  endpoint: string,
394
799
  method: 'GET' | 'POST' | 'PUT' | 'DELETE',
395
- body?: any,
800
+ body?: unknown,
396
801
  queryParams?: Record<string, string>
397
802
  ): Promise<T> {
398
803
  const functionsBaseUrl = getFunctionsBaseUrl();
804
+ const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
399
805
  let url = \`\${functionsBaseUrl}\${endpoint}\`;
400
806
  if (queryParams) {
401
807
  const params = new URLSearchParams(queryParams);
@@ -407,6 +813,7 @@ async function request<T>(
407
813
  method,
408
814
  headers: {
409
815
  'Content-Type': 'application/json',
816
+ ...keyHeaders,
410
817
  },
411
818
  body: body ? JSON.stringify(body) : undefined,
412
819
  });
@@ -462,14 +869,14 @@ export const api = {
462
869
  /**
463
870
  * Make a POST request
464
871
  */
465
- post: <T>(endpoint: string, body?: any): Promise<T> => {
872
+ post: <T>(endpoint: string, body?: unknown): Promise<T> => {
466
873
  return request<T>(endpoint, 'POST', body);
467
874
  },
468
875
 
469
876
  /**
470
877
  * Make a PUT request
471
878
  */
472
- put: <T>(endpoint: string, body?: any): Promise<T> => {
879
+ put: <T>(endpoint: string, body?: unknown): Promise<T> => {
473
880
  return request<T>(endpoint, 'PUT', body);
474
881
  },
475
882
 
@@ -487,7 +894,8 @@ export const api = {
487
894
  fs.mkdirSync(componentsDir, { recursive: true });
488
895
  // 6. Create .env.example
489
896
  const envExample = `# Azure Functions Backend URL
490
- FUNCTIONS_BASE_URL=http://localhost:7071
897
+ BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071
898
+ BACKEND_FUNCTIONS_KEY=
491
899
 
492
900
  # Azure Configuration
493
901
  AZURE_RESOURCE_GROUP=your-resource-group
@@ -526,7 +934,7 @@ export async function register() {
526
934
  const originalConsoleError = console.error;
527
935
  const originalConsoleWarn = console.warn;
528
936
 
529
- console.log = function(...args: any[]) {
937
+ console.log = function(...args: unknown[]) {
530
938
  originalConsoleLog.apply(console, args);
531
939
  const message = args.map(arg =>
532
940
  typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
@@ -537,7 +945,7 @@ export async function register() {
537
945
  });
538
946
  };
539
947
 
540
- console.error = function(...args: any[]) {
948
+ console.error = function(...args: unknown[]) {
541
949
  originalConsoleError.apply(console, args);
542
950
  const message = args.map(arg =>
543
951
  typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
@@ -548,7 +956,7 @@ export async function register() {
548
956
  });
549
957
  };
550
958
 
551
- console.warn = function(...args: any[]) {
959
+ console.warn = function(...args: unknown[]) {
552
960
  originalConsoleWarn.apply(console, args);
553
961
  const message = args.map(arg =>
554
962
  typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
@@ -570,7 +978,8 @@ export async function register() {
570
978
  // 8. Create .env.local for local development
571
979
  const envLocalContent = [
572
980
  '# Azure Functions Backend URL (Local)',
573
- 'FUNCTIONS_BASE_URL=http://localhost:7071',
981
+ 'BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071',
982
+ 'BACKEND_FUNCTIONS_KEY=',
574
983
  ''
575
984
  ].join('\n');
576
985
  fs.writeFileSync(path.join(projectDir, '.env.local'), envLocalContent);
@@ -596,7 +1005,7 @@ export async function register() {
596
1005
  };
597
1006
  fs.writeFileSync(path.join(projectDir, 'staticwebapp.config.json'), JSON.stringify(swaConfig, null, 2));
598
1007
  // 14. Create Azure Functions project
599
- await createAzureFunctionsProject(projectDir, pm);
1008
+ await createAzureFunctionsProject(projectDir, pm, backendLanguage);
600
1009
  // 15. Create BFF API route to call Azure Functions
601
1010
  await createBffApiRoute(projectDir);
602
1011
  // 16. Create home page
@@ -606,9 +1015,9 @@ export async function register() {
606
1015
  await installDependencies(projectDir, pm);
607
1016
  console.log('✅ Project structure created\n');
608
1017
  // 18. Create README.md
609
- createReadme(projectDir, projectName, cicdChoice, azureConfig, pm);
1018
+ createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage);
610
1019
  // 19. Create AI agent instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
611
- createAiAgentFiles(projectDir, projectName);
1020
+ createAiAgentFiles(projectDir, projectName, backendLanguage, pm);
612
1021
  }
613
1022
  async function createSharedPackage(projectDir, projectName) {
614
1023
  console.log('📦 Creating shared workspace package for Zod models...\n');
@@ -617,6 +1026,7 @@ async function createSharedPackage(projectDir, projectName) {
617
1026
  fs.mkdirSync(modelsDir, { recursive: true });
618
1027
  // shared/package.json
619
1028
  const sharedPackageJson = {
1029
+ private: true,
620
1030
  name: `@${projectName}/shared`,
621
1031
  version: '1.0.0',
622
1032
  description: 'Shared Zod models — Single Source of Truth for validation schemas',
@@ -635,65 +1045,131 @@ async function createSharedPackage(projectDir, projectName) {
635
1045
  };
636
1046
  fs.writeFileSync(path.join(sharedDir, 'package.json'), JSON.stringify(sharedPackageJson, null, 2));
637
1047
  // 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));
1048
+ fs.writeFileSync(path.join(sharedDir, 'tsconfig.json'), JSON.stringify(buildSharedTsConfig(), null, 2));
658
1049
  // shared/index.ts (empty re-export file, scaffold will add entries)
659
1050
  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
1051
  // shared/.gitignore
661
1052
  fs.writeFileSync(path.join(sharedDir, '.gitignore'), `node_modules\ndist\n`);
662
1053
  console.log('✅ Shared package created\n');
663
1054
  }
664
- async function createAzureFunctionsProject(projectDir, pm = 'pnpm') {
665
- console.log('📦 Creating Azure Functions project...\n');
1055
+ async function createAzureFunctionsProject(projectDir, pm = 'pnpm', backendLanguage = 'typescript') {
1056
+ console.log(`📦 Creating Azure Functions project (${getBackendLanguageLabel(backendLanguage)})...\n`);
666
1057
  const functionsDir = path.join(projectDir, 'functions');
667
1058
  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)
1059
+ const projectName = path.basename(projectDir);
1060
+ const databaseName = `${projectName.charAt(0).toUpperCase() + projectName.slice(1)}Database`;
1061
+ createFunctionsHostFiles(functionsDir, databaseName, backendLanguage);
1062
+ if (backendLanguage === 'typescript') {
1063
+ createTypeScriptFunctionsProject(projectDir, functionsDir, pm);
1064
+ }
1065
+ else if (backendLanguage === 'csharp') {
1066
+ createCSharpFunctionsProject(projectDir, functionsDir);
1067
+ }
1068
+ else {
1069
+ createPythonFunctionsProject(projectDir, functionsDir);
1070
+ }
1071
+ console.log('✅ Azure Functions project created\n');
1072
+ }
1073
+ function createFunctionsHostFiles(functionsDir, databaseName, backendLanguage) {
1074
+ const hostJson = {
1075
+ version: '2.0',
1076
+ logging: {
1077
+ applicationInsights: {
1078
+ samplingSettings: {
1079
+ isEnabled: true,
1080
+ maxTelemetryItemsPerSecond: 20,
1081
+ },
1082
+ },
678
1083
  },
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`]: '*',
1084
+ extensionBundle: {
1085
+ id: 'Microsoft.Azure.Functions.ExtensionBundle',
1086
+ version: '[4.0.0, 4.10.0)',
685
1087
  },
686
- devDependencies: {
687
- '@types/node': '^20.0.0',
688
- 'typescript': '^5.0.0'
689
- }
690
1088
  };
691
- fs.writeFileSync(path.join(functionsDir, 'package.json'), JSON.stringify(functionsPackageJson, null, 2));
692
- // Create functions tsconfig.json
693
- const sharedPkgName = `@${path.basename(projectDir)}/shared`;
694
- const functionsTsConfig = {
695
- compilerOptions: {
696
- target: 'ES2020',
1089
+ fs.writeFileSync(path.join(functionsDir, 'host.json'), JSON.stringify(hostJson, null, 2));
1090
+ const localSettings = {
1091
+ IsEncrypted: false,
1092
+ Values: {
1093
+ AzureWebJobsStorage: '',
1094
+ FUNCTIONS_WORKER_RUNTIME: getFunctionsWorkerRuntime(backendLanguage),
1095
+ AzureWebJobsFeatureFlags: 'EnableWorkerIndexing',
1096
+ CosmosDBConnection: 'AccountEndpoint=http://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==',
1097
+ COSMOS_DB_DATABASE_NAME: databaseName,
1098
+ NODE_TLS_REJECT_UNAUTHORIZED: '0',
1099
+ },
1100
+ };
1101
+ fs.writeFileSync(path.join(functionsDir, 'local.settings.json'), JSON.stringify(localSettings, null, 2));
1102
+ const gitignoreLines = [
1103
+ 'local.settings.json',
1104
+ '*.log',
1105
+ '.vscode',
1106
+ '.DS_Store',
1107
+ ];
1108
+ if (backendLanguage === 'typescript') {
1109
+ gitignoreLines.unshift('node_modules', 'dist');
1110
+ fs.writeFileSync(path.join(functionsDir, '.funcignore'), `node_modules
1111
+ .git
1112
+ .vscode
1113
+ local.settings.json
1114
+ test
1115
+ tsconfig.json
1116
+ *.ts
1117
+ !dist/**/*.js
1118
+ `);
1119
+ }
1120
+ else if (backendLanguage === 'python') {
1121
+ gitignoreLines.unshift('.venv', '.codegen-venv', '__pycache__', '.python_packages');
1122
+ fs.writeFileSync(path.join(functionsDir, '.funcignore'), `.venv
1123
+ .codegen-venv
1124
+ __pycache__
1125
+ .pytest_cache
1126
+ .mypy_cache
1127
+ .ruff_cache
1128
+ local.settings.json
1129
+ tests
1130
+ `);
1131
+ }
1132
+ else {
1133
+ gitignoreLines.unshift('bin', 'obj');
1134
+ fs.writeFileSync(path.join(functionsDir, '.funcignore'), `bin
1135
+ obj
1136
+ local.settings.json
1137
+ tests
1138
+ `);
1139
+ }
1140
+ fs.writeFileSync(path.join(functionsDir, '.gitignore'), `${gitignoreLines.join('\n')}\n`);
1141
+ }
1142
+ function buildTypeScriptFunctionsPackageJson(projectName, pm = 'pnpm') {
1143
+ return {
1144
+ name: 'functions',
1145
+ version: '1.0.0',
1146
+ description: 'Azure Functions backend',
1147
+ main: 'dist/*.js',
1148
+ scripts: {
1149
+ start: 'func start',
1150
+ build: 'tsc',
1151
+ prestart: (0, package_manager_1.getFunctionsPrestart)(pm),
1152
+ },
1153
+ dependencies: {
1154
+ '@azure/functions': '~4.5.0',
1155
+ '@azure/cosmos': '^4.0.0',
1156
+ '@azure/identity': '^4.0.0',
1157
+ zod: '>=3.25.0',
1158
+ [`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:../shared',
1159
+ },
1160
+ devDependencies: {
1161
+ '@types/node': '^20.0.0',
1162
+ typescript: '^5.0.0',
1163
+ },
1164
+ };
1165
+ }
1166
+ function createTypeScriptFunctionsProject(projectDir, functionsDir, pm) {
1167
+ const functionsPackageJson = buildTypeScriptFunctionsPackageJson(path.basename(projectDir), pm);
1168
+ fs.writeFileSync(path.join(functionsDir, 'package.json'), JSON.stringify(functionsPackageJson, null, 2));
1169
+ const sharedPkgName = `@${path.basename(projectDir)}/shared`;
1170
+ const functionsTsConfig = {
1171
+ compilerOptions: {
1172
+ target: 'ES2020',
697
1173
  module: 'commonjs',
698
1174
  moduleResolution: 'node',
699
1175
  lib: ['ES2020'],
@@ -709,69 +1185,14 @@ async function createAzureFunctionsProject(projectDir, pm = 'pnpm') {
709
1185
  },
710
1186
  },
711
1187
  include: ['src/**/*'],
712
- exclude: ['node_modules', 'dist']
1188
+ exclude: ['node_modules', 'dist'],
713
1189
  };
714
1190
  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
1191
  const srcDir = path.join(functionsDir, 'src');
769
1192
  fs.mkdirSync(srcDir, { recursive: true });
770
- // Create greet function directly in src
771
- const greetFunction = `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
1193
+ fs.writeFileSync(path.join(srcDir, 'greet.ts'), `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
772
1194
  import { z } from 'zod/v4';
773
1195
 
774
- // Zod schema for request validation
775
1196
  const greetRequestSchema = z.object({
776
1197
  name: z.string().min(1, 'Name is required').max(50, 'Name must be less than 50 characters'),
777
1198
  });
@@ -780,12 +1201,9 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
780
1201
  context.log('HTTP trigger function processed a request.');
781
1202
 
782
1203
  try {
783
- // Get name from query or body
784
1204
  const name = request.query.get('name') || (await request.text());
785
-
786
- // Validate with Zod
787
1205
  const result = greetRequestSchema.safeParse({ name });
788
-
1206
+
789
1207
  if (!result.success) {
790
1208
  return {
791
1209
  status: 400,
@@ -796,7 +1214,7 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
796
1214
  }
797
1215
 
798
1216
  const greeting = \`Hello, \${result.data.name}! This message is from Azure Functions.\`;
799
-
1217
+
800
1218
  return {
801
1219
  status: 200,
802
1220
  jsonBody: {
@@ -817,13 +1235,109 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
817
1235
 
818
1236
  app.http('greet', {
819
1237
  methods: ['GET', 'POST'],
820
- authLevel: 'anonymous',
1238
+ authLevel: 'function',
821
1239
  handler: greet
822
1240
  });
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');
1241
+ `);
1242
+ }
1243
+ function createCSharpFunctionsProject(projectDir, functionsDir) {
1244
+ const projectBaseName = path.basename(projectDir);
1245
+ const projectPascal = projectBaseName.charAt(0).toUpperCase() + projectBaseName.slice(1);
1246
+ const csprojName = `${projectPascal}.Functions.csproj`;
1247
+ fs.writeFileSync(path.join(functionsDir, csprojName), buildCSharpFunctionsProjectSource());
1248
+ fs.mkdirSync(path.join(functionsDir, '.config'), { recursive: true });
1249
+ fs.writeFileSync(path.join(functionsDir, '.config', 'dotnet-tools.json'), (0, native_schema_generator_1.buildCSharpCodegenToolManifestSource)());
1250
+ fs.writeFileSync(path.join(functionsDir, 'Program.cs'), buildCSharpFunctionsProgramSource());
1251
+ const crudDir = path.join(functionsDir, 'Crud');
1252
+ fs.mkdirSync(crudDir, { recursive: true });
1253
+ fs.writeFileSync(path.join(crudDir, 'GreetFunction.cs'), `using System.Net;
1254
+ using Microsoft.Azure.Functions.Worker;
1255
+ using Microsoft.Azure.Functions.Worker.Http;
1256
+
1257
+ namespace SwallowKit.Functions;
1258
+
1259
+ public sealed class GreetFunction
1260
+ {
1261
+ [Function("greet")]
1262
+ public async Task<HttpResponseData> Run(
1263
+ [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "greet")] HttpRequestData request)
1264
+ {
1265
+ var query = request.Url.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
1266
+ var name = "SwallowKit";
1267
+ foreach (var segment in query)
1268
+ {
1269
+ var parts = segment.Split('=', 2);
1270
+ if (parts.Length == 2 && parts[0] == "name")
1271
+ {
1272
+ name = Uri.UnescapeDataString(parts[1]);
1273
+ break;
1274
+ }
1275
+ }
1276
+ var response = request.CreateResponse(HttpStatusCode.OK);
1277
+ await response.WriteAsJsonAsync(new
1278
+ {
1279
+ message = $"Hello, {name}! This message is from Azure Functions.",
1280
+ timestamp = DateTimeOffset.UtcNow.ToString("O"),
1281
+ });
1282
+ return response;
1283
+ }
1284
+ }
1285
+ `);
1286
+ }
1287
+ function createPythonFunctionsProject(projectDir, functionsDir) {
1288
+ fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
1289
+ ensureProjectGitignoreEntry(projectDir, '.uv');
1290
+ fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
1291
+ azure-cosmos>=4.9.0
1292
+ azure-identity>=1.19.0
1293
+ `);
1294
+ fs.writeFileSync(path.join(functionsDir, 'requirements.codegen.txt'), (0, native_schema_generator_1.buildPythonCodegenRequirementsSource)());
1295
+ const blueprintsDir = path.join(functionsDir, 'blueprints');
1296
+ fs.mkdirSync(blueprintsDir, { recursive: true });
1297
+ fs.writeFileSync(path.join(blueprintsDir, '__init__.py'), '');
1298
+ fs.writeFileSync(path.join(blueprintsDir, 'greet.py'), `import json
1299
+ from datetime import datetime, timezone
1300
+
1301
+ import azure.functions as func
1302
+
1303
+ bp = func.Blueprint()
1304
+
1305
+
1306
+ @bp.route(route="greet", methods=["GET", "POST"])
1307
+ def greet(req: func.HttpRequest) -> func.HttpResponse:
1308
+ name = req.params.get("name") or "SwallowKit"
1309
+ payload = {
1310
+ "message": f"Hello, {name}! This message is from Azure Functions.",
1311
+ "timestamp": datetime.now(timezone.utc).isoformat(),
1312
+ }
1313
+ return func.HttpResponse(
1314
+ body=json.dumps(payload, ensure_ascii=False),
1315
+ status_code=200,
1316
+ mimetype="application/json",
1317
+ )
1318
+ `);
1319
+ fs.writeFileSync(path.join(functionsDir, 'function_app.py'), `import azure.functions as func
1320
+
1321
+ from blueprints.greet import bp as greet_bp
1322
+
1323
+ app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
1324
+
1325
+ app.register_blueprint(greet_bp)
1326
+ # SwallowKit scaffold registrations
1327
+ `);
1328
+ }
1329
+ function ensureProjectGitignoreEntry(projectDir, entry) {
1330
+ const gitignorePath = path.join(projectDir, '.gitignore');
1331
+ if (!fs.existsSync(gitignorePath)) {
1332
+ return;
1333
+ }
1334
+ const current = fs.readFileSync(gitignorePath, 'utf8');
1335
+ const lines = current.split(/\r?\n/);
1336
+ if (lines.includes(entry)) {
1337
+ return;
1338
+ }
1339
+ const normalized = current.endsWith('\n') ? current : `${current}\n`;
1340
+ fs.writeFileSync(gitignorePath, `${normalized}${entry}\n`);
827
1341
  }
828
1342
  async function createBffApiRoute(projectDir) {
829
1343
  console.log('📦 Creating BFF API route...\n');
@@ -874,17 +1388,17 @@ export async function POST(request: NextRequest) {
874
1388
  }
875
1389
  `;
876
1390
  fs.writeFileSync(path.join(apiDir, 'route.ts'), apiRoute);
877
- // Update .env.example to include FUNCTIONS_BASE_URL
1391
+ // Update .env.example to include BACKEND_FUNCTIONS_BASE_URL
878
1392
  const envExamplePath = path.join(projectDir, '.env.example');
879
1393
  let envExample = fs.readFileSync(envExamplePath, 'utf-8');
880
- if (!envExample.includes('FUNCTIONS_BASE_URL')) {
1394
+ if (!envExample.includes('BACKEND_FUNCTIONS_BASE_URL')) {
881
1395
  envExample += `\n# Azure Functions Backend URL\nBACKEND_FUNCTIONS_BASE_URL=http://localhost:7071\n`;
882
1396
  fs.writeFileSync(envExamplePath, envExample);
883
1397
  }
884
1398
  // Update .env.local
885
1399
  const envLocalPath = path.join(projectDir, '.env.local');
886
1400
  let envLocal = fs.readFileSync(envLocalPath, 'utf-8');
887
- if (!envLocal.includes('FUNCTIONS_BASE_URL')) {
1401
+ if (!envLocal.includes('BACKEND_FUNCTIONS_BASE_URL')) {
888
1402
  envLocal += `\n# Azure Functions Backend URL (Local)\nBACKEND_FUNCTIONS_BASE_URL=http://localhost:7071\n`;
889
1403
  fs.writeFileSync(envLocalPath, envLocal);
890
1404
  }
@@ -985,7 +1499,7 @@ export default function Home() {
985
1499
  Create your first model with Zod and generate CRUD operations automatically.
986
1500
  </p>
987
1501
  <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
1502
+ ${pmCmd.exec} swallowkit scaffold shared/models/your-model.ts
989
1503
  </code>
990
1504
  </div>
991
1505
  </section>
@@ -1021,13 +1535,29 @@ export const scaffoldConfig = {
1021
1535
  fs.writeFileSync(path.join(scaffoldConfigDir, 'scaffold-config.ts'), scaffoldConfigContent);
1022
1536
  console.log('✅ Scaffold config created\n');
1023
1537
  }
1024
- function createReadme(projectDir, projectName, cicdChoice, azureConfig, pm) {
1538
+ function createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage) {
1025
1539
  console.log('📝 Creating README.md...\n');
1026
1540
  const pmCmd = (0, package_manager_1.getCommands)(pm);
1027
1541
  const cosmosDbModeLabel = azureConfig.cosmosDbMode === 'freetier' ? 'Free Tier (1000 RU/s)' : 'Serverless';
1028
1542
  const cicdLabel = cicdChoice === 'github' ? 'GitHub Actions' : cicdChoice === 'azure' ? 'Azure Pipelines' : 'None';
1029
1543
  const vnetLabel = azureConfig.vnetOption === 'none' ? 'None (public endpoints)' :
1030
1544
  'Outbound VNet (Cosmos DB Private Endpoint)';
1545
+ const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
1546
+ const schemaBridgeDescription = backendLanguage === 'typescript'
1547
+ ? 'Zod (shared between frontend and backend)'
1548
+ : `Zod + OpenAPI export (Zod in shared/, native-generated ${backendLanguageLabel} schemas in functions/generated/)`;
1549
+ const functionsTree = backendLanguage === 'typescript'
1550
+ ? `│ └── src/\n│ └── greet.ts # Sample function`
1551
+ : backendLanguage === 'csharp'
1552
+ ? `│ ├── Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ # Native-generated C# schema assets`
1553
+ : `│ ├── blueprints/\n│ │ └── greet.py\n│ └── generated/ # Native-generated Python schema assets`;
1554
+ const backendScaffoldNote = backendLanguage === 'typescript'
1555
+ ? '- Azure Functions CRUD endpoints'
1556
+ : `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI export + native-generated ${backendLanguageLabel} schema assets`;
1557
+ const pythonUvPaths = (0, python_uv_1.getProjectLocalUvPaths)(projectDir);
1558
+ const pythonLocalDevNote = backendLanguage === 'python'
1559
+ ? `\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`
1560
+ : '';
1031
1561
  const readme = `# ${projectName}
1032
1562
 
1033
1563
  A full-stack application built with **SwallowKit** - Next.js on Azure Static Web Apps + Functions + Cosmos DB with Zod schema sharing.
@@ -1036,9 +1566,9 @@ A full-stack application built with **SwallowKit** - Next.js on Azure Static Web
1036
1566
 
1037
1567
  - **Frontend**: Next.js 15 (App Router), React, TypeScript, Tailwind CSS
1038
1568
  - **BFF (Backend for Frontend)**: Next.js API Routes
1039
- - **Backend**: Azure Functions (TypeScript)
1569
+ - **Backend**: Azure Functions (${backendLanguageLabel})
1040
1570
  - **Database**: Azure Cosmos DB
1041
- - **Schema Validation**: Zod (shared between frontend and backend)
1571
+ - **Schema Validation**: ${schemaBridgeDescription}
1042
1572
  - **Infrastructure**: Bicep (Infrastructure as Code)
1043
1573
  - **CI/CD**: ${cicdLabel}
1044
1574
 
@@ -1055,7 +1585,7 @@ This project was initialized with the following settings:
1055
1585
 
1056
1586
  Before you begin, ensure you have the following installed:
1057
1587
 
1058
- 1. **Node.js 18+**: [Download](https://nodejs.org/)${pm === 'pnpm' ? `\n2. **pnpm**: \`corepack enable\` or \`npm install -g pnpm\`` : ''}
1588
+ 1. **Node.js 22.14+**: [Download](https://nodejs.org/)${pm === 'pnpm' ? `\n2. **pnpm**: \`corepack enable\` or \`npm install -g pnpm\`` : ''}
1059
1589
  ${pm === 'pnpm' ? '3' : '2'}. **Azure CLI**: Required for provisioning Azure resources
1060
1590
  - Install: \`winget install Microsoft.AzureCLI\` (Windows)
1061
1591
  - Or: [Download](https://aka.ms/installazurecliwindows)
@@ -1063,7 +1593,7 @@ ${pm === 'pnpm' ? '4' : '3'}. **Azure Cosmos DB Emulator**: Required for local d
1063
1593
  - Windows: \`winget install Microsoft.Azure.CosmosEmulator\`
1064
1594
  - Or: [Download](https://aka.ms/cosmosdb-emulator)
1065
1595
  - Docker: \`docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator\`
1066
- ${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools**: Automatically installed with project dependencies
1596
+ ${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools v4**: Install separately for local Azure Functions execution
1067
1597
 
1068
1598
  ## 📁 Project Structure
1069
1599
 
@@ -1073,11 +1603,8 @@ ${projectName}/
1073
1603
  │ ├── api/ # BFF API routes (proxy to Functions)
1074
1604
  │ └── page.tsx # Home page
1075
1605
  ├── functions/ # Azure Functions (backend)
1076
- │ └── src/
1077
- │ ├── models/ # Data models (copied from lib/models)
1078
- │ └── hello.ts # Sample function
1606
+ ${functionsTree}
1079
1607
  ├── lib/
1080
- │ ├── models/ # Shared Zod schemas
1081
1608
  │ └── api/ # API client utilities
1082
1609
  ├── infra/ # Bicep infrastructure files
1083
1610
  │ ├── main.bicep
@@ -1092,21 +1619,21 @@ ${projectName}/
1092
1619
  Define your data model with Zod schema:
1093
1620
 
1094
1621
  \`\`\`bash
1095
- ${pmCmd.dlx} swallowkit create-model <model-name>
1622
+ ${pmCmd.exec} swallowkit create-model <model-name>
1096
1623
  \`\`\`
1097
1624
 
1098
- This creates a model file in \`lib/models/<model-name>.ts\`. Edit it to define your schema.
1625
+ This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to define your schema.
1099
1626
 
1100
1627
  ### 2. Generate CRUD Code
1101
1628
 
1102
1629
  Generate complete CRUD operations (Functions, API routes, UI):
1103
1630
 
1104
1631
  \`\`\`bash
1105
- ${pmCmd.dlx} swallowkit scaffold lib/models/<model-name>.ts
1632
+ ${pmCmd.exec} swallowkit scaffold shared/models/<model-name>.ts
1106
1633
  \`\`\`
1107
1634
 
1108
1635
  This generates:
1109
- - Azure Functions CRUD endpoints
1636
+ ${backendScaffoldNote}
1110
1637
  - Next.js BFF API routes
1111
1638
  - React UI components (list, detail, create, edit)
1112
1639
  - Navigation menu integration
@@ -1114,7 +1641,7 @@ This generates:
1114
1641
  ### 3. Start Development Servers
1115
1642
 
1116
1643
  \`\`\`bash
1117
- ${pmCmd.dlx} swallowkit dev
1644
+ ${pmCmd.exec} swallowkit dev
1118
1645
  \`\`\`
1119
1646
 
1120
1647
  This starts:
@@ -1123,6 +1650,7 @@ This starts:
1123
1650
  - Cosmos DB Emulator check (must be running separately)
1124
1651
 
1125
1652
  **Note**: You need to start Cosmos DB Emulator manually before running \`swallowkit dev\`.
1653
+ ${pythonLocalDevNote}
1126
1654
 
1127
1655
  ## ☁️ Deploy to Azure
1128
1656
 
@@ -1131,7 +1659,7 @@ This starts:
1131
1659
  Create all required Azure resources using Bicep:
1132
1660
 
1133
1661
  \`\`\`bash
1134
- ${pmCmd.dlx} swallowkit provision --resource-group <rg-name>
1662
+ ${pmCmd.exec} swallowkit provision --resource-group <rg-name>
1135
1663
  \`\`\`
1136
1664
 
1137
1665
  This creates:
@@ -1187,10 +1715,10 @@ func azure functionapp publish func-${projectName}
1187
1715
 
1188
1716
  ## 🔧 Available Commands
1189
1717
 
1190
- - \`${pmCmd.dlx} swallowkit create-model <name>\` - Create a new data model
1191
- - \`${pmCmd.dlx} swallowkit scaffold <model-file>\` - Generate CRUD code
1192
- - \`${pmCmd.dlx} swallowkit dev\` - Start development servers
1193
- - \`${pmCmd.dlx} swallowkit provision -g <rg-name>\` - Provision Azure resources
1718
+ - \`${pmCmd.exec} swallowkit create-model <name>\` - Create a new data model
1719
+ - \`${pmCmd.exec} swallowkit scaffold <model-file>\` - Generate CRUD code
1720
+ - \`${pmCmd.exec} swallowkit dev\` - Start development servers
1721
+ - \`${pmCmd.exec} swallowkit provision -g <rg-name>\` - Provision Azure resources
1194
1722
  ${azureConfig.vnetOption !== 'none' ? `
1195
1723
  ## 🔒 Network Security (VNet Configuration)
1196
1724
 
@@ -1237,415 +1765,71 @@ This project was generated by SwallowKit. If you encounter any issues or have su
1237
1765
  fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
1238
1766
  console.log('✅ README.md created\n');
1239
1767
  }
1240
- function createAiAgentFiles(projectDir, projectName) {
1768
+ function createAiAgentFiles(projectDir, projectName, backendLanguage, pm) {
1241
1769
  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
- });
1770
+ const runCmd = (0, package_manager_1.getCommands)(pm).exec;
1771
+ (0, agent_instructions_1.writeAgentInstructionFiles)(projectDir, {
1772
+ projectName,
1773
+ backendLanguage,
1774
+ packageManager: pm,
1775
+ });
1776
+ console.log(' ✅ AGENTS.md (canonical project contract)');
1777
+ console.log(' ✅ CLAUDE.md (Claude Code adapter)');
1778
+ console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot adapter)');
1779
+ console.log(' ✅ .github/instructions/ (path-specific instructions)');
1780
+ fs.writeFileSync(path.join(projectDir, '.mcp.json'), buildSwallowKitMcpProjectConfigSource(pm));
1781
+ console.log(' ✅ .mcp.json (project-scoped MCP bootstrap)');
1782
+ (0, workflows_1.writeWorkflowDocs)(projectDir, runCmd);
1783
+ console.log(' ✅ .swallowkit/workflows/ (agent-agnostic autonomous-loop runbooks)');
1784
+ const skillPaths = (0, workflows_1.writeAgentSkills)(projectDir, runCmd);
1785
+ console.log(' ✅ .github/skills/ (Agent Skills — agentskills.io open standard)');
1786
+ for (const skillPath of skillPaths) {
1787
+ console.log(` - ${skillPath}`);
1788
+ }
1789
+ console.log('\n✅ AI agent files created\n');
1306
1790
  }
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
- });
1791
+ function buildFunctionsHostKeyBicepExpression() {
1792
+ return "listKeys('${resourceId('Microsoft.Web/sites', functionsAppName)}/host/default', '2023-12-01').functionKeys.default";
1318
1793
  }
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
- \`\`\`
1794
+ function buildStaticWebAppConfigBicepSource() {
1795
+ return `@description('Static Web App name')
1796
+ param staticWebAppName string
1514
1797
 
1515
- ## Key Rules
1798
+ @description('Functions App default hostname for backend API calls')
1799
+ param functionsDefaultHostname string
1516
1800
 
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.
1801
+ @description('Functions App name used to resolve its host key after deployment')
1802
+ param functionsAppName string
1521
1803
 
1522
- ## Naming
1804
+ @description('Application Insights connection string for SWA')
1805
+ param appInsightsConnectionString string
1523
1806
 
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\`
1807
+ resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
1808
+ name: staticWebAppName
1809
+ }
1527
1810
 
1528
- ## Managed Fields
1811
+ resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
1812
+ parent: staticWebApp
1813
+ name: 'appsettings'
1814
+ properties: {
1815
+ APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
1816
+ ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
1817
+ BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
1818
+ BACKEND_FUNCTIONS_KEY: ${buildFunctionsHostKeyBicepExpression()}
1819
+ }
1820
+ }
1529
1821
 
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.
1822
+ output configName string = staticWebAppConfig.name
1559
1823
  `;
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
1824
  }
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) {
1825
+ async function createInfrastructure(projectDir, projectName, azureConfig, backendLanguage) {
1644
1826
  console.log('📦 Creating infrastructure files (Bicep)...\n');
1645
1827
  const infraDir = path.join(projectDir, 'infra');
1646
1828
  const modulesDir = path.join(infraDir, 'modules');
1647
1829
  fs.mkdirSync(modulesDir, { recursive: true });
1648
1830
  const enableVNet = azureConfig.vnetOption !== 'none';
1831
+ const functionsRuntime = getFunctionsRuntimeConfig(backendLanguage);
1832
+ const staticWebAppSku = getStaticWebAppSku(azureConfig.swaPlan);
1649
1833
  // main.bicep
1650
1834
  const mainBicep = `targetScope = 'resourceGroup'
1651
1835
 
@@ -1658,6 +1842,13 @@ param location string = resourceGroup().location
1658
1842
  @description('Location for Static Web App (must be explicitly provided)')
1659
1843
  param swaLocation string
1660
1844
 
1845
+ @description('Static Web Apps hosting plan')
1846
+ @allowed([
1847
+ 'Free'
1848
+ 'Standard'
1849
+ ])
1850
+ param staticWebAppSku string = '${staticWebAppSku}'
1851
+
1661
1852
  @description('Cosmos DB mode')
1662
1853
  @allowed(['freetier', 'serverless'])
1663
1854
  param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
@@ -1700,7 +1891,7 @@ module staticWebApp 'modules/staticwebapp.bicep' = {
1700
1891
  params: {
1701
1892
  name: 'swa-\${projectName}'
1702
1893
  location: swaLocation
1703
- sku: 'Standard'
1894
+ sku: staticWebAppSku
1704
1895
  appInsightsConnectionString: appInsightsSwa.outputs.connectionString
1705
1896
  }
1706
1897
  }
@@ -1741,35 +1932,30 @@ module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enabl
1741
1932
  params: {
1742
1933
  name: 'pe-cosmos-\${projectName}'
1743
1934
  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
1935
+ cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.id : cosmosDbServerless!.outputs.id
1936
+ cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.accountName : cosmosDbServerless!.outputs.accountName
1937
+ subnetId: vnet!.outputs.privateEndpointSubnetId
1938
+ vnetId: vnet!.outputs.id
1748
1939
  }
1749
- dependsOn: [
1750
- cosmosDbFreeTier
1751
- cosmosDbServerless
1752
- vnet
1753
- ]
1754
1940
  }
1755
1941
 
1756
1942
  // Azure Functions (Flex Consumption) - Deploy AFTER Cosmos DB
1757
1943
  module functionsFlex 'modules/functions-flex.bicep' = {
1758
1944
  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 : ''
1945
+ params: {
1946
+ name: 'func-\${projectName}'
1947
+ location: location
1948
+ storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
1949
+ appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
1950
+ swaDefaultHostname: staticWebApp.outputs.defaultHostname
1951
+ cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.endpoint : cosmosDbServerless!.outputs.endpoint
1952
+ cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.databaseName : cosmosDbServerless!.outputs.databaseName
1953
+ functionsRuntimeName: '${functionsRuntime.name}'
1954
+ functionsRuntimeVersion: '${functionsRuntime.version}'
1955
+ enableVNet: enableVNet
1956
+ vnetSubnetId: enableVNet ? vnet!.outputs.functionsSubnetId : ''
1769
1957
  }
1770
1958
  dependsOn: [
1771
- cosmosDbFreeTier
1772
- cosmosDbServerless
1773
1959
  cosmosPrivateEndpoint
1774
1960
  ]
1775
1961
  }
@@ -1778,23 +1964,17 @@ module functionsFlex 'modules/functions-flex.bicep' = {
1778
1964
  module cosmosDbRoleAssignmentFreeTier 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'freetier') {
1779
1965
  name: 'cosmosDbRoleAssignment'
1780
1966
  params: {
1781
- cosmosAccountName: cosmosDbFreeTier.outputs.accountName
1967
+ cosmosAccountName: cosmosDbFreeTier!.outputs.accountName
1782
1968
  functionsPrincipalId: functionsFlex.outputs.principalId
1783
1969
  }
1784
- dependsOn: [
1785
- functionsFlex
1786
- ]
1787
1970
  }
1788
1971
 
1789
1972
  module cosmosDbRoleAssignmentServerless 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'serverless') {
1790
1973
  name: 'cosmosDbRoleAssignment'
1791
1974
  params: {
1792
- cosmosAccountName: cosmosDbServerless.outputs.accountName
1975
+ cosmosAccountName: cosmosDbServerless!.outputs.accountName
1793
1976
  functionsPrincipalId: functionsFlex.outputs.principalId
1794
1977
  }
1795
- dependsOn: [
1796
- functionsFlex
1797
- ]
1798
1978
  }
1799
1979
 
1800
1980
  // Update SWA config with Functions hostname (after Functions deployment)
@@ -1803,20 +1983,18 @@ module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
1803
1983
  params: {
1804
1984
  staticWebAppName: staticWebApp.outputs.name
1805
1985
  functionsDefaultHostname: functionsFlex.outputs.defaultHostname
1986
+ functionsAppName: functionsFlex.outputs.name
1806
1987
  appInsightsConnectionString: appInsightsSwa.outputs.connectionString
1807
1988
  }
1808
- dependsOn: [
1809
- functionsFlex
1810
- ]
1811
1989
  }
1812
1990
 
1813
1991
  output staticWebAppName string = staticWebApp.outputs.name
1814
1992
  output staticWebAppUrl string = staticWebApp.outputs.defaultHostname
1815
1993
  output functionsAppName string = functionsFlex.outputs.name
1816
1994
  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
1995
+ output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.accountName : cosmosDbServerless!.outputs.accountName
1996
+ output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.endpoint : cosmosDbServerless!.outputs.endpoint
1997
+ output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.databaseName : cosmosDbServerless!.outputs.databaseName
1820
1998
  output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
1821
1999
  output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
1822
2000
  output appInsightsSwaName string = appInsightsSwa.outputs.name
@@ -1824,7 +2002,7 @@ output appInsightsSwaConnectionString string = appInsightsSwa.outputs.connection
1824
2002
  output appInsightsFunctionsName string = appInsightsFunctions.outputs.name
1825
2003
  output appInsightsFunctionsConnectionString string = appInsightsFunctions.outputs.connectionString
1826
2004
  output vnetEnabled bool = enableVNet
1827
- output vnetName string = enableVNet ? vnet.outputs.name : ''
2005
+ output vnetName string = enableVNet ? vnet!.outputs.name : ''
1828
2006
  `;
1829
2007
  fs.writeFileSync(path.join(infraDir, 'main.bicep'), mainBicep);
1830
2008
  // main.parameters.json
@@ -1838,6 +2016,9 @@ output vnetName string = enableVNet ? vnet.outputs.name : ''
1838
2016
  "cosmosDbMode": {
1839
2017
  "value": "${azureConfig.cosmosDbMode}"
1840
2018
  },
2019
+ "staticWebAppSku": {
2020
+ "value": "${staticWebAppSku}"
2021
+ },
1841
2022
  "enableVNet": {
1842
2023
  "value": ${enableVNet}
1843
2024
  }
@@ -1892,38 +2073,14 @@ output defaultHostname string = staticWebApp.properties.defaultHostname
1892
2073
  `;
1893
2074
  fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
1894
2075
  // 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
2076
+ const staticWebAppConfigBicep = buildStaticWebAppConfigBicepSource();
2077
+ fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
2078
+ // modules/loganalytics.bicep (Shared Log Analytics Workspace)
2079
+ const logAnalyticsBicep = `@description('Log Analytics workspace name')
2080
+ param name string
1903
2081
 
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
- `;
1920
- fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
1921
- // modules/loganalytics.bicep (Shared Log Analytics Workspace)
1922
- const logAnalyticsBicep = `@description('Log Analytics workspace name')
1923
- param name string
1924
-
1925
- @description('Location for Log Analytics workspace')
1926
- param location string
2082
+ @description('Location for Log Analytics workspace')
2083
+ param location string
1927
2084
 
1928
2085
  resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
1929
2086
  name: name
@@ -1996,6 +2153,12 @@ param enableVNet bool = false
1996
2153
  @description('VNet subnet ID for Functions (required if enableVNet is true)')
1997
2154
  param vnetSubnetId string = ''
1998
2155
 
2156
+ @description('Functions runtime name')
2157
+ param functionsRuntimeName string
2158
+
2159
+ @description('Functions runtime version')
2160
+ param functionsRuntimeVersion string
2161
+
1999
2162
  // Storage Account for Functions
2000
2163
  resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
2001
2164
  name: storageAccountName
@@ -2066,8 +2229,8 @@ resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
2066
2229
  instanceMemoryMB: 2048
2067
2230
  }
2068
2231
  runtime: {
2069
- name: 'node'
2070
- version: '22'
2232
+ name: functionsRuntimeName
2233
+ version: functionsRuntimeVersion
2071
2234
  }
2072
2235
  }
2073
2236
  siteConfig: {
@@ -2129,63 +2292,7 @@ output principalId string = functionApp.identity.principalId
2129
2292
  `;
2130
2293
  fs.writeFileSync(path.join(modulesDir, 'functions-flex.bicep'), functionsFlexBicep);
2131
2294
  // 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
- `;
2295
+ const cosmosDbFreeTierBicep = buildCosmosDbFreeTierBicepSource();
2189
2296
  fs.writeFileSync(path.join(modulesDir, 'cosmosdb-freetier.bicep'), cosmosDbFreeTierBicep);
2190
2297
  // modules/cosmosdb-serverless.bicep (Serverless)
2191
2298
  const cosmosDbServerlessBicep = `@description('Cosmos DB account name')
@@ -2275,10 +2382,11 @@ resource roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignment
2275
2382
  output roleAssignmentId string = roleAssignment.id
2276
2383
  `;
2277
2384
  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')
2385
+ // Bicep resolves module files at compile time even when their deployment condition is false,
2386
+ // so these definitions must always be generated. main.bicep still controls deployment with
2387
+ // `if (enableVNet)`.
2388
+ // modules/vnet.bicep
2389
+ const vnetBicep = `@description('VNet name')
2282
2390
  param name string
2283
2391
 
2284
2392
  @description('Location for VNet')
@@ -2324,9 +2432,9 @@ output name string = vnet.name
2324
2432
  output functionsSubnetId string = vnet.properties.subnets[0].id
2325
2433
  output privateEndpointSubnetId string = vnet.properties.subnets[1].id
2326
2434
  `;
2327
- fs.writeFileSync(path.join(modulesDir, 'vnet.bicep'), vnetBicep);
2328
- // modules/private-endpoint-cosmos.bicep
2329
- const cosmosPrivateEndpointBicep = `@description('Private endpoint name')
2435
+ fs.writeFileSync(path.join(modulesDir, 'vnet.bicep'), vnetBicep);
2436
+ // modules/private-endpoint-cosmos.bicep
2437
+ const cosmosPrivateEndpointBicep = `@description('Private endpoint name')
2330
2438
  param name string
2331
2439
 
2332
2440
  @description('Location')
@@ -2404,73 +2512,85 @@ resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneG
2404
2512
  output privateEndpointId string = privateEndpoint.id
2405
2513
  output privateDnsZoneId string = privateDnsZone.id
2406
2514
  `;
2407
- fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
2408
- console.log('✅ VNet modules created\n');
2409
- }
2515
+ fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
2516
+ console.log('✅ VNet module definitions created\n');
2410
2517
  console.log('✅ Infrastructure files created\n');
2411
2518
  }
2412
- async function createGitHubActionsWorkflows(projectDir, azureConfig, pm) {
2413
- console.log('📦 Creating GitHub Actions workflows...\n');
2519
+ function getGitHubFunctionsWorkflow(pm, backendLanguage, projectPm = pm) {
2414
2520
  const pmCmd = (0, package_manager_1.getCommands)(pm);
2415
2521
  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
2522
+ const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
2523
+ const commonSetup = ` - uses: actions/checkout@v4
2524
+
2525
+ ${pnpmSetupStep ? `${pnpmSetupStep}\n` : ''}
2526
+ - name: Setup Node.js
2527
+ uses: actions/setup-node@v4
2528
+ with:
2529
+ node-version: '22'
2530
+ cache: ${pm}
2531
+ cache-dependency-path: ${pm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}
2532
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
2533
+ - name: Install dependencies
2534
+ run: |
2535
+ ${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
2536
+
2537
+ - name: Build shared package
2538
+ run: |
2539
+ ${pmCmd.runFilter('shared')} build
2540
+ `;
2541
+ if (backendLanguage === 'typescript') {
2542
+ return `name: Deploy Azure Functions
2420
2543
 
2421
2544
  on:
2422
2545
  push:
2423
2546
  branches:
2424
2547
  - main
2425
2548
  paths:
2426
- - 'app/**'
2427
- - 'components/**'
2428
- - 'lib/**'
2549
+ - 'functions/**'
2429
2550
  - 'shared/**'
2430
- - 'public/**'
2431
- - 'package.json'
2432
- - 'next.config.js'
2433
- - 'next.config.ts'
2434
- workflow_dispatch:
2435
2551
  pull_request:
2436
2552
  branches:
2437
2553
  - main
2438
2554
  paths:
2439
- - 'app/**'
2440
- - 'components/**'
2441
- - 'lib/**'
2555
+ - 'functions/**'
2442
2556
  - 'shared/**'
2443
- - 'public/**'
2444
- - 'package.json'
2445
- - 'next.config.js'
2446
- - 'next.config.ts'
2557
+ workflow_dispatch:
2447
2558
 
2448
2559
  jobs:
2449
2560
  build-and-deploy:
2450
2561
  runs-on: ubuntu-latest
2451
- name: Build and Deploy Static Web App
2562
+ name: Build and Deploy Functions
2452
2563
 
2453
2564
  steps:
2454
- - uses: actions/checkout@v4
2455
- with:
2456
- submodules: true
2565
+ ${commonSetup} - name: Build Functions
2566
+ run: |
2567
+ ${pmCmd.runFilter('functions')} build
2457
2568
 
2458
- - name: Deploy to Azure Static Web Apps
2569
+ - name: Prepare functions for deployment
2570
+ run: |
2571
+ SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
2572
+ mkdir -p /tmp/fn-deps
2573
+ 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));"
2574
+ cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
2575
+ rm -rf ./functions/node_modules
2576
+ mv /tmp/fn-deps/node_modules ./functions/node_modules
2577
+ SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
2578
+ mkdir -p "$SHARED_DEST"
2579
+ cp -r ./shared/dist "$SHARED_DEST/dist"
2580
+ cp ./shared/package.json "$SHARED_DEST/package.json"
2581
+
2582
+ - name: Deploy to Azure Functions
2459
2583
  if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
2460
- uses: Azure/static-web-apps-deploy@v1
2584
+ uses: Azure/functions-action@v1
2461
2585
  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'
2586
+ app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
2587
+ package: './functions'
2588
+ publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
2589
+ sku: flexconsumption
2470
2590
  `;
2471
- fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
2472
- // deploy-functions.yml
2473
- const functionsWorkflow = `name: Deploy Azure Functions
2591
+ }
2592
+ if (backendLanguage === 'csharp') {
2593
+ return `name: Deploy Azure Functions
2474
2594
 
2475
2595
  on:
2476
2596
  push:
@@ -2493,37 +2613,57 @@ jobs:
2493
2613
  name: Build and Deploy Functions
2494
2614
 
2495
2615
  steps:
2496
- - uses: actions/checkout@v4
2497
-
2498
- - name: Setup Node.js
2499
- uses: actions/setup-node@v4
2616
+ ${commonSetup} - name: Setup .NET
2617
+ uses: actions/setup-dotnet@v4
2500
2618
  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
2619
+ dotnet-version: '10.0.x'
2620
+
2621
+ - name: Publish Functions
2512
2622
  run: |
2513
- ${pmCmd.runFilter('functions')} build
2623
+ dotnet publish ./functions -c Release -o ./functions/publish
2514
2624
 
2515
- - name: Prepare functions for deployment
2625
+ - name: Deploy to Azure Functions
2626
+ if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
2627
+ uses: Azure/functions-action@v1
2628
+ with:
2629
+ app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
2630
+ package: './functions/publish'
2631
+ publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
2632
+ sku: flexconsumption
2633
+ `;
2634
+ }
2635
+ return `name: Deploy Azure Functions
2636
+
2637
+ on:
2638
+ push:
2639
+ branches:
2640
+ - main
2641
+ paths:
2642
+ - 'functions/**'
2643
+ - 'shared/**'
2644
+ pull_request:
2645
+ branches:
2646
+ - main
2647
+ paths:
2648
+ - 'functions/**'
2649
+ - 'shared/**'
2650
+ workflow_dispatch:
2651
+
2652
+ jobs:
2653
+ build-and-deploy:
2654
+ runs-on: ubuntu-latest
2655
+ name: Build and Deploy Functions
2656
+
2657
+ steps:
2658
+ ${commonSetup} - name: Setup Python
2659
+ uses: actions/setup-python@v5
2660
+ with:
2661
+ python-version: '3.11'
2662
+
2663
+ - name: Install Functions dependencies
2516
2664
  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"
2665
+ python -m pip install --upgrade pip
2666
+ python -m pip install -r ./functions/requirements.txt --target "./functions/.python_packages/lib/site-packages"
2527
2667
 
2528
2668
  - name: Deploy to Azure Functions
2529
2669
  if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
@@ -2534,29 +2674,34 @@ ${pnpmSetupStep ? `\n${pnpmSetupStep}\n` : ''}
2534
2674
  publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
2535
2675
  sku: flexconsumption
2536
2676
  `;
2537
- fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
2538
- console.log('✅ GitHub Actions workflows created\n');
2539
2677
  }
2540
- async function createAzurePipelines(projectDir, pm) {
2541
- console.log('📦 Creating Azure Pipelines...\n');
2678
+ function getAzureFunctionsPipeline(pm, backendLanguage, projectPm = pm) {
2542
2679
  const pmCmd = (0, package_manager_1.getCommands)(pm);
2543
2680
  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:
2681
+ const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
2682
+ const commonSetup = ` - task: NodeTool@0
2683
+ inputs:
2684
+ versionSpec: '22.x'
2685
+ displayName: 'Install Node.js'
2686
+ ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
2687
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
2688
+ - script: |
2689
+ ${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
2690
+ displayName: 'Install workspace dependencies'
2691
+
2692
+ - script: |
2693
+ ${pmCmd.runFilter('shared')} build
2694
+ displayName: 'Build shared package'
2695
+ `;
2696
+ if (backendLanguage === 'typescript') {
2697
+ return `trigger:
2548
2698
  branches:
2549
2699
  include:
2550
2700
  - main
2551
2701
  paths:
2552
2702
  include:
2553
- - app/**
2554
- - components/**
2555
- - lib/**
2703
+ - functions/**
2556
2704
  - shared/**
2557
- - public/**
2558
- - package.json
2559
- - next.config.js
2560
2705
 
2561
2706
  pr:
2562
2707
  branches:
@@ -2564,13 +2709,8 @@ pr:
2564
2709
  - main
2565
2710
  paths:
2566
2711
  include:
2567
- - app/**
2568
- - components/**
2569
- - lib/**
2712
+ - functions/**
2570
2713
  - shared/**
2571
- - public/**
2572
- - package.json
2573
- - next.config.js
2574
2714
 
2575
2715
  pool:
2576
2716
  vmImage: 'ubuntu-latest'
@@ -2579,33 +2719,51 @@ variables:
2579
2719
  - group: azure-deployment
2580
2720
 
2581
2721
  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'
2722
+ ${commonSetup} - script: |
2723
+ ${pmCmd.runFilter('functions')} build
2724
+ displayName: 'Build Functions'
2590
2725
 
2591
2726
  - script: |
2592
- ${pmCmd.run} build
2593
- env:
2594
- NODE_ENV: production
2595
- displayName: 'Build Next.js app'
2596
-
2597
- - task: AzureStaticWebApp@0
2598
- condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2599
- 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'
2727
+ SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
2728
+ mkdir -p /tmp/fn-deps
2729
+ 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));"
2730
+ cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
2731
+ rm -rf ./functions/node_modules
2732
+ mv /tmp/fn-deps/node_modules ./functions/node_modules
2733
+ SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
2734
+ mkdir -p "$SHARED_DEST"
2735
+ cp -r ./shared/dist "$SHARED_DEST/dist"
2736
+ cp ./shared/package.json "$SHARED_DEST/package.json"
2737
+ displayName: 'Prepare functions for deployment'
2738
+
2739
+ - task: ArchiveFiles@2
2740
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2741
+ inputs:
2742
+ rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions'
2743
+ includeRootFolder: false
2744
+ archiveType: 'zip'
2745
+ archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
2746
+ displayName: 'Archive Functions'
2747
+
2748
+ - task: PublishBuildArtifacts@1
2749
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2750
+ inputs:
2751
+ PathtoPublish: '$(Build.ArtifactStagingDirectory)/functions.zip'
2752
+ ArtifactName: 'functions'
2753
+ displayName: 'Publish Functions artifact'
2754
+
2755
+ - task: AzureFunctionApp@2
2756
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2757
+ inputs:
2758
+ azureSubscription: '$(AZURE_SUBSCRIPTION)'
2759
+ appType: 'functionAppLinux'
2760
+ appName: '$(AZURE_FUNCTIONAPP_NAME)'
2761
+ package: '$(Build.ArtifactStagingDirectory)/functions.zip'
2762
+ displayName: 'Deploy to Azure Functions'
2605
2763
  `;
2606
- fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
2607
- // functions.yml
2608
- const functionsPipeline = `trigger:
2764
+ }
2765
+ if (backendLanguage === 'csharp') {
2766
+ return `trigger:
2609
2767
  branches:
2610
2768
  include:
2611
2769
  - main
@@ -2630,35 +2788,68 @@ variables:
2630
2788
  - group: azure-deployment
2631
2789
 
2632
2790
  steps:
2633
- - task: NodeTool@0
2791
+ ${commonSetup} - task: UseDotNet@2
2634
2792
  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'
2793
+ version: '10.0.x'
2794
+ displayName: 'Install .NET SDK'
2641
2795
 
2642
2796
  - script: |
2643
- ${pmCmd.runFilter('shared')} build
2644
- displayName: 'Build shared package'
2797
+ dotnet publish ./functions -c Release -o ./functions/publish
2798
+ displayName: 'Publish Functions'
2645
2799
 
2646
- - script: |
2647
- ${pmCmd.runFilter('functions')} build
2648
- displayName: 'Build Functions'
2800
+ - task: ArchiveFiles@2
2801
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2802
+ inputs:
2803
+ rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions/publish'
2804
+ includeRootFolder: false
2805
+ archiveType: 'zip'
2806
+ archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
2807
+ displayName: 'Archive Functions'
2808
+
2809
+ - task: AzureFunctionApp@2
2810
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2811
+ inputs:
2812
+ azureSubscription: '$(AZURE_SUBSCRIPTION)'
2813
+ appType: 'functionAppLinux'
2814
+ appName: '$(AZURE_FUNCTIONAPP_NAME)'
2815
+ package: '$(Build.ArtifactStagingDirectory)/functions.zip'
2816
+ displayName: 'Deploy to Azure Functions'
2817
+ `;
2818
+ }
2819
+ return `trigger:
2820
+ branches:
2821
+ include:
2822
+ - main
2823
+ paths:
2824
+ include:
2825
+ - functions/**
2826
+ - shared/**
2827
+
2828
+ pr:
2829
+ branches:
2830
+ include:
2831
+ - main
2832
+ paths:
2833
+ include:
2834
+ - functions/**
2835
+ - shared/**
2836
+
2837
+ pool:
2838
+ vmImage: 'ubuntu-latest'
2839
+
2840
+ variables:
2841
+ - group: azure-deployment
2842
+
2843
+ steps:
2844
+ ${commonSetup} - task: UsePythonVersion@0
2845
+ inputs:
2846
+ versionSpec: '3.11'
2847
+ displayName: 'Install Python'
2649
2848
 
2650
2849
  - 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'
2850
+ python -m pip install --upgrade pip
2851
+ python -m pip install -r ./functions/requirements.txt --target "./functions/.python_packages/lib/site-packages"
2852
+ displayName: 'Install Functions dependencies'
2662
2853
 
2663
2854
  - task: ArchiveFiles@2
2664
2855
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
@@ -2669,13 +2860,6 @@ ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
2669
2860
  archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
2670
2861
  displayName: 'Archive Functions'
2671
2862
 
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
2863
  - task: AzureFunctionApp@2
2680
2864
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2681
2865
  inputs:
@@ -2685,6 +2869,268 @@ ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
2685
2869
  package: '$(Build.ArtifactStagingDirectory)/functions.zip'
2686
2870
  displayName: 'Deploy to Azure Functions'
2687
2871
  `;
2872
+ }
2873
+ function buildAzureSwaPipeline(pm, projectPm = pm) {
2874
+ const pmCmd = (0, package_manager_1.getCommands)(pm);
2875
+ const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
2876
+ const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
2877
+ return `trigger:
2878
+ branches:
2879
+ include:
2880
+ - main
2881
+ paths:
2882
+ include:
2883
+ - app/**
2884
+ - components/**
2885
+ - lib/**
2886
+ - shared/**
2887
+ - public/**
2888
+ - package.json
2889
+ - next.config.js
2890
+
2891
+ pr:
2892
+ branches:
2893
+ include:
2894
+ - main
2895
+ paths:
2896
+ include:
2897
+ - app/**
2898
+ - components/**
2899
+ - lib/**
2900
+ - shared/**
2901
+ - public/**
2902
+ - package.json
2903
+ - next.config.js
2904
+
2905
+ pool:
2906
+ vmImage: 'ubuntu-latest'
2907
+
2908
+ variables:
2909
+ - group: azure-deployment
2910
+
2911
+ steps:
2912
+ - task: NodeTool@0
2913
+ inputs:
2914
+ versionSpec: '22.x'
2915
+ displayName: 'Install Node.js'
2916
+ ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
2917
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
2918
+ - script: |
2919
+ ${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
2920
+ displayName: 'Install dependencies'
2921
+
2922
+ - script: |
2923
+ ${pmCmd.run} build
2924
+ env:
2925
+ NODE_ENV: production
2926
+ displayName: 'Build Next.js app'
2927
+
2928
+ - task: AzureStaticWebApp@0
2929
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2930
+ inputs:
2931
+ app_location: '.'
2932
+ output_location: '.next/standalone'
2933
+ skip_app_build: true
2934
+ azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
2935
+ displayName: 'Deploy to Azure Static Web Apps'
2936
+ `;
2937
+ }
2938
+ function getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm) {
2939
+ if (pm !== 'npm' || projectPm !== 'pnpm') {
2940
+ return '';
2941
+ }
2942
+ return ` - script: |
2943
+ node <<'NODE'
2944
+ const fs = require('fs');
2945
+
2946
+ function readJson(path) {
2947
+ return JSON.parse(fs.readFileSync(path, 'utf8'));
2948
+ }
2949
+
2950
+ function writeJson(path, value) {
2951
+ fs.writeFileSync(path, JSON.stringify(value, null, 2) + '\\n');
2952
+ }
2953
+
2954
+ const sharedPackagePath = 'shared/package.json';
2955
+ const sharedPackageName = fs.existsSync(sharedPackagePath) ? readJson(sharedPackagePath).name : undefined;
2956
+
2957
+ function replaceSharedWorkspaceDep(pkg, replacement) {
2958
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
2959
+ if (!pkg[section]) continue;
2960
+
2961
+ for (const [name, version] of Object.entries(pkg[section])) {
2962
+ if (typeof version === 'string' && version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
2963
+ pkg[section][name] = replacement;
2964
+ }
2965
+ }
2966
+ }
2967
+ }
2968
+
2969
+ const root = readJson('package.json');
2970
+ root.workspaces = ['shared', 'functions'];
2971
+ replaceSharedWorkspaceDep(root, 'file:shared');
2972
+ root.scripts = root.scripts || {};
2973
+ root.scripts.build = ${JSON.stringify((0, package_manager_1.getBuildScript)('npm'))};
2974
+ writeJson('package.json', root);
2975
+
2976
+ if (fs.existsSync('functions/package.json')) {
2977
+ const functionsPkg = readJson('functions/package.json');
2978
+ replaceSharedWorkspaceDep(functionsPkg, 'file:../shared');
2979
+ if (functionsPkg.scripts && functionsPkg.scripts.prestart) {
2980
+ functionsPkg.scripts.prestart = 'npm run build';
2981
+ }
2982
+ writeJson('functions/package.json', functionsPkg);
2983
+ }
2984
+ NODE
2985
+
2986
+ npm install --package-lock-only --ignore-scripts
2987
+ displayName: 'Normalize pnpm workspace for npm CI'`;
2988
+ }
2989
+ function getGitHubNpmWorkspaceNormalizationStep(pm, projectPm) {
2990
+ if (pm !== 'npm' || projectPm !== 'pnpm') {
2991
+ return '';
2992
+ }
2993
+ return ` - name: Normalize pnpm workspace for npm CI
2994
+ shell: bash
2995
+ run: |
2996
+ node <<'NODE'
2997
+ const fs = require('fs');
2998
+
2999
+ function readJson(path) {
3000
+ return JSON.parse(fs.readFileSync(path, 'utf8'));
3001
+ }
3002
+
3003
+ function writeJson(path, value) {
3004
+ fs.writeFileSync(path, JSON.stringify(value, null, 2) + '\\n');
3005
+ }
3006
+
3007
+ const sharedPackagePath = 'shared/package.json';
3008
+ const sharedPackageName = fs.existsSync(sharedPackagePath) ? readJson(sharedPackagePath).name : undefined;
3009
+
3010
+ function replaceSharedWorkspaceDep(pkg, replacement) {
3011
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
3012
+ if (!pkg[section]) continue;
3013
+
3014
+ for (const [name, version] of Object.entries(pkg[section])) {
3015
+ if (typeof version === 'string' && version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
3016
+ pkg[section][name] = replacement;
3017
+ }
3018
+ }
3019
+ }
3020
+ }
3021
+
3022
+ const root = readJson('package.json');
3023
+ root.workspaces = ['shared', 'functions'];
3024
+ replaceSharedWorkspaceDep(root, 'file:shared');
3025
+ root.scripts = root.scripts || {};
3026
+ root.scripts.build = ${JSON.stringify((0, package_manager_1.getBuildScript)('npm'))};
3027
+ writeJson('package.json', root);
3028
+
3029
+ if (fs.existsSync('functions/package.json')) {
3030
+ const functionsPkg = readJson('functions/package.json');
3031
+ replaceSharedWorkspaceDep(functionsPkg, 'file:../shared');
3032
+ if (functionsPkg.scripts && functionsPkg.scripts.prestart) {
3033
+ functionsPkg.scripts.prestart = 'npm run build';
3034
+ }
3035
+ writeJson('functions/package.json', functionsPkg);
3036
+ }
3037
+ NODE
3038
+
3039
+ npm install --package-lock-only --ignore-scripts
3040
+ `;
3041
+ }
3042
+ async function createGitHubActionsWorkflows(projectDir, azureConfig, pm, backendLanguage) {
3043
+ console.log('📦 Creating GitHub Actions workflows...\n');
3044
+ const workflowsDir = path.join(projectDir, '.github', 'workflows');
3045
+ fs.mkdirSync(workflowsDir, { recursive: true });
3046
+ const actionsPm = pm;
3047
+ const swaWorkflow = buildGitHubSwaWorkflow(actionsPm, pm);
3048
+ fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
3049
+ // deploy-functions.yml
3050
+ const functionsWorkflow = getGitHubFunctionsWorkflow(actionsPm, backendLanguage, pm);
3051
+ fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
3052
+ console.log('✅ GitHub Actions workflows created\n');
3053
+ }
3054
+ function buildGitHubSwaWorkflow(pm, projectPm = pm) {
3055
+ const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
3056
+ return `name: Deploy Static Web App
3057
+
3058
+ on:
3059
+ push:
3060
+ branches:
3061
+ - main
3062
+ paths:
3063
+ - 'app/**'
3064
+ - 'components/**'
3065
+ - 'lib/**'
3066
+ - 'shared/**'
3067
+ - 'public/**'
3068
+ - 'package.json'
3069
+ - '${pm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
3070
+ - 'next.config.js'
3071
+ - 'next.config.ts'
3072
+ workflow_dispatch:
3073
+ pull_request:
3074
+ branches:
3075
+ - main
3076
+ paths:
3077
+ - 'app/**'
3078
+ - 'components/**'
3079
+ - 'lib/**'
3080
+ - 'shared/**'
3081
+ - 'public/**'
3082
+ - 'package.json'
3083
+ - '${pm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
3084
+ - 'next.config.js'
3085
+ - 'next.config.ts'
3086
+
3087
+ jobs:
3088
+ build-and-deploy:
3089
+ runs-on: ubuntu-latest
3090
+ name: Build and Deploy Static Web App
3091
+
3092
+ steps:
3093
+ - uses: actions/checkout@v4
3094
+ with:
3095
+ submodules: true
3096
+
3097
+ ${(0, package_manager_1.getCiSetupStep)(pm) ? `${(0, package_manager_1.getCiSetupStep)(pm)}\n` : ''}
3098
+ - name: Setup Node.js
3099
+ uses: actions/setup-node@v4
3100
+ with:
3101
+ node-version: '22'
3102
+ cache: ${pm}
3103
+ cache-dependency-path: ${pm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}
3104
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
3105
+ - name: Install and build app
3106
+ run: |
3107
+ ${(0, package_manager_1.getSwaAppBuildCommand)(pm, projectPm)}
3108
+
3109
+ - name: Deploy to Azure Static Web Apps
3110
+ if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
3111
+ uses: Azure/static-web-apps-deploy@v1
3112
+ with:
3113
+ azure_static_web_apps_api_token: \${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
3114
+ repo_token: \${{ secrets.GITHUB_TOKEN }}
3115
+ action: 'upload'
3116
+ app_location: '/'
3117
+ api_location: ''
3118
+ output_location: ''
3119
+ app_build_command: '${(0, package_manager_1.getSwaAppBuildCommand)(pm, projectPm)}'
3120
+ env:
3121
+ NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
3122
+ `;
3123
+ }
3124
+ async function createAzurePipelines(projectDir, pm, backendLanguage) {
3125
+ console.log('📦 Creating Azure Pipelines...\n');
3126
+ const pipelinesPm = pm;
3127
+ const pipelinesDir = path.join(projectDir, 'pipelines');
3128
+ fs.mkdirSync(pipelinesDir, { recursive: true });
3129
+ // swa.yml
3130
+ const swaPipeline = buildAzureSwaPipeline(pipelinesPm, pm);
3131
+ fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
3132
+ // functions.yml
3133
+ const functionsPipeline = getAzureFunctionsPipeline(pipelinesPm, backendLanguage, pm);
2688
3134
  fs.writeFileSync(path.join(pipelinesDir, 'functions.yml'), functionsPipeline);
2689
3135
  console.log('✅ Azure Pipelines created\n');
2690
3136
  }