swallowkit 1.0.0-beta.5 → 1.0.0-beta.51

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 (222) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +153 -242
  3. package/README.md +153 -243
  4. package/dist/__tests__/fixtures.d.ts +22 -0
  5. package/dist/__tests__/fixtures.d.ts.map +1 -0
  6. package/dist/__tests__/fixtures.js +146 -0
  7. package/dist/__tests__/fixtures.js.map +1 -0
  8. package/dist/cli/commands/add-auth.d.ts +13 -0
  9. package/dist/cli/commands/add-auth.d.ts.map +1 -0
  10. package/dist/cli/commands/add-auth.js +759 -0
  11. package/dist/cli/commands/add-auth.js.map +1 -0
  12. package/dist/cli/commands/add-connector.d.ts +20 -0
  13. package/dist/cli/commands/add-connector.d.ts.map +1 -0
  14. package/dist/cli/commands/add-connector.js +163 -0
  15. package/dist/cli/commands/add-connector.js.map +1 -0
  16. package/dist/cli/commands/create-model.d.ts +1 -4
  17. package/dist/cli/commands/create-model.d.ts.map +1 -1
  18. package/dist/cli/commands/create-model.js +19 -81
  19. package/dist/cli/commands/create-model.js.map +1 -1
  20. package/dist/cli/commands/dev-seeds.d.ts +57 -0
  21. package/dist/cli/commands/dev-seeds.d.ts.map +1 -0
  22. package/dist/cli/commands/dev-seeds.js +470 -0
  23. package/dist/cli/commands/dev-seeds.js.map +1 -0
  24. package/dist/cli/commands/dev.d.ts +37 -0
  25. package/dist/cli/commands/dev.d.ts.map +1 -1
  26. package/dist/cli/commands/dev.js +720 -156
  27. package/dist/cli/commands/dev.js.map +1 -1
  28. package/dist/cli/commands/index.d.ts +1 -0
  29. package/dist/cli/commands/index.d.ts.map +1 -1
  30. package/dist/cli/commands/index.js +3 -1
  31. package/dist/cli/commands/index.js.map +1 -1
  32. package/dist/cli/commands/init.d.ts +73 -0
  33. package/dist/cli/commands/init.d.ts.map +1 -1
  34. package/dist/cli/commands/init.js +3089 -1779
  35. package/dist/cli/commands/init.js.map +1 -1
  36. package/dist/cli/commands/scaffold.d.ts +13 -0
  37. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  38. package/dist/cli/commands/scaffold.js +513 -129
  39. package/dist/cli/commands/scaffold.js.map +1 -1
  40. package/dist/cli/index.d.ts +5 -1
  41. package/dist/cli/index.d.ts.map +1 -1
  42. package/dist/cli/index.js +206 -55
  43. package/dist/cli/index.js.map +1 -1
  44. package/dist/core/config.d.ts +14 -3
  45. package/dist/core/config.d.ts.map +1 -1
  46. package/dist/core/config.js +172 -5
  47. package/dist/core/config.js.map +1 -1
  48. package/dist/core/mock/connector-mock-server.d.ts +101 -0
  49. package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
  50. package/dist/core/mock/connector-mock-server.js +480 -0
  51. package/dist/core/mock/connector-mock-server.js.map +1 -0
  52. package/dist/core/mock/zod-mock-generator.d.ts +14 -0
  53. package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
  54. package/dist/core/mock/zod-mock-generator.js +163 -0
  55. package/dist/core/mock/zod-mock-generator.js.map +1 -0
  56. package/dist/core/operations/create-model.d.ts +15 -0
  57. package/dist/core/operations/create-model.d.ts.map +1 -0
  58. package/dist/core/operations/create-model.js +171 -0
  59. package/dist/core/operations/create-model.js.map +1 -0
  60. package/dist/core/operations/runtime.d.ts +32 -0
  61. package/dist/core/operations/runtime.d.ts.map +1 -0
  62. package/dist/core/operations/runtime.js +225 -0
  63. package/dist/core/operations/runtime.js.map +1 -0
  64. package/dist/core/operations/scaffold-machine.d.ts +16 -0
  65. package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
  66. package/dist/core/operations/scaffold-machine.js +63 -0
  67. package/dist/core/operations/scaffold-machine.js.map +1 -0
  68. package/dist/core/project/manifest.d.ts +92 -0
  69. package/dist/core/project/manifest.d.ts.map +1 -0
  70. package/dist/core/project/manifest.js +321 -0
  71. package/dist/core/project/manifest.js.map +1 -0
  72. package/dist/core/project/validation.d.ts +20 -0
  73. package/dist/core/project/validation.d.ts.map +1 -0
  74. package/dist/core/project/validation.js +226 -0
  75. package/dist/core/project/validation.js.map +1 -0
  76. package/dist/core/scaffold/auth-generator.d.ts +63 -0
  77. package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
  78. package/dist/core/scaffold/auth-generator.js +1714 -0
  79. package/dist/core/scaffold/auth-generator.js.map +1 -0
  80. package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
  81. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
  82. package/dist/core/scaffold/connector-functions-generator.js +1029 -0
  83. package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
  84. package/dist/core/scaffold/functions-generator.d.ts +7 -1
  85. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  86. package/dist/core/scaffold/functions-generator.js +922 -220
  87. package/dist/core/scaffold/functions-generator.js.map +1 -1
  88. package/dist/core/scaffold/model-parser.d.ts +20 -1
  89. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  90. package/dist/core/scaffold/model-parser.js +333 -139
  91. package/dist/core/scaffold/model-parser.js.map +1 -1
  92. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  93. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
  94. package/dist/core/scaffold/native-schema-generator.js +696 -0
  95. package/dist/core/scaffold/native-schema-generator.js.map +1 -0
  96. package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
  97. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  98. package/dist/core/scaffold/nextjs-generator.js +326 -182
  99. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  100. package/dist/core/scaffold/openapi-generator.d.ts +3 -0
  101. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
  102. package/dist/core/scaffold/openapi-generator.js +190 -0
  103. package/dist/core/scaffold/openapi-generator.js.map +1 -0
  104. package/dist/core/scaffold/ui-generator.d.ts +13 -4
  105. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  106. package/dist/core/scaffold/ui-generator.js +772 -663
  107. package/dist/core/scaffold/ui-generator.js.map +1 -1
  108. package/dist/database/base-model.d.ts +3 -3
  109. package/dist/database/base-model.js +3 -3
  110. package/dist/index.d.ts +2 -2
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +2 -1
  113. package/dist/index.js.map +1 -1
  114. package/dist/machine/contracts.d.ts +16 -0
  115. package/dist/machine/contracts.d.ts.map +1 -0
  116. package/dist/machine/contracts.js +3 -0
  117. package/dist/machine/contracts.js.map +1 -0
  118. package/dist/machine/errors.d.ts +11 -0
  119. package/dist/machine/errors.d.ts.map +1 -0
  120. package/dist/machine/errors.js +34 -0
  121. package/dist/machine/errors.js.map +1 -0
  122. package/dist/machine/index.d.ts +3 -0
  123. package/dist/machine/index.d.ts.map +1 -0
  124. package/dist/machine/index.js +156 -0
  125. package/dist/machine/index.js.map +1 -0
  126. package/dist/mcp/index.d.ts +25 -0
  127. package/dist/mcp/index.d.ts.map +1 -0
  128. package/dist/mcp/index.js +202 -0
  129. package/dist/mcp/index.js.map +1 -0
  130. package/dist/types/index.d.ts +95 -0
  131. package/dist/types/index.d.ts.map +1 -1
  132. package/dist/utils/package-manager.d.ts +5 -2
  133. package/dist/utils/package-manager.d.ts.map +1 -1
  134. package/dist/utils/package-manager.js +34 -13
  135. package/dist/utils/package-manager.js.map +1 -1
  136. package/dist/utils/python-uv.d.ts +21 -0
  137. package/dist/utils/python-uv.d.ts.map +1 -0
  138. package/dist/utils/python-uv.js +111 -0
  139. package/dist/utils/python-uv.js.map +1 -0
  140. package/dist/version.d.ts +2 -0
  141. package/dist/version.d.ts.map +1 -0
  142. package/dist/version.js +49 -0
  143. package/dist/version.js.map +1 -0
  144. package/package.json +84 -74
  145. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +1139 -0
  146. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +206 -0
  147. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +532 -0
  148. package/src/__tests__/auth.test.ts +761 -0
  149. package/src/__tests__/config.test.ts +295 -0
  150. package/src/__tests__/connector-functions-generator.test.ts +288 -0
  151. package/src/__tests__/connector-mock-server.test.ts +439 -0
  152. package/src/__tests__/connector-model-bff.test.ts +162 -0
  153. package/src/__tests__/dev-seeds.test.ts +173 -0
  154. package/src/__tests__/dev.test.ts +287 -0
  155. package/src/__tests__/fixtures.ts +144 -0
  156. package/src/__tests__/functions-generator.test.ts +237 -0
  157. package/src/__tests__/init.test.ts +356 -0
  158. package/src/__tests__/jest-globals.d.ts +8 -0
  159. package/src/__tests__/machine.test.ts +356 -0
  160. package/src/__tests__/mcp.test.ts +145 -0
  161. package/src/__tests__/model-parser.test.ts +52 -0
  162. package/src/__tests__/multi-auth.test.ts +133 -0
  163. package/src/__tests__/nextjs-generator.test.ts +105 -0
  164. package/src/__tests__/openapi-generator.test.ts +43 -0
  165. package/src/__tests__/package-manager.test.ts +227 -0
  166. package/src/__tests__/python-uv.test.ts +48 -0
  167. package/src/__tests__/scaffold.test.ts +191 -0
  168. package/src/__tests__/string-utils.test.ts +75 -0
  169. package/src/__tests__/tsconfig.json +15 -0
  170. package/src/__tests__/ui-generator.test.ts +144 -0
  171. package/src/__tests__/zod-mock-generator.test.ts +132 -0
  172. package/src/cli/commands/add-auth.ts +815 -0
  173. package/src/cli/commands/add-connector.ts +158 -0
  174. package/src/cli/commands/create-model.ts +62 -0
  175. package/src/cli/commands/dev-seeds.ts +614 -0
  176. package/src/cli/commands/dev.ts +1234 -0
  177. package/src/cli/commands/index.ts +9 -0
  178. package/src/cli/commands/init.ts +3902 -0
  179. package/src/cli/commands/provision.ts +193 -0
  180. package/src/cli/commands/scaffold.ts +1067 -0
  181. package/src/cli/index.ts +202 -0
  182. package/src/core/config.ts +379 -0
  183. package/src/core/mock/connector-mock-server.ts +555 -0
  184. package/src/core/mock/zod-mock-generator.ts +205 -0
  185. package/src/core/operations/create-model.ts +174 -0
  186. package/src/core/operations/runtime.ts +235 -0
  187. package/src/core/operations/scaffold-machine.ts +91 -0
  188. package/src/core/project/manifest.ts +402 -0
  189. package/src/core/project/validation.ts +245 -0
  190. package/src/core/scaffold/auth-generator.ts +1760 -0
  191. package/src/core/scaffold/connector-functions-generator.ts +1133 -0
  192. package/src/core/scaffold/functions-generator.ts +972 -0
  193. package/src/core/scaffold/model-parser.ts +847 -0
  194. package/src/core/scaffold/native-schema-generator.ts +822 -0
  195. package/src/core/scaffold/nextjs-generator.ts +382 -0
  196. package/src/core/scaffold/openapi-generator.ts +212 -0
  197. package/src/core/scaffold/ui-generator.ts +1068 -0
  198. package/src/database/base-model.ts +184 -0
  199. package/src/database/client.ts +140 -0
  200. package/src/database/repository.ts +104 -0
  201. package/src/database/runtime-check.ts +25 -0
  202. package/src/index.ts +27 -0
  203. package/src/machine/contracts.ts +17 -0
  204. package/src/machine/errors.ts +34 -0
  205. package/src/machine/index.ts +173 -0
  206. package/src/mcp/index.ts +209 -0
  207. package/src/types/index.ts +165 -0
  208. package/src/utils/package-manager.ts +246 -0
  209. package/src/utils/python-uv.ts +96 -0
  210. package/src/version.ts +13 -0
  211. package/dist/cli/commands/build.d.ts +0 -6
  212. package/dist/cli/commands/build.d.ts.map +0 -1
  213. package/dist/cli/commands/build.js +0 -177
  214. package/dist/cli/commands/build.js.map +0 -1
  215. package/dist/cli/commands/deploy.d.ts +0 -3
  216. package/dist/cli/commands/deploy.d.ts.map +0 -1
  217. package/dist/cli/commands/deploy.js +0 -147
  218. package/dist/cli/commands/deploy.js.map +0 -1
  219. package/dist/cli/commands/setup.d.ts +0 -6
  220. package/dist/cli/commands/setup.d.ts.map +0 -1
  221. package/dist/cli/commands/setup.js +0 -254
  222. package/dist/cli/commands/setup.js.map +0 -1
@@ -0,0 +1,3902 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { spawn, execSync } from "child_process";
4
+ import prompts from "prompts";
5
+ import { BackendLanguage } from "../../types";
6
+ import {
7
+ type PackageManager,
8
+ detectFromUserAgent,
9
+ getCommands,
10
+ getWorkspaceConfig,
11
+ getCiSetupStep,
12
+ getCiInstallCommand,
13
+ getAzurePipelinesSetup,
14
+ getBuildScript,
15
+ getFunctionsPrestart,
16
+ getFunctionsStartScript,
17
+ getSwaAppBuildCommand,
18
+ } from "../../utils/package-manager";
19
+ import { syncProjectManifest } from "../../core/project/manifest";
20
+ import {
21
+ buildCSharpCodegenToolManifestSource,
22
+ buildPythonCodegenRequirementsSource,
23
+ } from "../../core/scaffold/native-schema-generator";
24
+ import { getProjectLocalUvPaths } from "../../utils/python-uv";
25
+
26
+ interface InitOptions {
27
+ name: string;
28
+ template: string;
29
+ nextVersion?: string;
30
+ cicd?: CiCdProvider;
31
+ backendLanguage?: BackendLanguage;
32
+ cosmosDbMode?: CosmosDbMode;
33
+ vnet?: VNetOption;
34
+ swaPlan?: StaticWebAppPlan;
35
+ }
36
+
37
+ type CiCdProvider = 'github' | 'azure' | 'skip';
38
+ type CosmosDbMode = 'freetier' | 'serverless';
39
+ type VNetOption = 'none' | 'outbound';
40
+ type StaticWebAppPlan = 'free' | 'standard';
41
+
42
+ export function getStaticWebAppSku(plan: StaticWebAppPlan): 'Free' | 'Standard' {
43
+ return plan === 'free' ? 'Free' : 'Standard';
44
+ }
45
+
46
+ export interface AzureConfig {
47
+ cosmosDbMode: CosmosDbMode;
48
+ vnetOption: VNetOption;
49
+ swaPlan: StaticWebAppPlan;
50
+ }
51
+
52
+ const BACKEND_LANGUAGE_CHOICES: Array<{ title: string; value: BackendLanguage }> = [
53
+ { title: "TypeScript", value: "typescript" },
54
+ { title: "C#", value: "csharp" },
55
+ { title: "Python", value: "python" },
56
+ ];
57
+
58
+ function usesNodeFunctionsProject(backendLanguage: BackendLanguage): boolean {
59
+ return backendLanguage === "typescript";
60
+ }
61
+
62
+ function getBackendLanguageLabel(backendLanguage: BackendLanguage): string {
63
+ return BACKEND_LANGUAGE_CHOICES.find((choice) => choice.value === backendLanguage)?.title || backendLanguage;
64
+ }
65
+
66
+ export function buildCosmosDbFreeTierBicepSource(): string {
67
+ return `@description('Cosmos DB account name')
68
+ param accountName string
69
+
70
+ @description('Database name')
71
+ param databaseName string
72
+
73
+ @description('Location for Cosmos DB')
74
+ param location string
75
+
76
+ @description('Public network access')
77
+ @allowed(['Enabled', 'Disabled'])
78
+ param publicNetworkAccess string = 'Enabled'
79
+
80
+ // Cosmos DB Account (Free Tier)
81
+ resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
82
+ name: accountName
83
+ location: location
84
+ kind: 'GlobalDocumentDB'
85
+ properties: {
86
+ databaseAccountOfferType: 'Standard'
87
+ enableAutomaticFailover: false
88
+ enableFreeTier: true
89
+ publicNetworkAccess: publicNetworkAccess
90
+ disableLocalAuth: true
91
+ consistencyPolicy: {
92
+ defaultConsistencyLevel: 'Session'
93
+ }
94
+ locations: [
95
+ {
96
+ locationName: location
97
+ failoverPriority: 0
98
+ isZoneRedundant: false
99
+ }
100
+ ]
101
+ disableKeyBasedMetadataWriteAccess: true
102
+ }
103
+ }
104
+
105
+ // Cosmos DB Database
106
+ resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
107
+ parent: cosmosAccount
108
+ name: databaseName
109
+ properties: {
110
+ resource: {
111
+ id: databaseName
112
+ }
113
+ options: {
114
+ throughput: 1000
115
+ }
116
+ }
117
+ }
118
+
119
+ output id string = cosmosAccount.id
120
+ output accountName string = cosmosAccount.name
121
+ output endpoint string = cosmosAccount.properties.documentEndpoint
122
+ output databaseName string = database.name
123
+ `;
124
+ }
125
+
126
+ function getFunctionsWorkerRuntime(backendLanguage: BackendLanguage): string {
127
+ if (backendLanguage === "csharp") {
128
+ return "dotnet-isolated";
129
+ }
130
+ if (backendLanguage === "python") {
131
+ return "python";
132
+ }
133
+ return "node";
134
+ }
135
+
136
+ function getFunctionsRuntimeConfig(backendLanguage: BackendLanguage): { name: string; version: string } {
137
+ if (backendLanguage === "csharp") {
138
+ return { name: "dotnet-isolated", version: "10.0" };
139
+ }
140
+ if (backendLanguage === "python") {
141
+ return { name: "python", version: "3.11" };
142
+ }
143
+ return { name: "node", version: "22" };
144
+ }
145
+
146
+ export function buildSharedTsConfig() {
147
+ return {
148
+ compilerOptions: {
149
+ target: "ES2020",
150
+ module: "Node16",
151
+ moduleResolution: "node16",
152
+ lib: ["ES2020"],
153
+ outDir: "dist",
154
+ rootDir: ".",
155
+ declaration: true,
156
+ declarationMap: true,
157
+ sourceMap: true,
158
+ strict: true,
159
+ esModuleInterop: true,
160
+ skipLibCheck: true,
161
+ forceConsistentCasingInFileNames: true,
162
+ },
163
+ include: ["index.ts", "models/**/*"],
164
+ exclude: ["node_modules", "dist"],
165
+ };
166
+ }
167
+
168
+ async function promptBackendLanguage(): Promise<BackendLanguage> {
169
+ const response = await prompts({
170
+ type: "select",
171
+ name: "backendLanguage",
172
+ message: "Azure Functions backend language:",
173
+ choices: BACKEND_LANGUAGE_CHOICES,
174
+ initial: 0,
175
+ });
176
+
177
+ return response.backendLanguage || "typescript";
178
+ }
179
+
180
+ async function promptCiCd(): Promise<CiCdProvider> {
181
+ const response = await prompts({
182
+ type: 'select',
183
+ name: 'cicd',
184
+ message: 'CI/CD Setup (choose deployment automation):',
185
+ choices: [
186
+ { title: 'GitHub Actions', value: 'github' },
187
+ { title: 'Azure Pipelines', value: 'azure' },
188
+ { title: 'Skip (manual deployment)', value: 'skip' }
189
+ ],
190
+ initial: 0
191
+ });
192
+
193
+ return response.cicd || 'skip';
194
+ }
195
+
196
+ async function promptAzureConfig(options: InitOptions): Promise<AzureConfig> {
197
+ let swaPlan = options.swaPlan;
198
+ if (!swaPlan) {
199
+ const response = await prompts({
200
+ type: 'select',
201
+ name: 'plan',
202
+ message: 'Azure Static Web Apps plan:',
203
+ choices: [
204
+ { title: 'Standard (production, SLA and advanced auth/networking features)', value: 'standard' },
205
+ { title: 'Free (personal projects, no SLA and lower quotas)', value: 'free' }
206
+ ],
207
+ initial: 0
208
+ });
209
+ swaPlan = response.plan || 'standard';
210
+ }
211
+
212
+ let cosmosDbMode = options.cosmosDbMode;
213
+ if (!cosmosDbMode) {
214
+ const response = await prompts({
215
+ type: 'select',
216
+ name: 'mode',
217
+ message: 'Cosmos DB mode (affects cost):',
218
+ choices: [
219
+ { title: 'Free Tier (1000 RU/s free, best for first project)', value: 'freetier' },
220
+ { title: 'Serverless (pay-per-use, flexible)', value: 'serverless' }
221
+ ],
222
+ initial: 0
223
+ });
224
+ cosmosDbMode = response.mode || 'freetier';
225
+ }
226
+
227
+ let vnetOption = options.vnet;
228
+ if (!vnetOption) {
229
+ const response = await prompts({
230
+ type: 'select',
231
+ name: 'vnet',
232
+ message: 'Network security:',
233
+ choices: [
234
+ { title: 'VNet Integration (recommended) - Cosmos DB via Private Endpoint', value: 'outbound' },
235
+ { title: 'None - Public endpoints, simplest but least secure', value: 'none' }
236
+ ],
237
+ initial: 0
238
+ });
239
+ vnetOption = response.vnet || 'outbound';
240
+ }
241
+
242
+ return {
243
+ cosmosDbMode: cosmosDbMode || 'freetier',
244
+ vnetOption: vnetOption || 'outbound',
245
+ swaPlan: swaPlan || 'standard'
246
+ };
247
+ }
248
+
249
+ const VALID_CICD: CiCdProvider[] = ['github', 'azure', 'skip'];
250
+ const VALID_BACKEND_LANGUAGE: BackendLanguage[] = ['typescript', 'csharp', 'python'];
251
+ const VALID_COSMOS_DB_MODE: CosmosDbMode[] = ['freetier', 'serverless'];
252
+ const VALID_VNET: VNetOption[] = ['none', 'outbound'];
253
+ const VALID_SWA_PLAN: StaticWebAppPlan[] = ['free', 'standard'];
254
+
255
+ function validateInitFlags(options: InitOptions): void {
256
+ if (options.cicd && !VALID_CICD.includes(options.cicd)) {
257
+ console.error(`❌ Invalid --cicd value: "${options.cicd}". Must be: ${VALID_CICD.join(', ')}`);
258
+ process.exit(1);
259
+ }
260
+ if (options.backendLanguage && !VALID_BACKEND_LANGUAGE.includes(options.backendLanguage)) {
261
+ console.error(`❌ Invalid --backend-language value: "${options.backendLanguage}". Must be: ${VALID_BACKEND_LANGUAGE.join(', ')}`);
262
+ process.exit(1);
263
+ }
264
+ if (options.cosmosDbMode && !VALID_COSMOS_DB_MODE.includes(options.cosmosDbMode)) {
265
+ console.error(`❌ Invalid --cosmos-db-mode value: "${options.cosmosDbMode}". Must be: ${VALID_COSMOS_DB_MODE.join(', ')}`);
266
+ process.exit(1);
267
+ }
268
+ if (options.vnet && !VALID_VNET.includes(options.vnet)) {
269
+ console.error(`❌ Invalid --vnet value: "${options.vnet}". Must be: ${VALID_VNET.join(', ')}`);
270
+ process.exit(1);
271
+ }
272
+ if (options.swaPlan && !VALID_SWA_PLAN.includes(options.swaPlan)) {
273
+ console.error(`❌ Invalid --swa-plan value: "${options.swaPlan}". Must be: ${VALID_SWA_PLAN.join(', ')}`);
274
+ process.exit(1);
275
+ }
276
+ }
277
+
278
+ export async function initCommand(options: InitOptions) {
279
+ // Validate flag values before doing anything
280
+ validateInitFlags(options);
281
+
282
+ console.log(`🚀 Initializing SwallowKit project: ${options.name}`);
283
+ console.log(`📋 Template: ${options.template}`);
284
+
285
+ // Detect package manager from invocation context (npx → npm, pnpm dlx → pnpm)
286
+ const pm: PackageManager = detectFromUserAgent();
287
+ const pmCmd = getCommands(pm);
288
+ console.log(`📦 Package manager: ${pm}`);
289
+
290
+ const projectDir = path.join(process.cwd(), options.name);
291
+
292
+ try {
293
+ // Check if directory already exists
294
+ if (fs.existsSync(projectDir)) {
295
+ console.error(`❌ Directory "${options.name}" already exists.`);
296
+ process.exit(1);
297
+ }
298
+
299
+ // Use flag values if provided, otherwise prompt interactively
300
+ const cicdProvider: CiCdProvider = options.cicd || await promptCiCd();
301
+ const backendLanguage: BackendLanguage = options.backendLanguage || await promptBackendLanguage();
302
+
303
+ const azureConfig = await promptAzureConfig(options);
304
+
305
+ // Create Next.js project with create-next-app
306
+ await createNextJsProject(options.name, pm);
307
+
308
+ // Upgrade Next.js to specified version (or latest) to avoid cached old versions
309
+ await upgradeNextJs(projectDir, options.nextVersion || 'latest', pm);
310
+
311
+ // Add SwallowKit specific files
312
+ await addSwallowKitFiles(projectDir, options, cicdProvider, azureConfig, pm, backendLanguage);
313
+
314
+ // Create infrastructure files (Bicep)
315
+ await createInfrastructure(projectDir, options.name, azureConfig, backendLanguage);
316
+
317
+ // Create CI/CD files based on choice
318
+ if (cicdProvider === 'github') {
319
+ await createGitHubActionsWorkflows(projectDir, azureConfig, pm, backendLanguage);
320
+ } else if (cicdProvider === 'azure') {
321
+ await createAzurePipelines(projectDir, pm, backendLanguage);
322
+ }
323
+
324
+ await syncProjectManifest(projectDir);
325
+
326
+ // Initialize Git repository and create initial commit
327
+ try {
328
+ // Try git init with -b main (Git 2.28+), fallback to git init
329
+ try {
330
+ execSync('git init -b main', { cwd: projectDir, stdio: 'ignore' });
331
+ } catch {
332
+ // Fallback for older Git versions
333
+ execSync('git init', { cwd: projectDir, stdio: 'ignore' });
334
+ }
335
+
336
+ // Configure git user if not set (required for commits)
337
+ try {
338
+ execSync('git config user.name', { cwd: projectDir, stdio: 'pipe' });
339
+ execSync('git config user.email', { cwd: projectDir, stdio: 'pipe' });
340
+ } catch {
341
+ // Not configured globally, set locally for this repository
342
+ execSync('git config user.name "SwallowKit"', { cwd: projectDir, stdio: 'ignore' });
343
+ execSync('git config user.email "swallowkit@example.com"', { cwd: projectDir, stdio: 'ignore' });
344
+ }
345
+
346
+ execSync('git add -A', { cwd: projectDir, stdio: 'ignore' });
347
+ execSync('git commit -m "Initial commit from SwallowKit"', { cwd: projectDir, stdio: 'ignore' });
348
+
349
+ // Rename branch to main if git init -b main didn't work
350
+ try {
351
+ const currentBranch = execSync('git branch --show-current', { cwd: projectDir, encoding: 'utf-8' }).trim();
352
+ if (currentBranch !== 'main') {
353
+ execSync('git branch -M main', { cwd: projectDir, stdio: 'ignore' });
354
+ }
355
+ } catch {
356
+ // Ignore errors - branch renaming is not critical
357
+ }
358
+
359
+ console.log('✅ Git repository initialized with initial commit\n');
360
+ } catch (error) {
361
+ console.warn('⚠️ Could not initialize Git repository (is git installed?)');
362
+ if (error instanceof Error) {
363
+ console.warn(` Error: ${error.message}`);
364
+ }
365
+ }
366
+
367
+ console.log(`\n✅ Project "${options.name}" created successfully!`);
368
+ console.log("\n📝 Next steps:");
369
+ console.log(` cd ${options.name}`);
370
+ console.log(` ${pmCmd.dlx} swallowkit create-model <name> # Create your first model`);
371
+ console.log(` ${pmCmd.dlx} swallowkit scaffold shared/models/<name>.ts # Generate CRUD code`);
372
+ console.log(` ${pmCmd.dlx} swallowkit dev # Start development servers`);
373
+ console.log("\n🚀 Deploy to Azure:");
374
+ console.log(` ${pmCmd.dlx} swallowkit provision --resource-group <name>`);
375
+ if (cicdProvider !== 'skip') {
376
+ console.log(" Configure CI/CD secrets and push to repository");
377
+ }
378
+ } catch (error) {
379
+ console.error("❌ Project creation failed:", error);
380
+ // Clean up on failure
381
+ if (fs.existsSync(projectDir)) {
382
+ fs.rmSync(projectDir, { recursive: true, force: true });
383
+ }
384
+ process.exit(1);
385
+ }
386
+ }
387
+
388
+ async function createNextJsProject(projectName: string, pm: PackageManager): Promise<void> {
389
+ return new Promise((resolve, reject) => {
390
+ console.log('\n📦 Creating Next.js project with create-next-app...\n');
391
+
392
+ const pmCmd = getCommands(pm);
393
+
394
+ // Build args: for pnpm use "pnpm dlx create-next-app@latest ... --use-pnpm"
395
+ // for npm use "npx create-next-app@latest ..."
396
+ const baseArgs = pm === 'pnpm'
397
+ ? ['dlx', 'create-next-app@latest']
398
+ : ['create-next-app@latest'];
399
+
400
+ const args = [
401
+ ...baseArgs,
402
+ projectName,
403
+ '--typescript',
404
+ '--tailwind',
405
+ '--app',
406
+ '--no-src',
407
+ '--disable-git',
408
+ '--import-alias',
409
+ '@/*',
410
+ ...(pmCmd.createNextAppFlag ? [pmCmd.createNextAppFlag] : []),
411
+ '--yes'
412
+ ];
413
+
414
+ // Run create-next-app with recommended options for Azure
415
+ const createNextApp = spawn(
416
+ pm === 'pnpm' ? 'pnpm' : 'npx',
417
+ args,
418
+ {
419
+ stdio: 'inherit',
420
+ shell: true,
421
+ }
422
+ );
423
+
424
+ createNextApp.on('close', (code: number | null) => {
425
+ if (code !== 0) {
426
+ reject(new Error(`create-next-app exited with code ${code}`));
427
+ } else {
428
+ console.log('\n✅ Next.js project created\n');
429
+ resolve();
430
+ }
431
+ });
432
+
433
+ createNextApp.on('error', (error: Error) => {
434
+ reject(error);
435
+ });
436
+ });
437
+ }
438
+
439
+ async function upgradeNextJs(projectDir: string, version: string, pm: PackageManager): Promise<void> {
440
+ console.log(`\n📦 Installing Next.js ${version} (to ensure latest security patches)...\n`);
441
+
442
+ // pnpm: pnpm add next@... ; npm: npm install next@...
443
+ const args = pm === 'pnpm'
444
+ ? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact']
445
+ : ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact'];
446
+
447
+ await runPackageManagerCommand(pm, args, projectDir, `${pm} add next@${version}`);
448
+
449
+ console.log(`\n✅ Next.js ${version} installed\n`);
450
+ }
451
+
452
+ async function installDependencies(projectDir: string, pm: PackageManager = 'pnpm'): Promise<void> {
453
+ console.log('\n📦 Installing dependencies...\n');
454
+ await runPackageManagerCommand(pm, ['install'], projectDir, `${pm} install`);
455
+
456
+ if (pm === 'pnpm') {
457
+ console.log('\n📦 Generating npm lockfile for CI/CD...\n');
458
+ await withNpmLockfileSafeManifests(projectDir, async () => {
459
+ await runPackageManagerCommand(
460
+ 'npm',
461
+ ['install', '--package-lock-only', '--ignore-scripts'],
462
+ projectDir,
463
+ 'npm install --package-lock-only'
464
+ );
465
+ });
466
+ }
467
+
468
+ console.log('\n✅ Dependencies installed\n');
469
+ }
470
+
471
+ type PackageJson = {
472
+ dependencies?: Record<string, string>;
473
+ devDependencies?: Record<string, string>;
474
+ optionalDependencies?: Record<string, string>;
475
+ [key: string]: unknown;
476
+ };
477
+
478
+ export async function withNpmLockfileSafeManifests(projectDir: string, action: () => Promise<void>): Promise<void> {
479
+ const packageJsonPath = path.join(projectDir, 'package.json');
480
+ const functionsPackageJsonPath = path.join(projectDir, 'functions', 'package.json');
481
+ const sharedPackageJsonPath = path.join(projectDir, 'shared', 'package.json');
482
+ const originalFiles = new Map<string, string>();
483
+ const hiddenNodeModules = new Map<string, string>();
484
+
485
+ function rememberOriginal(filePath: string): void {
486
+ if (!originalFiles.has(filePath)) {
487
+ originalFiles.set(filePath, fs.readFileSync(filePath, 'utf-8'));
488
+ }
489
+ }
490
+
491
+ function readJson(filePath: string): PackageJson {
492
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
493
+ }
494
+
495
+ function writeJson(filePath: string, value: PackageJson): void {
496
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
497
+ }
498
+
499
+ const sharedPackageName = fs.existsSync(sharedPackageJsonPath)
500
+ ? (readJson(sharedPackageJsonPath).name as string | undefined)
501
+ : undefined;
502
+
503
+ function replaceSharedWorkspaceDependency(packageJson: PackageJson, replacement: string): boolean {
504
+ let changed = false;
505
+
506
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies'] as const) {
507
+ const dependencies = packageJson[section];
508
+ if (!dependencies) continue;
509
+
510
+ for (const [name, version] of Object.entries(dependencies)) {
511
+ if (version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
512
+ dependencies[name] = replacement;
513
+ changed = true;
514
+ }
515
+ }
516
+ }
517
+
518
+ return changed;
519
+ }
520
+
521
+ function normalizePackageJson(filePath: string, replacement: string): void {
522
+ if (!fs.existsSync(filePath)) return;
523
+
524
+ const packageJson = readJson(filePath);
525
+ if (!replaceSharedWorkspaceDependency(packageJson, replacement)) return;
526
+
527
+ rememberOriginal(filePath);
528
+ writeJson(filePath, packageJson);
529
+ }
530
+
531
+ function hideNodeModules(directory: string): void {
532
+ const nodeModulesPath = path.join(directory, 'node_modules');
533
+ if (!fs.existsSync(nodeModulesPath)) return;
534
+
535
+ const hiddenPath = path.join(directory, `.node_modules.swallowkit-npm-lockfile-${process.pid}`);
536
+ fs.renameSync(nodeModulesPath, hiddenPath);
537
+ hiddenNodeModules.set(nodeModulesPath, hiddenPath);
538
+ }
539
+
540
+ try {
541
+ normalizePackageJson(packageJsonPath, 'file:shared');
542
+ normalizePackageJson(functionsPackageJsonPath, 'file:../shared');
543
+ hideNodeModules(projectDir);
544
+ hideNodeModules(path.join(projectDir, 'shared'));
545
+ hideNodeModules(path.join(projectDir, 'functions'));
546
+ await action();
547
+ } finally {
548
+ for (const [nodeModulesPath, hiddenPath] of hiddenNodeModules) {
549
+ if (fs.existsSync(nodeModulesPath)) {
550
+ fs.rmSync(nodeModulesPath, { recursive: true, force: true });
551
+ }
552
+ fs.renameSync(hiddenPath, nodeModulesPath);
553
+ }
554
+
555
+ for (const [filePath, content] of originalFiles) {
556
+ fs.writeFileSync(filePath, content);
557
+ }
558
+ }
559
+ }
560
+
561
+ /**
562
+ * Run a package-manager command with stdio passed through to the user.
563
+ *
564
+ * For pnpm, additionally tee stdout/stderr to a buffer so that if the command
565
+ * fails due to `ERR_PNPM_IGNORED_BUILDS`, we can:
566
+ * 1. detect which packages were ignored,
567
+ * 2. interactively ask the user whether to approve them, and
568
+ * 3. run `pnpm approve-builds` followed by a retry of the original command.
569
+ */
570
+ async function runPackageManagerCommand(
571
+ pm: PackageManager,
572
+ args: string[],
573
+ projectDir: string,
574
+ label: string,
575
+ ): Promise<void> {
576
+ const { code, output } = await spawnAndCapture(pm, args, projectDir, pm === 'pnpm');
577
+
578
+ if (code === 0) return;
579
+
580
+ if (pm === 'pnpm') {
581
+ const ignoredBuilds = parseIgnoredBuilds(output);
582
+ if (ignoredBuilds.length > 0) {
583
+ const approved = await maybeApproveBuilds(projectDir, ignoredBuilds);
584
+ if (approved) {
585
+ // Retry the original command now that build scripts are approved.
586
+ const retry = await spawnAndCapture(pm, args, projectDir, true);
587
+ if (retry.code === 0) return;
588
+ throw new Error(`${label} exited with code ${retry.code} after approving builds`);
589
+ }
590
+ }
591
+ }
592
+
593
+ throw new Error(`${label} exited with code ${code}`);
594
+ }
595
+
596
+ /**
597
+ * Spawn a child process inheriting stdin (so interactive prompts still work)
598
+ * while either inheriting or capturing+teeing stdout/stderr.
599
+ */
600
+ function spawnAndCapture(
601
+ pm: PackageManager,
602
+ args: string[],
603
+ projectDir: string,
604
+ capture: boolean,
605
+ ): Promise<{ code: number; output: string }> {
606
+ return new Promise((resolve, reject) => {
607
+ const stdio: ('inherit' | 'pipe')[] = capture
608
+ ? ['inherit', 'pipe', 'pipe']
609
+ : ['inherit', 'inherit', 'inherit'];
610
+
611
+ const child = spawn(pm, args, {
612
+ cwd: projectDir,
613
+ stdio,
614
+ shell: true,
615
+ });
616
+
617
+ let combined = '';
618
+ if (capture) {
619
+ child.stdout?.on('data', (chunk: Buffer) => {
620
+ process.stdout.write(chunk);
621
+ combined += chunk.toString('utf8');
622
+ });
623
+ child.stderr?.on('data', (chunk: Buffer) => {
624
+ process.stderr.write(chunk);
625
+ combined += chunk.toString('utf8');
626
+ });
627
+ }
628
+
629
+ child.on('close', (code) => resolve({ code: code ?? 0, output: combined }));
630
+ child.on('error', reject);
631
+ });
632
+ }
633
+
634
+ /**
635
+ * Parse pnpm's `Ignored build scripts: foo@1.0.0, bar@2.0.0` warning/error and
636
+ * return the bare package names (without versions), de-duplicated.
637
+ */
638
+ export function parseIgnoredBuilds(output: string): string[] {
639
+ const match = output.match(/Ignored build scripts:\s*([^\n\r]+)/i);
640
+ if (!match) return [];
641
+ const list = match[1]
642
+ .split(',')
643
+ .map((entry) => entry.trim())
644
+ // strip version suffix: `sharp@0.34.5` -> `sharp`, `@scope/pkg@1.0.0` -> `@scope/pkg`
645
+ .map((entry) => entry.replace(/@[^@]+$/, ''))
646
+ .filter((name) => name.length > 0);
647
+ return Array.from(new Set(list));
648
+ }
649
+
650
+ /**
651
+ * Prompt the user and, if approved, run `pnpm approve-builds` followed by
652
+ * `pnpm rebuild <packages>`. Returns true if the user approved (regardless of
653
+ * which packages they actually selected inside approve-builds).
654
+ */
655
+ async function maybeApproveBuilds(projectDir: string, ignoredBuilds: string[]): Promise<boolean> {
656
+ console.log(
657
+ `\n⚠️ pnpm refused to run build scripts for: ${ignoredBuilds.join(', ')}\n` +
658
+ ' These packages (e.g. sharp) need their build scripts to run correctly.\n',
659
+ );
660
+
661
+ const response = await prompts({
662
+ type: 'confirm',
663
+ name: 'approve',
664
+ message: 'Run `pnpm approve-builds` now to approve these build scripts?',
665
+ initial: true,
666
+ });
667
+
668
+ if (!response.approve) {
669
+ console.log(
670
+ '\nℹ️ Skipped. You can run `pnpm approve-builds` later inside the project directory.\n',
671
+ );
672
+ return false;
673
+ }
674
+
675
+ await runSimple('pnpm', ['approve-builds'], projectDir);
676
+ await runSimple('pnpm', ['rebuild', ...ignoredBuilds], projectDir);
677
+
678
+ console.log('\n✅ Build scripts approved and packages rebuilt\n');
679
+ return true;
680
+ }
681
+
682
+ function runSimple(command: string, args: string[], cwd: string): Promise<void> {
683
+ return new Promise((resolve, reject) => {
684
+ const child = spawn(command, args, { cwd, stdio: 'inherit', shell: true });
685
+ child.on('close', (code) => {
686
+ if (code !== 0) reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`));
687
+ else resolve();
688
+ });
689
+ child.on('error', reject);
690
+ });
691
+ }
692
+
693
+ export function injectSwallowKitNextConfig(nextConfigContent: string, projectName: string): string {
694
+ return nextConfigContent.replace(
695
+ /(const\s+nextConfig[:\s]*(?::\s*NextConfig\s*)?=\s*\{)(\s*\/\*[^*]*\*\/)?/,
696
+ `$1\n output: 'standalone',\n transpilePackages: ['@${projectName}/shared'],\n serverExternalPackages: ['applicationinsights', 'diagnostic-channel-publishers'],$2`
697
+ );
698
+ }
699
+
700
+ export function buildCSharpFunctionsProgramSource(): string {
701
+ return `using Microsoft.Azure.Functions.Worker;
702
+ using Microsoft.Extensions.DependencyInjection;
703
+ using Microsoft.Extensions.Hosting;
704
+ using Microsoft.Azure.Functions.Worker.ApplicationInsights;
705
+
706
+ var host = new HostBuilder()
707
+ .ConfigureFunctionsWorkerDefaults()
708
+ .ConfigureServices(services =>
709
+ {
710
+ services.AddApplicationInsightsTelemetryWorkerService();
711
+ services.ConfigureFunctionsApplicationInsights();
712
+ })
713
+ .Build();
714
+
715
+ host.Run();
716
+ `;
717
+ }
718
+
719
+ export function buildCSharpFunctionsProjectSource(): string {
720
+ return `<Project Sdk="Microsoft.NET.Sdk">
721
+ <PropertyGroup>
722
+ <TargetFramework>net10.0</TargetFramework>
723
+ <AzureFunctionsVersion>v4</AzureFunctionsVersion>
724
+ <OutputType>Exe</OutputType>
725
+ <ImplicitUsings>enable</ImplicitUsings>
726
+ <Nullable>enable</Nullable>
727
+ </PropertyGroup>
728
+ <ItemGroup>
729
+ <Compile Remove="generated\\**\\bin\\**\\*.cs;generated\\**\\obj\\**\\*.cs" />
730
+ <EmbeddedResource Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />
731
+ <None Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />
732
+ </ItemGroup>
733
+ <ItemGroup>
734
+ <PackageReference Include="Microsoft.Azure.Cosmos" Version="3.47.0" />
735
+ <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
736
+ <PackageReference Include="Azure.Identity" Version="1.17.0" />
737
+ <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" />
738
+ <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
739
+ <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" OutputItemType="Analyzer" />
740
+ <PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" />
741
+ <PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
742
+ </ItemGroup>
743
+ </Project>
744
+ `;
745
+ }
746
+
747
+ export function buildSwallowKitConfigSource(backendLanguage: BackendLanguage): string {
748
+ return `module.exports = {
749
+ backend: {
750
+ language: '${backendLanguage}',
751
+ },
752
+ functions: {
753
+ baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
754
+ },
755
+ deployment: {
756
+ resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
757
+ swaName: process.env.AZURE_SWA_NAME || '',
758
+ },
759
+ }
760
+ `;
761
+ }
762
+
763
+ export function buildGeneratedProjectDependencies(
764
+ projectName: string,
765
+ pm: PackageManager = 'pnpm',
766
+ ): Record<string, string> {
767
+ return {
768
+ '@azure/cosmos': '^4.0.0',
769
+ 'applicationinsights': '^3.3.0',
770
+ 'zod': '^4.0.0',
771
+ [`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:shared',
772
+ };
773
+ }
774
+
775
+ function getSwallowKitPackageMetadata(): { name: string; version: string } {
776
+ const packageJsonPath = path.resolve(__dirname, "..", "..", "..", "package.json");
777
+
778
+ try {
779
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as {
780
+ name?: string;
781
+ version?: string;
782
+ };
783
+
784
+ return {
785
+ name: packageJson.name || "swallowkit",
786
+ version: packageJson.version || "latest",
787
+ };
788
+ } catch {
789
+ // Fall back to the published package name when package metadata is unavailable.
790
+ }
791
+
792
+ return {
793
+ name: "swallowkit",
794
+ version: "latest",
795
+ };
796
+ }
797
+
798
+ export function buildGeneratedProjectDevDependencies(): Record<string, string> {
799
+ const { name, version } = getSwallowKitPackageMetadata();
800
+ return {
801
+ [name]: version,
802
+ };
803
+ }
804
+
805
+ export function buildSwallowKitMcpProjectConfigSource(pm: PackageManager = 'pnpm'): string {
806
+ const { name } = getSwallowKitPackageMetadata();
807
+ return JSON.stringify(
808
+ {
809
+ mcpServers: {
810
+ swallowkit: {
811
+ command: pm === 'pnpm' ? 'pnpm' : 'npx',
812
+ args: pm === 'pnpm'
813
+ ? ["dlx", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"]
814
+ : ["--yes", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"],
815
+ cwd: ".",
816
+ env: {
817
+ SWALLOWKIT_MCP_VERSION: "latest",
818
+ },
819
+ },
820
+ },
821
+ },
822
+ null,
823
+ 2
824
+ );
825
+ }
826
+
827
+ async function addSwallowKitFiles(
828
+ projectDir: string,
829
+ options: InitOptions,
830
+ cicdChoice: string,
831
+ azureConfig: AzureConfig,
832
+ pm: PackageManager,
833
+ backendLanguage: BackendLanguage
834
+ ) {
835
+ console.log('📦 Adding SwallowKit files...\n');
836
+
837
+ const projectName = options.name;
838
+
839
+ // 1. Update package.json to add runtime dependencies for generated projects
840
+ const packageJsonPath = path.join(projectDir, 'package.json');
841
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
842
+
843
+ // zod is in the shared workspace package, not here
844
+ packageJson.dependencies = {
845
+ ...packageJson.dependencies,
846
+ ...buildGeneratedProjectDependencies(projectName, pm),
847
+ };
848
+ packageJson.devDependencies = {
849
+ ...packageJson.devDependencies,
850
+ ...buildGeneratedProjectDevDependencies(),
851
+ };
852
+
853
+ packageJson.scripts = {
854
+ ...packageJson.scripts,
855
+ 'build': getBuildScript(pm),
856
+ 'start': 'next start',
857
+ 'functions:start': getFunctionsStartScript(pm, backendLanguage),
858
+ };
859
+
860
+ packageJson.engines = {
861
+ node: '20.x',
862
+ };
863
+
864
+ // Keep package.json workspaces for npm-based CI/CD even when local development uses pnpm.
865
+ const workspacePackages = usesNodeFunctionsProject(backendLanguage) ? ['shared', 'functions'] : ['shared'];
866
+ const wsConfig = getWorkspaceConfig(pm, workspacePackages);
867
+ packageJson.workspaces = workspacePackages;
868
+
869
+ if (wsConfig.type !== 'file') {
870
+ packageJson.workspaces = wsConfig.value;
871
+ }
872
+
873
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
874
+
875
+ // Create pnpm-workspace.yaml for local pnpm development when needed.
876
+ if (wsConfig.type === 'file') {
877
+ fs.writeFileSync(path.join(projectDir, wsConfig.filename), wsConfig.content);
878
+ }
879
+
880
+ // Don't install yet — wait until all workspace packages (shared, functions) are created
881
+
882
+ // 2. Update next.config to add standalone output
883
+ // Check for both .ts and .js variants
884
+ let nextConfigPath = path.join(projectDir, 'next.config.ts');
885
+ if (!fs.existsSync(nextConfigPath)) {
886
+ nextConfigPath = path.join(projectDir, 'next.config.js');
887
+ }
888
+
889
+ if (fs.existsSync(nextConfigPath)) {
890
+ let nextConfigContent = fs.readFileSync(nextConfigPath, 'utf-8');
891
+
892
+ // Add output, transpiled workspace package, and server externals for standalone deployment
893
+ if (!nextConfigContent.includes("output:") && !nextConfigContent.includes('output =')) {
894
+ nextConfigContent = injectSwallowKitNextConfig(nextConfigContent, projectName);
895
+ fs.writeFileSync(nextConfigPath, nextConfigContent);
896
+ }
897
+ }
898
+
899
+ // 3. Update tsconfig.json to exclude functions directory
900
+ const tsconfigPath = path.join(projectDir, 'tsconfig.json');
901
+ if (fs.existsSync(tsconfigPath)) {
902
+ const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, 'utf-8'));
903
+ if (!tsconfig.exclude) {
904
+ tsconfig.exclude = [];
905
+ }
906
+ if (!tsconfig.exclude.includes('functions')) {
907
+ tsconfig.exclude.push('functions');
908
+ }
909
+ if (!tsconfig.exclude.includes('shared')) {
910
+ tsconfig.exclude.push('shared');
911
+ }
912
+ fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
913
+ }
914
+
915
+ // 3. Create SwallowKit config
916
+ const swallowkitConfig = buildSwallowKitConfigSource(backendLanguage);
917
+ fs.writeFileSync(path.join(projectDir, 'swallowkit.config.js'), swallowkitConfig);
918
+
919
+ // 4. Create shared workspace package for Zod models (Single Source of Truth)
920
+ await createSharedPackage(projectDir, projectName);
921
+
922
+ // Create lib directory for Next.js-specific utilities
923
+ const libDir = path.join(projectDir, 'lib');
924
+
925
+ // Create lib/api directory for backend utilities
926
+ const apiLibDir = path.join(libDir, 'api');
927
+ fs.mkdirSync(apiLibDir, { recursive: true });
928
+
929
+ // Create backend utility for calling Azure Functions
930
+ const backendUtilContent = `// Get Functions base URL at runtime (not at build time)
931
+ function getFunctionsBaseUrl(): string {
932
+ return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
933
+ }
934
+
935
+ function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string> {
936
+ const key = process.env.BACKEND_FUNCTIONS_KEY;
937
+ if (key) return { 'x-functions-key': key };
938
+ if (functionsBaseUrl.startsWith('http://localhost:') || functionsBaseUrl.startsWith('http://127.0.0.1:')) return {};
939
+ throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
940
+ }
941
+
942
+ /**
943
+ * Simple HTTP client for calling backend APIs
944
+ * Use this to make requests to BFF API routes (which forward to Azure Functions)
945
+ */
946
+ async function request<T>(
947
+ endpoint: string,
948
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE',
949
+ body?: any,
950
+ queryParams?: Record<string, string>
951
+ ): Promise<T> {
952
+ const functionsBaseUrl = getFunctionsBaseUrl();
953
+ const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
954
+ let url = \`\${functionsBaseUrl}\${endpoint}\`;
955
+ if (queryParams) {
956
+ const params = new URLSearchParams(queryParams);
957
+ url += \`?\${params.toString()}\`;
958
+ }
959
+
960
+ try {
961
+ const response = await fetch(url, {
962
+ method,
963
+ headers: {
964
+ 'Content-Type': 'application/json',
965
+ ...keyHeaders,
966
+ },
967
+ body: body ? JSON.stringify(body) : undefined,
968
+ });
969
+
970
+ if (!response.ok) {
971
+ const text = await response.text();
972
+ let errorMessage = text || 'Failed to call backend function';
973
+ try {
974
+ const error = JSON.parse(text);
975
+ errorMessage = error.error || error.message || text;
976
+ } catch {
977
+ // If not JSON, use text as-is
978
+ }
979
+ throw new Error(errorMessage);
980
+ }
981
+
982
+ const contentType = response.headers.get('content-type');
983
+ if (!contentType?.includes('application/json')) {
984
+ const text = await response.text();
985
+ return text as T;
986
+ }
987
+
988
+ return await response.json();
989
+ } catch (error) {
990
+ console.error('Error calling backend:', error);
991
+ throw error;
992
+ }
993
+ }
994
+
995
+ /**
996
+ * Generic API client for making HTTP requests
997
+ * Simply calls endpoints - no DB dependencies, no schema validation
998
+ * Validation happens on the backend (BFF/Functions)
999
+ *
1000
+ * @example
1001
+ * // Call custom endpoint
1002
+ * await api.get('/api/greet?name=World')
1003
+ *
1004
+ * // Call scaffolded CRUD endpoints
1005
+ * await api.get('/api/todos')
1006
+ * await api.post('/api/todos', { title: 'New task' })
1007
+ * await api.put('/api/todos/123', { title: 'Updated' })
1008
+ * await api.delete('/api/todos/123')
1009
+ */
1010
+ export const api = {
1011
+ /**
1012
+ * Make a GET request
1013
+ */
1014
+ get: <T>(endpoint: string, params?: Record<string, string>): Promise<T> => {
1015
+ return request<T>(endpoint, 'GET', undefined, params);
1016
+ },
1017
+
1018
+ /**
1019
+ * Make a POST request
1020
+ */
1021
+ post: <T>(endpoint: string, body?: any): Promise<T> => {
1022
+ return request<T>(endpoint, 'POST', body);
1023
+ },
1024
+
1025
+ /**
1026
+ * Make a PUT request
1027
+ */
1028
+ put: <T>(endpoint: string, body?: any): Promise<T> => {
1029
+ return request<T>(endpoint, 'PUT', body);
1030
+ },
1031
+
1032
+ /**
1033
+ * Make a DELETE request
1034
+ */
1035
+ delete: <T>(endpoint: string): Promise<T> => {
1036
+ return request<T>(endpoint, 'DELETE');
1037
+ },
1038
+ };
1039
+ `;
1040
+ fs.writeFileSync(path.join(apiLibDir, 'backend.ts'), backendUtilContent);
1041
+
1042
+ // 5. Create components directory
1043
+ const componentsDir = path.join(projectDir, 'components');
1044
+ fs.mkdirSync(componentsDir, { recursive: true });
1045
+
1046
+ // 6. Create .env.example
1047
+ const envExample = `# Azure Functions Backend URL
1048
+ BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071
1049
+ BACKEND_FUNCTIONS_KEY=
1050
+
1051
+ # Azure Configuration
1052
+ AZURE_RESOURCE_GROUP=your-resource-group
1053
+ AZURE_SWA_NAME=your-static-web-app-name
1054
+ `;
1055
+ fs.writeFileSync(path.join(projectDir, '.env.example'), envExample);
1056
+
1057
+ // 7. Create instrumentation.ts for Application Insights (Next.js official way)
1058
+ const instrumentationContent = `// Application Insights instrumentation for Next.js
1059
+ // This file is automatically loaded by Next.js when instrumentationHook is enabled
1060
+ export async function register() {
1061
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
1062
+ // Only run on server-side
1063
+ const connectionString = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING;
1064
+
1065
+ if (connectionString) {
1066
+ const appInsights = await import('applicationinsights');
1067
+
1068
+ appInsights
1069
+ .setup(connectionString)
1070
+ .setAutoCollectConsole(true)
1071
+ .setAutoCollectDependencies(true)
1072
+ .setAutoCollectExceptions(true)
1073
+ .setAutoCollectHeartbeat(true)
1074
+ .setAutoCollectPerformance(true, true)
1075
+ .setAutoCollectRequests(true)
1076
+ .setAutoDependencyCorrelation(true)
1077
+ .setDistributedTracingMode(appInsights.DistributedTracingModes.AI_AND_W3C)
1078
+ .setSendLiveMetrics(true)
1079
+ .setUseDiskRetryCaching(true);
1080
+
1081
+ appInsights.defaultClient.setAutoPopulateAzureProperties();
1082
+ appInsights.start();
1083
+
1084
+ // Override console methods to send to Application Insights
1085
+ const originalConsoleLog = console.log;
1086
+ const originalConsoleError = console.error;
1087
+ const originalConsoleWarn = console.warn;
1088
+
1089
+ console.log = function(...args: any[]) {
1090
+ originalConsoleLog.apply(console, args);
1091
+ const message = args.map(arg =>
1092
+ typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
1093
+ ).join(' ');
1094
+ appInsights.defaultClient.trackTrace({
1095
+ message: message,
1096
+ severity: '1'
1097
+ });
1098
+ };
1099
+
1100
+ console.error = function(...args: any[]) {
1101
+ originalConsoleError.apply(console, args);
1102
+ const message = args.map(arg =>
1103
+ typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
1104
+ ).join(' ');
1105
+ appInsights.defaultClient.trackTrace({
1106
+ message: message,
1107
+ severity: '3'
1108
+ });
1109
+ };
1110
+
1111
+ console.warn = function(...args: any[]) {
1112
+ originalConsoleWarn.apply(console, args);
1113
+ const message = args.map(arg =>
1114
+ typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
1115
+ ).join(' ');
1116
+ appInsights.defaultClient.trackTrace({
1117
+ message: message,
1118
+ severity: '2'
1119
+ });
1120
+ };
1121
+
1122
+ console.log('[App Insights] Initialized for Next.js server-side telemetry with console override');
1123
+ } else {
1124
+ console.log('[App Insights] Not configured (skipped in development mode)');
1125
+ }
1126
+ }
1127
+ }
1128
+ `;
1129
+ fs.writeFileSync(path.join(projectDir, 'instrumentation.ts'), instrumentationContent);
1130
+
1131
+ // 8. Create .env.local for local development
1132
+ const envLocalContent = [
1133
+ '# Azure Functions Backend URL (Local)',
1134
+ 'BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071',
1135
+ 'BACKEND_FUNCTIONS_KEY=',
1136
+ ''
1137
+ ].join('\n');
1138
+ fs.writeFileSync(path.join(projectDir, '.env.local'), envLocalContent);
1139
+
1140
+ // 8. Create staticwebapp.config.json for Azure Static Web Apps (Next.js Hybrid Rendering)
1141
+ const swaConfig = {
1142
+ platform: {
1143
+ apiRuntime: "node:20"
1144
+ },
1145
+ routes: [
1146
+ {
1147
+ route: "/*",
1148
+ allowedRoles: ["anonymous"]
1149
+ }
1150
+ ],
1151
+ globalHeaders: {
1152
+ "X-Content-Type-Options": "nosniff",
1153
+ "X-Frame-Options": "DENY",
1154
+ "X-XSS-Protection": "1; mode=block"
1155
+ },
1156
+ mimeTypes: {
1157
+ ".json": "application/json"
1158
+ }
1159
+ };
1160
+ fs.writeFileSync(
1161
+ path.join(projectDir, 'staticwebapp.config.json'),
1162
+ JSON.stringify(swaConfig, null, 2)
1163
+ );
1164
+
1165
+ // 14. Create Azure Functions project
1166
+ await createAzureFunctionsProject(projectDir, pm, backendLanguage);
1167
+
1168
+ // 15. Create BFF API route to call Azure Functions
1169
+ await createBffApiRoute(projectDir);
1170
+
1171
+ // 16. Create home page
1172
+ await createHomePage(projectDir, pm);
1173
+
1174
+ // 17. Install all workspace dependencies (root + shared + functions)
1175
+ console.log('📦 Installing workspace dependencies...\n');
1176
+ await installDependencies(projectDir, pm);
1177
+
1178
+ console.log('✅ Project structure created\n');
1179
+
1180
+ // 18. Create README.md
1181
+ createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage);
1182
+
1183
+ // 19. Create AI agent instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
1184
+ createAiAgentFiles(projectDir, projectName, backendLanguage, pm);
1185
+ }
1186
+
1187
+ async function createSharedPackage(projectDir: string, projectName: string) {
1188
+ console.log('📦 Creating shared workspace package for Zod models...\n');
1189
+
1190
+ const sharedDir = path.join(projectDir, 'shared');
1191
+ const modelsDir = path.join(sharedDir, 'models');
1192
+ fs.mkdirSync(modelsDir, { recursive: true });
1193
+
1194
+ // shared/package.json
1195
+ const sharedPackageJson = {
1196
+ private: true,
1197
+ name: `@${projectName}/shared`,
1198
+ version: '1.0.0',
1199
+ description: 'Shared Zod models — Single Source of Truth for validation schemas',
1200
+ main: 'dist/index.js',
1201
+ types: 'dist/index.d.ts',
1202
+ scripts: {
1203
+ build: 'tsc',
1204
+ watch: 'tsc --watch',
1205
+ },
1206
+ dependencies: {
1207
+ 'zod': '>=3.25.0',
1208
+ },
1209
+ devDependencies: {
1210
+ 'typescript': '^5.0.0',
1211
+ },
1212
+ };
1213
+ fs.writeFileSync(
1214
+ path.join(sharedDir, 'package.json'),
1215
+ JSON.stringify(sharedPackageJson, null, 2)
1216
+ );
1217
+
1218
+ // shared/tsconfig.json
1219
+ fs.writeFileSync(
1220
+ path.join(sharedDir, 'tsconfig.json'),
1221
+ JSON.stringify(buildSharedTsConfig(), null, 2)
1222
+ );
1223
+
1224
+ // shared/index.ts (empty re-export file, scaffold will add entries)
1225
+ fs.writeFileSync(
1226
+ path.join(sharedDir, 'index.ts'),
1227
+ `// Shared Zod models — auto-managed by SwallowKit scaffold command\n// Do not edit the export list below manually\n`
1228
+ );
1229
+
1230
+ // shared/.gitignore
1231
+ fs.writeFileSync(
1232
+ path.join(sharedDir, '.gitignore'),
1233
+ `node_modules\ndist\n`
1234
+ );
1235
+
1236
+ console.log('✅ Shared package created\n');
1237
+ }
1238
+
1239
+ async function createAzureFunctionsProject(
1240
+ projectDir: string,
1241
+ pm: PackageManager = 'pnpm',
1242
+ backendLanguage: BackendLanguage = 'typescript'
1243
+ ) {
1244
+ console.log(`📦 Creating Azure Functions project (${getBackendLanguageLabel(backendLanguage)})...\n`);
1245
+
1246
+ const functionsDir = path.join(projectDir, 'functions');
1247
+ fs.mkdirSync(functionsDir, { recursive: true });
1248
+
1249
+ const projectName = path.basename(projectDir);
1250
+ const databaseName = `${projectName.charAt(0).toUpperCase() + projectName.slice(1)}Database`;
1251
+
1252
+ createFunctionsHostFiles(functionsDir, databaseName, backendLanguage);
1253
+
1254
+ if (backendLanguage === 'typescript') {
1255
+ createTypeScriptFunctionsProject(projectDir, functionsDir, pm);
1256
+ } else if (backendLanguage === 'csharp') {
1257
+ createCSharpFunctionsProject(projectDir, functionsDir);
1258
+ } else {
1259
+ createPythonFunctionsProject(projectDir, functionsDir);
1260
+ }
1261
+
1262
+ console.log('✅ Azure Functions project created\n');
1263
+ }
1264
+
1265
+ function createFunctionsHostFiles(functionsDir: string, databaseName: string, backendLanguage: BackendLanguage): void {
1266
+ const hostJson = {
1267
+ version: '2.0',
1268
+ logging: {
1269
+ applicationInsights: {
1270
+ samplingSettings: {
1271
+ isEnabled: true,
1272
+ maxTelemetryItemsPerSecond: 20,
1273
+ },
1274
+ },
1275
+ },
1276
+ extensionBundle: {
1277
+ id: 'Microsoft.Azure.Functions.ExtensionBundle',
1278
+ version: '[4.0.0, 4.10.0)',
1279
+ },
1280
+ };
1281
+ fs.writeFileSync(path.join(functionsDir, 'host.json'), JSON.stringify(hostJson, null, 2));
1282
+
1283
+ const localSettings = {
1284
+ IsEncrypted: false,
1285
+ Values: {
1286
+ AzureWebJobsStorage: '',
1287
+ FUNCTIONS_WORKER_RUNTIME: getFunctionsWorkerRuntime(backendLanguage),
1288
+ AzureWebJobsFeatureFlags: 'EnableWorkerIndexing',
1289
+ CosmosDBConnection: 'AccountEndpoint=http://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==',
1290
+ COSMOS_DB_DATABASE_NAME: databaseName,
1291
+ NODE_TLS_REJECT_UNAUTHORIZED: '0',
1292
+ },
1293
+ };
1294
+ fs.writeFileSync(path.join(functionsDir, 'local.settings.json'), JSON.stringify(localSettings, null, 2));
1295
+
1296
+ const gitignoreLines = [
1297
+ 'local.settings.json',
1298
+ '*.log',
1299
+ '.vscode',
1300
+ '.DS_Store',
1301
+ ];
1302
+
1303
+ if (backendLanguage === 'typescript') {
1304
+ gitignoreLines.unshift('node_modules', 'dist');
1305
+ fs.writeFileSync(path.join(functionsDir, '.funcignore'), `node_modules
1306
+ .git
1307
+ .vscode
1308
+ local.settings.json
1309
+ test
1310
+ tsconfig.json
1311
+ *.ts
1312
+ !dist/**/*.js
1313
+ `);
1314
+ } else if (backendLanguage === 'python') {
1315
+ gitignoreLines.unshift('.venv', '.codegen-venv', '__pycache__', '.python_packages');
1316
+ fs.writeFileSync(path.join(functionsDir, '.funcignore'), `.venv
1317
+ .codegen-venv
1318
+ __pycache__
1319
+ .pytest_cache
1320
+ .mypy_cache
1321
+ .ruff_cache
1322
+ local.settings.json
1323
+ tests
1324
+ `);
1325
+ } else {
1326
+ gitignoreLines.unshift('bin', 'obj');
1327
+ fs.writeFileSync(path.join(functionsDir, '.funcignore'), `bin
1328
+ obj
1329
+ local.settings.json
1330
+ tests
1331
+ `);
1332
+ }
1333
+
1334
+ fs.writeFileSync(path.join(functionsDir, '.gitignore'), `${gitignoreLines.join('\n')}\n`);
1335
+ }
1336
+
1337
+ export function buildTypeScriptFunctionsPackageJson(projectName: string, pm: PackageManager = 'pnpm') {
1338
+ return {
1339
+ name: 'functions',
1340
+ version: '1.0.0',
1341
+ description: 'Azure Functions backend',
1342
+ main: 'dist/*.js',
1343
+ scripts: {
1344
+ start: 'func start',
1345
+ build: 'tsc',
1346
+ prestart: getFunctionsPrestart(pm),
1347
+ },
1348
+ dependencies: {
1349
+ '@azure/functions': '~4.5.0',
1350
+ '@azure/cosmos': '^4.0.0',
1351
+ '@azure/identity': '^4.0.0',
1352
+ zod: '>=3.25.0',
1353
+ [`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:../shared',
1354
+ },
1355
+ devDependencies: {
1356
+ '@types/node': '^20.0.0',
1357
+ typescript: '^5.0.0',
1358
+ },
1359
+ };
1360
+ }
1361
+
1362
+ function createTypeScriptFunctionsProject(projectDir: string, functionsDir: string, pm: PackageManager): void {
1363
+ const functionsPackageJson = buildTypeScriptFunctionsPackageJson(path.basename(projectDir), pm);
1364
+ fs.writeFileSync(path.join(functionsDir, 'package.json'), JSON.stringify(functionsPackageJson, null, 2));
1365
+
1366
+ const sharedPkgName = `@${path.basename(projectDir)}/shared`;
1367
+ const functionsTsConfig = {
1368
+ compilerOptions: {
1369
+ target: 'ES2020',
1370
+ module: 'commonjs',
1371
+ moduleResolution: 'node',
1372
+ lib: ['ES2020'],
1373
+ outDir: 'dist',
1374
+ rootDir: 'src',
1375
+ strict: true,
1376
+ esModuleInterop: true,
1377
+ skipLibCheck: true,
1378
+ forceConsistentCasingInFileNames: true,
1379
+ paths: {
1380
+ [sharedPkgName]: ['../shared'],
1381
+ [`${sharedPkgName}/*`]: ['../shared/*'],
1382
+ },
1383
+ },
1384
+ include: ['src/**/*'],
1385
+ exclude: ['node_modules', 'dist'],
1386
+ };
1387
+ fs.writeFileSync(path.join(functionsDir, 'tsconfig.json'), JSON.stringify(functionsTsConfig, null, 2));
1388
+
1389
+ const srcDir = path.join(functionsDir, 'src');
1390
+ fs.mkdirSync(srcDir, { recursive: true });
1391
+ fs.writeFileSync(path.join(srcDir, 'greet.ts'), `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
1392
+ import { z } from 'zod/v4';
1393
+
1394
+ const greetRequestSchema = z.object({
1395
+ name: z.string().min(1, 'Name is required').max(50, 'Name must be less than 50 characters'),
1396
+ });
1397
+
1398
+ export async function greet(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
1399
+ context.log('HTTP trigger function processed a request.');
1400
+
1401
+ try {
1402
+ const name = request.query.get('name') || (await request.text());
1403
+ const result = greetRequestSchema.safeParse({ name });
1404
+
1405
+ if (!result.success) {
1406
+ return {
1407
+ status: 400,
1408
+ jsonBody: {
1409
+ error: result.error.issues[0].message
1410
+ }
1411
+ };
1412
+ }
1413
+
1414
+ const greeting = \`Hello, \${result.data.name}! This message is from Azure Functions.\`;
1415
+
1416
+ return {
1417
+ status: 200,
1418
+ jsonBody: {
1419
+ message: greeting,
1420
+ timestamp: new Date().toISOString()
1421
+ }
1422
+ };
1423
+ } catch (error) {
1424
+ context.error('Error processing request:', error);
1425
+ return {
1426
+ status: 500,
1427
+ jsonBody: {
1428
+ error: 'Internal server error'
1429
+ }
1430
+ };
1431
+ }
1432
+ }
1433
+
1434
+ app.http('greet', {
1435
+ methods: ['GET', 'POST'],
1436
+ authLevel: 'function',
1437
+ handler: greet
1438
+ });
1439
+ `);
1440
+ }
1441
+
1442
+ function createCSharpFunctionsProject(projectDir: string, functionsDir: string): void {
1443
+ const projectBaseName = path.basename(projectDir);
1444
+ const projectPascal = projectBaseName.charAt(0).toUpperCase() + projectBaseName.slice(1);
1445
+ const csprojName = `${projectPascal}.Functions.csproj`;
1446
+
1447
+ fs.writeFileSync(path.join(functionsDir, csprojName), buildCSharpFunctionsProjectSource());
1448
+ fs.mkdirSync(path.join(functionsDir, '.config'), { recursive: true });
1449
+ fs.writeFileSync(
1450
+ path.join(functionsDir, '.config', 'dotnet-tools.json'),
1451
+ buildCSharpCodegenToolManifestSource()
1452
+ );
1453
+
1454
+ fs.writeFileSync(path.join(functionsDir, 'Program.cs'), buildCSharpFunctionsProgramSource());
1455
+
1456
+ const crudDir = path.join(functionsDir, 'Crud');
1457
+ fs.mkdirSync(crudDir, { recursive: true });
1458
+ fs.writeFileSync(path.join(crudDir, 'GreetFunction.cs'), `using System.Net;
1459
+ using Microsoft.Azure.Functions.Worker;
1460
+ using Microsoft.Azure.Functions.Worker.Http;
1461
+
1462
+ namespace SwallowKit.Functions;
1463
+
1464
+ public sealed class GreetFunction
1465
+ {
1466
+ [Function("greet")]
1467
+ public async Task<HttpResponseData> Run(
1468
+ [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "greet")] HttpRequestData request)
1469
+ {
1470
+ var query = request.Url.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
1471
+ var name = "SwallowKit";
1472
+ foreach (var segment in query)
1473
+ {
1474
+ var parts = segment.Split('=', 2);
1475
+ if (parts.Length == 2 && parts[0] == "name")
1476
+ {
1477
+ name = Uri.UnescapeDataString(parts[1]);
1478
+ break;
1479
+ }
1480
+ }
1481
+ var response = request.CreateResponse(HttpStatusCode.OK);
1482
+ await response.WriteAsJsonAsync(new
1483
+ {
1484
+ message = $"Hello, {name}! This message is from Azure Functions.",
1485
+ timestamp = DateTimeOffset.UtcNow.ToString("O"),
1486
+ });
1487
+ return response;
1488
+ }
1489
+ }
1490
+ `);
1491
+ }
1492
+
1493
+ function createPythonFunctionsProject(projectDir: string, functionsDir: string): void {
1494
+ fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
1495
+ ensureProjectGitignoreEntry(projectDir, '.uv');
1496
+
1497
+ fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
1498
+ azure-cosmos>=4.9.0
1499
+ azure-identity>=1.19.0
1500
+ `);
1501
+ fs.writeFileSync(
1502
+ path.join(functionsDir, 'requirements.codegen.txt'),
1503
+ buildPythonCodegenRequirementsSource()
1504
+ );
1505
+
1506
+ const blueprintsDir = path.join(functionsDir, 'blueprints');
1507
+ fs.mkdirSync(blueprintsDir, { recursive: true });
1508
+ fs.writeFileSync(path.join(blueprintsDir, '__init__.py'), '');
1509
+
1510
+ fs.writeFileSync(path.join(blueprintsDir, 'greet.py'), `import json
1511
+ from datetime import datetime, timezone
1512
+
1513
+ import azure.functions as func
1514
+
1515
+ bp = func.Blueprint()
1516
+
1517
+
1518
+ @bp.route(route="greet", methods=["GET", "POST"])
1519
+ def greet(req: func.HttpRequest) -> func.HttpResponse:
1520
+ name = req.params.get("name") or "SwallowKit"
1521
+ payload = {
1522
+ "message": f"Hello, {name}! This message is from Azure Functions.",
1523
+ "timestamp": datetime.now(timezone.utc).isoformat(),
1524
+ }
1525
+ return func.HttpResponse(
1526
+ body=json.dumps(payload, ensure_ascii=False),
1527
+ status_code=200,
1528
+ mimetype="application/json",
1529
+ )
1530
+ `);
1531
+
1532
+ fs.writeFileSync(path.join(functionsDir, 'function_app.py'), `import azure.functions as func
1533
+
1534
+ from blueprints.greet import bp as greet_bp
1535
+
1536
+ app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
1537
+
1538
+ app.register_blueprint(greet_bp)
1539
+ # SwallowKit scaffold registrations
1540
+ `);
1541
+ }
1542
+
1543
+ function ensureProjectGitignoreEntry(projectDir: string, entry: string): void {
1544
+ const gitignorePath = path.join(projectDir, '.gitignore');
1545
+ if (!fs.existsSync(gitignorePath)) {
1546
+ return;
1547
+ }
1548
+
1549
+ const current = fs.readFileSync(gitignorePath, 'utf8');
1550
+ const lines = current.split(/\r?\n/);
1551
+ if (lines.includes(entry)) {
1552
+ return;
1553
+ }
1554
+
1555
+ const normalized = current.endsWith('\n') ? current : `${current}\n`;
1556
+ fs.writeFileSync(gitignorePath, `${normalized}${entry}\n`);
1557
+ }
1558
+
1559
+ async function createBffApiRoute(projectDir: string) {
1560
+ console.log('📦 Creating BFF API route...\n');
1561
+
1562
+ const apiDir = path.join(projectDir, 'app', 'api', 'greet');
1563
+ fs.mkdirSync(apiDir, { recursive: true });
1564
+
1565
+ // Create API route that calls Azure Functions using shared utility
1566
+ const apiRoute = `import { NextRequest, NextResponse } from 'next/server';
1567
+ import { api } from '@/lib/api/backend';
1568
+
1569
+ interface GreetResponse {
1570
+ message: string;
1571
+ timestamp: string;
1572
+ }
1573
+
1574
+ export async function GET(request: NextRequest) {
1575
+ try {
1576
+ const { searchParams } = new URL(request.url);
1577
+ const name = searchParams.get('name') || 'World';
1578
+
1579
+ const data = await api.get<GreetResponse>('/api/greet', { name });
1580
+
1581
+ return NextResponse.json(data);
1582
+ } catch (error) {
1583
+ console.error('Error calling Azure Functions:', error);
1584
+ const errorMessage = error instanceof Error ? error.message : 'Failed to call backend function';
1585
+ return NextResponse.json(
1586
+ { error: errorMessage, details: 'Make sure Azure Functions is running on port 7071' },
1587
+ { status: 500 }
1588
+ );
1589
+ }
1590
+ }
1591
+
1592
+ export async function POST(request: NextRequest) {
1593
+ try {
1594
+ const body = await request.json();
1595
+
1596
+ const data = await api.post<GreetResponse>('/api/greet', body);
1597
+
1598
+ return NextResponse.json(data);
1599
+ } catch (error) {
1600
+ console.error('Error calling Azure Functions:', error);
1601
+ const errorMessage = error instanceof Error ? error.message : 'Failed to call backend function';
1602
+ return NextResponse.json(
1603
+ { error: errorMessage, details: 'Make sure Azure Functions is running on port 7071' },
1604
+ { status: 500 }
1605
+ );
1606
+ }
1607
+ }
1608
+ `;
1609
+ fs.writeFileSync(path.join(apiDir, 'route.ts'), apiRoute);
1610
+
1611
+ // Update .env.example to include BACKEND_FUNCTIONS_BASE_URL
1612
+ const envExamplePath = path.join(projectDir, '.env.example');
1613
+ let envExample = fs.readFileSync(envExamplePath, 'utf-8');
1614
+
1615
+ if (!envExample.includes('BACKEND_FUNCTIONS_BASE_URL')) {
1616
+ envExample += `\n# Azure Functions Backend URL\nBACKEND_FUNCTIONS_BASE_URL=http://localhost:7071\n`;
1617
+ fs.writeFileSync(envExamplePath, envExample);
1618
+ }
1619
+
1620
+ // Update .env.local
1621
+ const envLocalPath = path.join(projectDir, '.env.local');
1622
+ let envLocal = fs.readFileSync(envLocalPath, 'utf-8');
1623
+
1624
+ if (!envLocal.includes('BACKEND_FUNCTIONS_BASE_URL')) {
1625
+ envLocal += `\n# Azure Functions Backend URL (Local)\nBACKEND_FUNCTIONS_BASE_URL=http://localhost:7071\n`;
1626
+ fs.writeFileSync(envLocalPath, envLocal);
1627
+ }
1628
+
1629
+ console.log('✅ BFF API route created\n');
1630
+ }
1631
+
1632
+ async function createHomePage(projectDir: string, pm: PackageManager = 'pnpm') {
1633
+ console.log('📦 Creating home page...\n');
1634
+
1635
+ const pmCmd = getCommands(pm);
1636
+
1637
+ const pageContent = `'use client'
1638
+
1639
+ export const dynamic = 'force-dynamic';
1640
+
1641
+ import { useState } from 'react';
1642
+ import { scaffoldConfig } from '@/lib/scaffold-config';
1643
+
1644
+ export default function Home() {
1645
+ const [greetingStatus, setGreetingStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
1646
+ const [message, setMessage] = useState('');
1647
+
1648
+ const testConnection = async () => {
1649
+ setGreetingStatus('loading');
1650
+ try {
1651
+ const response = await fetch('/api/greet?name=SwallowKit');
1652
+ const data = await response.json();
1653
+ if (!response.ok) {
1654
+ throw new Error(data.error || \`Server error: \${response.status}\`);
1655
+ }
1656
+ setMessage(data.message);
1657
+ setGreetingStatus('success');
1658
+ } catch (error) {
1659
+ setMessage(error instanceof Error ? error.message : 'Failed to connect to Azure Functions');
1660
+ setGreetingStatus('error');
1661
+ }
1662
+ };
1663
+
1664
+ return (
1665
+ <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-900 dark:to-gray-800">
1666
+ <div className="container mx-auto px-4 py-12">
1667
+ <header className="text-center mb-16">
1668
+ <h1 className="text-5xl font-bold text-gray-800 dark:text-white mb-4">
1669
+ Welcome to SwallowKit
1670
+ </h1>
1671
+ <p className="text-xl text-gray-600 dark:text-gray-400">
1672
+ Next.js on Azure Static Web Apps + Functions + Cosmos DB — Zod schema sharing
1673
+ </p>
1674
+ </header>
1675
+
1676
+ {/* Connection Test */}
1677
+ <section className="max-w-2xl mx-auto mb-12">
1678
+ <div className="bg-white dark:bg-gray-800 rounded-xl p-8 border border-gray-200 dark:border-gray-700">
1679
+ <h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
1680
+ Test BFF → Functions Connection
1681
+ </h2>
1682
+ <button
1683
+ onClick={testConnection}
1684
+ disabled={greetingStatus === 'loading'}
1685
+ className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-lg font-medium transition-colors"
1686
+ >
1687
+ {greetingStatus === 'loading' ? 'Testing...' : 'Test Connection'}
1688
+ </button>
1689
+ {greetingStatus === 'success' && (
1690
+ <div className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
1691
+ <p className="text-green-800 dark:text-green-200 font-medium">✅ Connection successful!</p>
1692
+ <p className="text-green-700 dark:text-green-300 text-sm mt-1">{message}</p>
1693
+ </div>
1694
+ )}
1695
+ {greetingStatus === 'error' && (
1696
+ <div className="mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
1697
+ <p className="text-red-800 dark:text-red-200 font-medium">❌ Connection failed</p>
1698
+ <p className="text-red-700 dark:text-red-300 text-sm mt-1">{message}</p>
1699
+ </div>
1700
+ )}
1701
+ </div>
1702
+ </section>
1703
+
1704
+ {/* Scaffolded Models Menu */}
1705
+ {scaffoldConfig.models.length > 0 ? (
1706
+ <section className="max-w-6xl mx-auto">
1707
+ <h2 className="text-3xl font-bold mb-8 text-gray-900 dark:text-gray-100">Your Models</h2>
1708
+ <div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
1709
+ {scaffoldConfig.models.map((model) => (
1710
+ <a
1711
+ key={model.name}
1712
+ href={model.path}
1713
+ className="block p-8 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl hover:shadow-lg hover:border-blue-400 dark:hover:border-blue-600 transition-all"
1714
+ >
1715
+ <h3 className="text-2xl font-semibold mb-2 text-gray-900 dark:text-gray-100">{model.label}</h3>
1716
+ <p className="text-gray-600 dark:text-gray-400">Manage {model.label.toLowerCase()}</p>
1717
+ </a>
1718
+ ))}
1719
+ </div>
1720
+ </section>
1721
+ ) : (
1722
+ <section className="max-w-2xl mx-auto text-center">
1723
+ <div className="bg-white dark:bg-gray-800 rounded-xl p-12 border border-gray-200 dark:border-gray-700">
1724
+ <h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Get Started</h2>
1725
+ <p className="text-gray-600 dark:text-gray-400 mb-6">
1726
+ Create your first model with Zod and generate CRUD operations automatically.
1727
+ </p>
1728
+ <code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
1729
+ ${pmCmd.dlx} swallowkit scaffold shared/models/your-model.ts
1730
+ </code>
1731
+ </div>
1732
+ </section>
1733
+ )}
1734
+
1735
+ <footer className="mt-16 text-center text-gray-600 dark:text-gray-400 text-sm">
1736
+ <p>Built with SwallowKit</p>
1737
+ </footer>
1738
+ </div>
1739
+ </div>
1740
+ );
1741
+ }
1742
+ `;
1743
+
1744
+ fs.writeFileSync(path.join(projectDir, 'app', 'page.tsx'), pageContent);
1745
+
1746
+ console.log('✅ Home page created\n');
1747
+
1748
+ // Create initial scaffold-config.ts
1749
+ const scaffoldConfigDir = path.join(projectDir, 'lib');
1750
+ if (!fs.existsSync(scaffoldConfigDir)) {
1751
+ fs.mkdirSync(scaffoldConfigDir, { recursive: true });
1752
+ }
1753
+
1754
+ const scaffoldConfigContent = `export interface ScaffoldModel {
1755
+ name: string;
1756
+ path: string;
1757
+ label: string;
1758
+ }
1759
+
1760
+ export const scaffoldConfig = {
1761
+ models: [
1762
+ // Scaffolded models will be added here by 'swallowkit scaffold' command
1763
+ ] as ScaffoldModel[]
1764
+ };
1765
+ `;
1766
+
1767
+ fs.writeFileSync(path.join(scaffoldConfigDir, 'scaffold-config.ts'), scaffoldConfigContent);
1768
+ console.log('✅ Scaffold config created\n');
1769
+ }
1770
+
1771
+ function createReadme(
1772
+ projectDir: string,
1773
+ projectName: string,
1774
+ cicdChoice: string,
1775
+ azureConfig: AzureConfig,
1776
+ pm: PackageManager,
1777
+ backendLanguage: BackendLanguage
1778
+ ) {
1779
+ console.log('📝 Creating README.md...\n');
1780
+
1781
+ const pmCmd = getCommands(pm);
1782
+ const cosmosDbModeLabel = azureConfig.cosmosDbMode === 'freetier' ? 'Free Tier (1000 RU/s)' : 'Serverless';
1783
+ const cicdLabel = cicdChoice === 'github' ? 'GitHub Actions' : cicdChoice === 'azure' ? 'Azure Pipelines' : 'None';
1784
+ const vnetLabel = azureConfig.vnetOption === 'none' ? 'None (public endpoints)' :
1785
+ 'Outbound VNet (Cosmos DB Private Endpoint)';
1786
+ const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
1787
+ const schemaBridgeDescription = backendLanguage === 'typescript'
1788
+ ? 'Zod (shared between frontend and backend)'
1789
+ : `Zod + OpenAPI export (Zod in shared/, native-generated ${backendLanguageLabel} schemas in functions/generated/)`;
1790
+ const functionsTree = backendLanguage === 'typescript'
1791
+ ? `│ └── src/\n│ └── greet.ts # Sample function`
1792
+ : backendLanguage === 'csharp'
1793
+ ? `│ ├── Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ # Native-generated C# schema assets`
1794
+ : `│ ├── blueprints/\n│ │ └── greet.py\n│ └── generated/ # Native-generated Python schema assets`;
1795
+ const backendScaffoldNote = backendLanguage === 'typescript'
1796
+ ? '- Azure Functions CRUD endpoints'
1797
+ : `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI export + native-generated ${backendLanguageLabel} schema assets`;
1798
+ const pythonUvPaths = getProjectLocalUvPaths(projectDir);
1799
+ const pythonLocalDevNote = backendLanguage === 'python'
1800
+ ? `\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`
1801
+ : '';
1802
+
1803
+ const readme = `# ${projectName}
1804
+
1805
+ A full-stack application built with **SwallowKit** - Next.js on Azure Static Web Apps + Functions + Cosmos DB with Zod schema sharing.
1806
+
1807
+ ## 🚀 Tech Stack
1808
+
1809
+ - **Frontend**: Next.js 15 (App Router), React, TypeScript, Tailwind CSS
1810
+ - **BFF (Backend for Frontend)**: Next.js API Routes
1811
+ - **Backend**: Azure Functions (${backendLanguageLabel})
1812
+ - **Database**: Azure Cosmos DB
1813
+ - **Schema Validation**: ${schemaBridgeDescription}
1814
+ - **Infrastructure**: Bicep (Infrastructure as Code)
1815
+ - **CI/CD**: ${cicdLabel}
1816
+
1817
+ ## 📋 Project Configuration
1818
+
1819
+ This project was initialized with the following settings:
1820
+
1821
+ - **Azure Functions Plan**: Flex Consumption
1822
+ - **Cosmos DB Mode**: ${cosmosDbModeLabel}
1823
+ - **Network Security**: ${vnetLabel}
1824
+ - **CI/CD**: ${cicdLabel}
1825
+
1826
+ ## ✅ Prerequisites
1827
+
1828
+ Before you begin, ensure you have the following installed:
1829
+
1830
+ 1. **Node.js 18+**: [Download](https://nodejs.org/)${pm === 'pnpm' ? `\n2. **pnpm**: \`corepack enable\` or \`npm install -g pnpm\`` : ''}
1831
+ ${pm === 'pnpm' ? '3' : '2'}. **Azure CLI**: Required for provisioning Azure resources
1832
+ - Install: \`winget install Microsoft.AzureCLI\` (Windows)
1833
+ - Or: [Download](https://aka.ms/installazurecliwindows)
1834
+ ${pm === 'pnpm' ? '4' : '3'}. **Azure Cosmos DB Emulator**: Required for local development
1835
+ - Windows: \`winget install Microsoft.Azure.CosmosEmulator\`
1836
+ - Or: [Download](https://aka.ms/cosmosdb-emulator)
1837
+ - Docker: \`docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator\`
1838
+ ${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools**: Automatically installed with project dependencies
1839
+
1840
+ ## 📁 Project Structure
1841
+
1842
+ \`\`\`
1843
+ ${projectName}/
1844
+ ├── app/ # Next.js App Router (frontend)
1845
+ │ ├── api/ # BFF API routes (proxy to Functions)
1846
+ │ └── page.tsx # Home page
1847
+ ├── functions/ # Azure Functions (backend)
1848
+ ${functionsTree}
1849
+ ├── lib/
1850
+ │ └── api/ # API client utilities
1851
+ ├── infra/ # Bicep infrastructure files
1852
+ │ ├── main.bicep
1853
+ │ └── modules/ # Bicep modules for each resource
1854
+ └── .github/workflows/ # CI/CD workflows
1855
+ \`\`\`
1856
+
1857
+ ## 🏗️ Getting Started
1858
+
1859
+ ### 1. Create Your First Model
1860
+
1861
+ Define your data model with Zod schema:
1862
+
1863
+ \`\`\`bash
1864
+ ${pmCmd.dlx} swallowkit create-model <model-name>
1865
+ \`\`\`
1866
+
1867
+ This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to define your schema.
1868
+
1869
+ ### 2. Generate CRUD Code
1870
+
1871
+ Generate complete CRUD operations (Functions, API routes, UI):
1872
+
1873
+ \`\`\`bash
1874
+ ${pmCmd.dlx} swallowkit scaffold shared/models/<model-name>.ts
1875
+ \`\`\`
1876
+
1877
+ This generates:
1878
+ ${backendScaffoldNote}
1879
+ - Next.js BFF API routes
1880
+ - React UI components (list, detail, create, edit)
1881
+ - Navigation menu integration
1882
+
1883
+ ### 3. Start Development Servers
1884
+
1885
+ \`\`\`bash
1886
+ ${pmCmd.dlx} swallowkit dev
1887
+ \`\`\`
1888
+
1889
+ This starts:
1890
+ - Next.js dev server (http://localhost:3000)
1891
+ - Azure Functions (http://localhost:7071)
1892
+ - Cosmos DB Emulator check (must be running separately)
1893
+
1894
+ **Note**: You need to start Cosmos DB Emulator manually before running \`swallowkit dev\`.
1895
+ ${pythonLocalDevNote}
1896
+
1897
+ ## ☁️ Deploy to Azure
1898
+
1899
+ ### Provision Azure Resources
1900
+
1901
+ Create all required Azure resources using Bicep:
1902
+
1903
+ \`\`\`bash
1904
+ ${pmCmd.dlx} swallowkit provision --resource-group <rg-name>
1905
+ \`\`\`
1906
+
1907
+ This creates:
1908
+ - Static Web App (\`swa-${projectName}\`)
1909
+ - Azure Functions (\`func-${projectName}\`)
1910
+ - Cosmos DB (\`cosmos-${projectName}\`)
1911
+ - Storage Account
1912
+
1913
+ You will be prompted to select Azure regions:
1914
+ 1. **Primary location**: For Functions and Cosmos DB (default: Japan East)
1915
+ 2. **Static Web App location**: Limited availability (default: East Asia)
1916
+
1917
+ ### CI/CD Setup
1918
+
1919
+ ${cicdChoice === 'github' ? `#### GitHub Actions
1920
+
1921
+ 1. Get Static Web App deployment token:
1922
+ \`\`\`bash
1923
+ az staticwebapp secrets list --name swa-${projectName} --resource-group <rg-name> --query "properties.apiKey" -o tsv
1924
+ \`\`\`
1925
+
1926
+ 2. Get Function App publish profile:
1927
+ \`\`\`bash
1928
+ az webapp deployment list-publishing-profiles --name func-${projectName} --resource-group <rg-name> --xml
1929
+ \`\`\`
1930
+
1931
+ 3. Add secrets to GitHub repository:
1932
+ - \`AZURE_STATIC_WEB_APPS_API_TOKEN\`: SWA deployment token (from step 1)
1933
+ - \`AZURE_FUNCTIONAPP_NAME\`: \`func-${projectName}\`
1934
+ - \`AZURE_FUNCTIONAPP_PUBLISH_PROFILE\`: Functions publish profile (from step 2)
1935
+
1936
+ 4. Push to \`main\` branch to trigger deployment (or use **Actions** → **Run workflow** for manual deployment)` : cicdChoice === 'azure' ? `#### Azure Pipelines
1937
+
1938
+ 1. Set up service connection in Azure DevOps
1939
+ 2. Update \`azure-pipelines.yml\` with your resource names
1940
+ 3. Configure pipeline variables:
1941
+ - \`azureSubscription\`: Service connection name
1942
+ - \`resourceGroupName\`: Resource group name
1943
+ 4. Run pipeline to deploy` : `CI/CD is not configured. You can manually deploy:
1944
+
1945
+ **Deploy Static Web App:**
1946
+ \`\`\`bash
1947
+ ${pmCmd.run} build
1948
+ az staticwebapp deploy --name swa-${projectName} --resource-group <rg-name> --app-location ./
1949
+ \`\`\`
1950
+
1951
+ **Deploy Functions:**
1952
+ \`\`\`bash
1953
+ cd functions
1954
+ ${pmCmd.run} build
1955
+ func azure functionapp publish func-${projectName}
1956
+ \`\`\``}
1957
+
1958
+ ## 🔧 Available Commands
1959
+
1960
+ - \`${pmCmd.dlx} swallowkit create-model <name>\` - Create a new data model
1961
+ - \`${pmCmd.dlx} swallowkit scaffold <model-file>\` - Generate CRUD code
1962
+ - \`${pmCmd.dlx} swallowkit dev\` - Start development servers
1963
+ - \`${pmCmd.dlx} swallowkit provision -g <rg-name>\` - Provision Azure resources
1964
+ ${azureConfig.vnetOption !== 'none' ? `
1965
+ ## 🔒 Network Security (VNet Configuration)
1966
+
1967
+ This project is configured with **${vnetLabel}**.
1968
+
1969
+ ### Architecture
1970
+
1971
+ \`\`\`
1972
+ Static Web App ──(public)──> Azure Functions ──(VNet/PE)──> Cosmos DB
1973
+ │
1974
+ VNet Integration
1975
+ (outbound only)
1976
+ \`\`\`
1977
+
1978
+ - **Functions → Cosmos DB**: Connected via Private Endpoint (private connection)
1979
+ - **SWA → Functions**: Connected via public endpoint (secured with CORS + IP restrictions)
1980
+
1981
+ ### VNet Resources
1982
+
1983
+ | Resource | Purpose |
1984
+ |----------|---------|
1985
+ | \`vnet-${projectName}\` | Virtual Network (10.0.0.0/16) |
1986
+ | \`snet-functions\` | Functions subnet (10.0.1.0/24) |
1987
+ | \`snet-private-endpoints\` | Private Endpoints subnet (10.0.2.0/24) |
1988
+ | \`pe-cosmos-${projectName}\` | Cosmos DB Private Endpoint |
1989
+
1990
+ ### Private DNS Zones
1991
+
1992
+ - \`privatelink.documents.azure.com\` (Cosmos DB)
1993
+ ` : ''}
1994
+ ## 📚 Learn More
1995
+
1996
+ - [SwallowKit Documentation](https://github.com/himanago/swallowkit)
1997
+ - [Azure Static Web Apps](https://learn.microsoft.com/en-us/azure/static-web-apps/)
1998
+ - [Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/)
1999
+ - [Azure Cosmos DB](https://learn.microsoft.com/en-us/azure/cosmos-db/)
2000
+ - [Next.js](https://nextjs.org/)
2001
+ - [Zod](https://zod.dev/)
2002
+
2003
+ ## 💭 Feedback
2004
+
2005
+ This project was generated by SwallowKit. If you encounter any issues or have suggestions for improvements, please open an issue on the [SwallowKit repository](https://github.com/himanago/swallowkit).
2006
+ `;
2007
+
2008
+ fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
2009
+ console.log('✅ README.md created\n');
2010
+ }
2011
+
2012
+ function createAiAgentFiles(projectDir: string, projectName: string, backendLanguage: BackendLanguage, pm: PackageManager) {
2013
+ console.log('🤖 Creating AI agent instruction files...\n');
2014
+ const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
2015
+ const runCmd = pm === 'pnpm' ? 'pnpm' : 'npx';
2016
+ const projectMcpConfigSource = buildSwallowKitMcpProjectConfigSource(pm);
2017
+ const functionsStructureLine = backendLanguage === 'typescript'
2018
+ ? `│ └── src/ # HTTP trigger handlers with Cosmos DB bindings`
2019
+ : backendLanguage === 'csharp'
2020
+ ? `│ ├── Crud/ # C# HTTP trigger handlers\n│ └── generated/ # Native-generated C# schema assets`
2021
+ : `│ ├── blueprints/ # Python HTTP trigger handlers\n│ └── generated/ # Native-generated Python schema assets`;
2022
+ const backendSchemaNote = backendLanguage === 'typescript'
2023
+ ? `- The shared package (\`@${projectName}/shared\`) is consumed by both Next.js and Azure Functions as a workspace dependency.`
2024
+ : `- The frontend/BFF source of truth stays in \`shared/models/\` as Zod schemas.\n- \`swallowkit scaffold\` exports OpenAPI into \`functions/openapi/\` and generates ${backendLanguageLabel} schema assets into \`functions/generated/\` with native ${backendLanguageLabel} tooling.`;
2025
+ const backendRulesNote = backendLanguage === 'typescript'
2026
+ ? `- All CRUD operations and business logic live in \`functions/src/\`.\n- Use Azure Functions Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.\n- Use the Cosmos DB SDK client directly **only** for delete operations (bindings do not support delete).\n- Validate all data against Zod schemas before writing to Cosmos DB.\n- The backend auto-generates \`id\` (UUID), \`createdAt\`, and \`updatedAt\` — never trust client-sent values for these fields.`
2027
+ : `- All business logic lives in \`functions/\` and the generated handlers perform real Cosmos DB CRUD.\n- Keep Zod schemas in \`shared/models/\` as the source of truth.\n- Regenerate backend contracts with \`swallowkit scaffold shared/models/<name>.ts\` whenever a schema changes.\n- Use the native-generated schema assets in \`functions/generated/\` to keep backend contracts aligned.\n- The backend should still own \`id\`, \`createdAt\`, and \`updatedAt\`.`;
2028
+
2029
+ // ── 1. AGENTS.md (Codex / generic agents) ──────────────────────────
2030
+
2031
+ const agentsMd = `# AGENTS.md
2032
+
2033
+ This project was generated by **SwallowKit**.
2034
+ All coding agents **must** follow the architecture and conventions described below.
2035
+
2036
+ ## Architecture Overview
2037
+
2038
+ This is a full-stack application deployed on Azure with a TypeScript frontend/BFF and an Azure Functions backend in ${backendLanguageLabel}.
2039
+
2040
+ \`\`\`
2041
+ Frontend (React / Next.js App Router)
2042
+ ↓ fetch('/api/{model}', ...)
2043
+ BFF Layer (Next.js API Routes)
2044
+ ↓ HTTP → Azure Functions
2045
+ Backend (Azure Functions)
2046
+ ↓
2047
+ Azure Cosmos DB (Document Database)
2048
+ \`\`\`
2049
+
2050
+ ### Project Structure
2051
+
2052
+ \`\`\`
2053
+ ${projectName}/
2054
+ ├── app/ # Next.js App Router
2055
+ │ ├── api/ # BFF API routes (proxy to Azure Functions)
2056
+ │ └── {model}/ # UI pages per model (list, detail, create, edit)
2057
+ ├── functions/ # Azure Functions (backend)
2058
+ ${functionsStructureLine}
2059
+ ├── shared/ # Shared workspace package
2060
+ │ ├── models/ # Zod schema definitions (single source of truth)
2061
+ │ └── index.ts # Re-exports all models
2062
+ ├── lib/
2063
+ │ └── api/ # API client utilities (backend.ts, call-function.ts)
2064
+ ├── components/ # Shared React components
2065
+ ├── infra/ # Bicep infrastructure-as-code files
2066
+ │ ├── main.bicep
2067
+ │ └── modules/
2068
+ ├── .mcp.json # Project-scoped MCP bootstrap resolving SwallowKit on each launch
2069
+ └── .github/workflows/ # CI/CD workflows (if configured)
2070
+ \`\`\`
2071
+
2072
+ ## SwallowKit MCP / Machine Workflow
2073
+
2074
+ - This repository includes a project-scoped \`.mcp.json\` file that resolves and starts the latest SwallowKit MCP server on each launch. Set \`SWALLOWKIT_MCP_VERSION\` in that file to pin a specific version.
2075
+ - Prefer the \`swallowkit_*\` MCP tools for framework-owned inspection, validation, and generation when they are available.
2076
+ - If MCP is unavailable in your runtime, fall back to the machine CLI:
2077
+ - \`${runCmd} swallowkit machine inspect project\`
2078
+ - \`${runCmd} swallowkit machine validate project\`
2079
+ - \`${runCmd} swallowkit machine generate scaffold <name> --api-only\`
2080
+ - Do not hand-edit framework-owned artifacts when the MCP or machine interface can generate or validate them for you.
2081
+ - The MCP bootstrap uses ${pm} and requires network access when the selected version is not cached.
2082
+ - **Always invoke SwallowKit via \`${runCmd}\`.** Do not mix package manager commands.
2083
+
2084
+ ## Critical Design Principles
2085
+
2086
+ ### 1. Next.js API Routes Are Strictly a BFF (Backend for Frontend)
2087
+
2088
+ - \`app/api/\` routes exist **only** to proxy requests to Azure Functions.
2089
+ - **Never** place business logic, database access, or direct Cosmos DB calls in Next.js API routes.
2090
+ - The BFF layer may validate input/output with Zod schemas before forwarding to Functions.
2091
+ - Use the \`callFunction\` helper (\`lib/api/call-function.ts\`) or the \`api\` client (\`lib/api/backend.ts\`) to call Azure Functions.
2092
+
2093
+ Example BFF route pattern:
2094
+
2095
+ \`\`\`typescript
2096
+ // app/api/{model}/route.ts
2097
+ import { callFunction } from '@/lib/api/call-function';
2098
+ import { ModelSchema } from '@${projectName}/shared';
2099
+ import { z } from 'zod/v4';
2100
+
2101
+ export async function GET() {
2102
+ return callFunction({
2103
+ method: 'GET',
2104
+ path: '/api/{model}',
2105
+ responseSchema: z.array(ModelSchema),
2106
+ });
2107
+ }
2108
+
2109
+ export async function POST(request: NextRequest) {
2110
+ const body = await request.json();
2111
+ return callFunction({
2112
+ method: 'POST',
2113
+ path: '/api/{model}',
2114
+ body,
2115
+ inputSchema: ModelSchema.omit({ id: true, createdAt: true, updatedAt: true }),
2116
+ responseSchema: ModelSchema,
2117
+ successStatus: 201,
2118
+ });
2119
+ }
2120
+ \`\`\`
2121
+
2122
+ ### 2. Zod Schemas Are the Single Source of Truth
2123
+
2124
+ - All data models are defined **once** as Zod schemas in \`shared/models/\`.
2125
+ - TypeScript types are derived with \`z.infer<typeof Schema>\` — never define types separately.
2126
+ - ${backendSchemaNote}
2127
+
2128
+ Model definition pattern:
2129
+
2130
+ \`\`\`typescript
2131
+ // shared/models/{model}.ts
2132
+ import { z } from 'zod/v4';
2133
+
2134
+ export const Todo = z.object({
2135
+ id: z.string(),
2136
+ name: z.string().min(1),
2137
+ // ... your fields
2138
+ createdAt: z.string().optional(),
2139
+ updatedAt: z.string().optional(),
2140
+ });
2141
+
2142
+ export type Todo = z.infer<typeof Todo>;
2143
+ export const displayName = 'Todo';
2144
+ \`\`\`
2145
+
2146
+ Key rules:
2147
+ - Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
2148
+ - \`id\`, \`createdAt\`, and \`updatedAt\` are auto-managed by the backend. Mark them as \`optional()\` in the schema.
2149
+ - Always re-export models from \`shared/index.ts\`.
2150
+
2151
+ ### 3. Azure Functions Own All Business Logic and Data Access
2152
+
2153
+ - ${backendRulesNote}
2154
+
2155
+ ${backendLanguage === 'typescript' ? 'Azure Functions handler pattern:' : `Generated ${backendLanguageLabel} handlers live under \`functions/\`. Re-run \`swallowkit scaffold shared/models/<name>.ts\` after schema changes to keep generated CRUD handlers and the native schema assets under \`functions/generated/\` in sync.`}
2156
+
2157
+ ${backendLanguage === 'typescript' ? `\`\`\`typescript
2158
+ // functions/src/{model}.ts
2159
+ import { app } from '@azure/functions';
2160
+ import { ModelSchema } from '@${projectName}/shared';
2161
+
2162
+ const containerName = 'Models'; // PascalCase + 's'
2163
+
2164
+ app.http('{model}-get-all', {
2165
+ methods: ['GET'],
2166
+ route: '{model}',
2167
+ authLevel: 'function',
2168
+ extraInputs: [{ type: 'cosmosDB', name: 'cosmosInput', containerName, ... }],
2169
+ handler: async (request, context) => {
2170
+ const documents = context.extraInputs.get('cosmosInput');
2171
+ const validated = z.array(ModelSchema).parse(documents);
2172
+ return { status: 200, jsonBody: validated };
2173
+ },
2174
+ });
2175
+ \`\`\`` : ''}
2176
+
2177
+ ## Naming Conventions
2178
+
2179
+ | Item | Convention | Example |
2180
+ |------|-----------|---------|
2181
+ | Model schema file | \`shared/models/{kebab-case}.ts\` | \`shared/models/todo.ts\` |
2182
+ | Schema/type name | PascalCase (same name for both) | \`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\` |
2183
+ | Functions handler file | backend-language specific under \`functions/\` | \`${backendLanguage === 'typescript' ? 'functions/src/todo.ts' : backendLanguage === 'csharp' ? 'functions/Crud/TodoCrudFunctions.cs' : 'functions/blueprints/todo.py'}\` |
2184
+ | Functions handler name | \`{camelCase}-{operation}\` | \`todo-get-all\`, \`todo-create\` |
2185
+ | API route path | \`/api/{camelCase}\` | \`/api/todo\`, \`/api/todo/{id}\` |
2186
+ | BFF route file | \`app/api/{kebab-case}/route.ts\` | \`app/api/todo/route.ts\` |
2187
+ | BFF detail route | \`app/api/{kebab-case}/[id]/route.ts\` | \`app/api/todo/[id]/route.ts\` |
2188
+ | UI page directory | \`app/{kebab-case}/\` | \`app/todo/page.tsx\` |
2189
+ | React component | PascalCase | \`TodoForm.tsx\` |
2190
+ | Cosmos DB container | PascalCase + 's' | \`Todos\` |
2191
+ | Cosmos DB partition key | \`/id\` (default) | Custom: \`export const partitionKey = '/field'\` |
2192
+ | Bicep container file | \`infra/containers/{kebab-case}-container.bicep\` | \`infra/containers/todo-container.bicep\` |
2193
+
2194
+ ## Adding New Models (SwallowKit CLI Skills)
2195
+
2196
+ Use the SwallowKit CLI — do **not** manually create model files or CRUD boilerplate.
2197
+
2198
+ ### Skill: Create a new data model
2199
+
2200
+ \`\`\`bash
2201
+ ${runCmd} swallowkit create-model <name>
2202
+ # Multiple models at once:
2203
+ ${runCmd} swallowkit create-model user post comment
2204
+ \`\`\`
2205
+
2206
+ Creates \`shared/models/<name>.ts\` with a Zod schema template including \`id\`, \`createdAt\`, \`updatedAt\`.
2207
+ Edit the generated file to add your domain-specific fields, then run scaffold.
2208
+
2209
+ ### Skill: Generate full CRUD from a model
2210
+
2211
+ \`\`\`bash
2212
+ ${runCmd} swallowkit scaffold shared/models/<name>.ts
2213
+ \`\`\`
2214
+
2215
+ Generates:
2216
+ - Azure Functions handlers (${backendLanguage === 'typescript' ? '`functions/src/<name>.ts`' : '`functions/` language-specific CRUD files + `functions/generated/` schema assets'})
2217
+ - BFF API routes (\`app/api/<name>/route.ts\`, \`app/api/<name>/[id]/route.ts\`)
2218
+ - UI pages (\`app/<name>/page.tsx\`, detail, create, edit pages)
2219
+ - Cosmos DB Bicep container config (\`infra/containers/<name>-container.bicep\`)
2220
+
2221
+ ### Skill: Start development servers
2222
+
2223
+ \`\`\`bash
2224
+ ${runCmd} swallowkit dev
2225
+ \`\`\`
2226
+
2227
+ Runs Next.js (http://localhost:3000) and Azure Functions (http://localhost:7071) concurrently.
2228
+ Checks for Cosmos DB Emulator availability.
2229
+
2230
+ ### Skill: Provision Azure resources
2231
+
2232
+ \`\`\`bash
2233
+ ${runCmd} swallowkit provision --resource-group <name> --location <region>
2234
+ \`\`\`
2235
+
2236
+ Deploys Bicep infrastructure: Static Web Apps, Functions, Cosmos DB, Storage, Managed Identity.
2237
+
2238
+ ### Typical workflow for "add a new feature/model"
2239
+
2240
+ 1. \`${runCmd} swallowkit create-model <name>\`
2241
+ 2. Edit \`shared/models/<name>.ts\` — add fields
2242
+ 3. \`${runCmd} swallowkit scaffold shared/models/<name>.ts\`
2243
+ 4. \`${runCmd} swallowkit dev\` — verify at http://localhost:3000/<name>
2244
+ 5. If \`dev-seeds/\` already exists, update the seed JSON files to include realistic data for the new model and adjust existing seeds if relationships changed.
2245
+
2246
+ ## Do NOT
2247
+
2248
+ - **Do not** put business logic or database calls in \`app/api/\` routes. They are BFF only.
2249
+ - **Do not** define TypeScript interfaces/types separately from Zod schemas. Always derive types with \`z.infer<>\`.
2250
+ - **Do not** manually duplicate model definitions across layers. Use the shared package.
2251
+ - **Do not** manually create CRUD boilerplate. Use \`swallowkit scaffold\`.
2252
+ - **Do not** hardcode Cosmos DB connection strings. Use Managed Identity (\`CosmosDBConnection__accountEndpoint\`) in production and emulator settings locally.
2253
+ - By default, all containers use \`/id\` as the partition key. To use a custom partition key, add \`export const partitionKey = '/yourField'\` to the model file. The scaffold command will apply it across all layers.
2254
+
2255
+ ## Technology Stack
2256
+
2257
+ - **Frontend**: Next.js (App Router), React, TypeScript, Tailwind CSS
2258
+ - **BFF**: Next.js API Routes (proxy only)
2259
+ - **Backend**: Azure Functions (${backendLanguageLabel})
2260
+ - **Database**: Azure Cosmos DB (NoSQL)
2261
+ - **Schema**: Zod (shared across all layers via workspace package)
2262
+ - **Infrastructure**: Bicep (IaC)
2263
+ - **Hosting**: Azure Static Web Apps (frontend) + Azure Functions Flex Consumption (backend)
2264
+ - **Auth**: Azure Managed Identity (no connection strings in production)
2265
+ - **Monitoring**: Application Insights
2266
+ `;
2267
+
2268
+ fs.writeFileSync(path.join(projectDir, 'AGENTS.md'), agentsMd);
2269
+ console.log(' ✅ AGENTS.md (Codex / generic agents)');
2270
+
2271
+ // ── 2. CLAUDE.md (Claude Code) ─────────────────────────────────────
2272
+
2273
+ const claudeMd = `# CLAUDE.md
2274
+
2275
+ This file is for Claude Code. Read AGENTS.md in the project root for the full architecture, conventions, and rules.
2276
+
2277
+ ## Quick Reference
2278
+
2279
+ - **Architecture**: Next.js (frontend) → BFF (API routes, proxy only) → Azure Functions (backend) → Cosmos DB
2280
+ - **Schema**: Zod schemas in \`shared/models/\` are the single source of truth. Never define types separately.
2281
+ - **BFF rule**: \`app/api/\` routes must ONLY proxy to Azure Functions via \`callFunction()\`. No business logic.
2282
+ - **Backend language**: ${backendLanguageLabel}
2283
+ - **Backend rule**: Regenerate backend contracts with \`swallowkit scaffold\` after schema changes and keep \`functions/generated/\` in sync.
2284
+
2285
+ ## SwallowKit MCP
2286
+
2287
+ - This repository includes a project-scoped \`.mcp.json\` that resolves and starts the latest SwallowKit MCP server on each launch. Set \`SWALLOWKIT_MCP_VERSION\` in that file to pin a specific version.
2288
+ - When the \`swallowkit_*\` tools are available, prefer them for inspect / validate / generate tasks.
2289
+ - If MCP is unavailable, use \`${runCmd} swallowkit machine ...\` instead.
2290
+ - **Always invoke SwallowKit via \`${runCmd}\`.** Do not mix package manager commands.
2291
+
2292
+ ## SwallowKit CLI Commands
2293
+
2294
+ | Task | Command |
2295
+ |------|---------|
2296
+ | Create model | \`${runCmd} swallowkit create-model <name>\` |
2297
+ | Generate CRUD | \`${runCmd} swallowkit scaffold shared/models/<name>.ts\` |
2298
+ | Dev servers | \`${runCmd} swallowkit dev\` |
2299
+ | Provision Azure | \`${runCmd} swallowkit provision --resource-group <rg> --location <region>\` |
2300
+
2301
+ ## Workflow: Add a new model
2302
+
2303
+ 1. \`${runCmd} swallowkit create-model <name>\`
2304
+ 2. Edit \`shared/models/<name>.ts\` — add your fields
2305
+ 3. \`${runCmd} swallowkit scaffold shared/models/<name>.ts\`
2306
+ 4. \`${runCmd} swallowkit dev\` — verify at http://localhost:3000/<name>
2307
+ 5. If \`dev-seeds/\` exists, update the seed JSON files to include data for the new model and adjust existing seeds if relationships changed.
2308
+ `;
2309
+
2310
+ fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), claudeMd);
2311
+ console.log(' ✅ CLAUDE.md (Claude Code)');
2312
+
2313
+ fs.writeFileSync(path.join(projectDir, '.mcp.json'), projectMcpConfigSource);
2314
+ console.log(' ✅ .mcp.json (project-scoped MCP bootstrap)');
2315
+
2316
+ // ── 3. .github/copilot-instructions.md (GitHub Copilot) ────────────
2317
+
2318
+ const ghDir = path.join(projectDir, '.github');
2319
+ fs.mkdirSync(ghDir, { recursive: true });
2320
+
2321
+ const copilotInstructions = `# Copilot Instructions
2322
+
2323
+ This project was generated by **SwallowKit**. See \`AGENTS.md\` in the project root for the full specification.
2324
+
2325
+ ## Architecture (3-layer)
2326
+
2327
+ \`\`\`
2328
+ Frontend (Next.js App Router) → BFF (Next.js API Routes) → Backend (Azure Functions) → Cosmos DB
2329
+ \`\`\`
2330
+
2331
+ ## Key Rules
2332
+
2333
+ 1. **BFF is proxy only** — \`app/api/\` routes call Azure Functions via \`callFunction()\`. No business logic, no direct DB access.
2334
+ 2. **Zod = single source of truth** — Models live in \`shared/models/\`. Types are derived with \`z.infer<>\`. Never define types separately.
2335
+ 3. **Backend owns data** — All CRUD and business logic stay in \`functions/\`, and generated contract assets under \`functions/generated/\` must stay aligned with \`shared/models/\`.
2336
+ 4. **Use the CLI** — Run \`${runCmd} swallowkit create-model <name>\` then \`${runCmd} swallowkit scaffold shared/models/<name>.ts\` to add models. Do not create boilerplate manually.
2337
+ 5. **Maintain seed data** — When adding models or changing schemas, update the JSON files under \`dev-seeds/\` to keep seed data consistent with the current schema.
2338
+
2339
+ ## SwallowKit Framework Operations
2340
+
2341
+ - Prefer the SwallowKit MCP or machine interface for framework-owned inspection, validation, and generation instead of hand-editing generated files.
2342
+ - If your runtime loads project-scoped MCP config from \`.mcp.json\`, use the \`swallowkit_*\` tools.
2343
+ - Otherwise use \`${runCmd} swallowkit machine inspect project\`, \`${runCmd} swallowkit machine validate project\`, and \`${runCmd} swallowkit machine generate scaffold <name> --api-only\`.
2344
+ - **Always invoke SwallowKit via \`${runCmd}\`.** Do not mix package manager commands.
2345
+
2346
+ ## Naming
2347
+
2348
+ - Schema/type: PascalCase, same name for both (\`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\`)
2349
+ - Files: kebab-case (\`shared/models/todo.ts\`, backend handlers under \`functions/\`)
2350
+ - Cosmos DB containers: PascalCase + 's' (\`Todos\`), partition key default \`/id\` (customizable via \`export const partitionKey\`)
2351
+
2352
+ ## Managed Fields
2353
+
2354
+ \`id\`, \`createdAt\`, \`updatedAt\` are auto-managed by the backend. Define them as \`optional()\` in schemas. Never trust client-sent values.
2355
+ `;
2356
+
2357
+ fs.writeFileSync(path.join(ghDir, 'copilot-instructions.md'), copilotInstructions);
2358
+ console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot)');
2359
+
2360
+ // ── 4. .github/instructions/*.instructions.md (Copilot layer-specific) ──
2361
+
2362
+ const instructionsDir = path.join(ghDir, 'instructions');
2363
+ fs.mkdirSync(instructionsDir, { recursive: true });
2364
+
2365
+ // 4a. shared/models — Zod schema layer
2366
+ const sharedModelsInstructions = `---
2367
+ applyTo: "shared/models/**"
2368
+ ---
2369
+
2370
+ # Shared Models — Zod Schema Rules
2371
+
2372
+ Files in this directory are the **single source of truth** for data models across the entire application.
2373
+
2374
+ ## Rules
2375
+
2376
+ - Define Zod schemas using \`zod/v4\` (\`import { z } from 'zod/v4'\`).
2377
+ - Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
2378
+ \`\`\`typescript
2379
+ export const Todo = z.object({ ... });
2380
+ export type Todo = z.infer<typeof Todo>;
2381
+ \`\`\`
2382
+ - Always include \`id: z.string()\`, \`createdAt: z.string().optional()\`, \`updatedAt: z.string().optional()\`. These are managed by the backend.
2383
+ - Export a \`displayName\` string constant for UI display.
2384
+ - Re-export every model from \`shared/index.ts\`.
2385
+ - For relationships, use **nested schemas** (import and embed the related schema), not ID references.
2386
+ - After editing a model, run \`${runCmd} swallowkit scaffold shared/models/<name>.ts\` to regenerate CRUD code.
2387
+ `;
2388
+
2389
+ fs.writeFileSync(
2390
+ path.join(instructionsDir, 'shared-models.instructions.md'),
2391
+ sharedModelsInstructions
2392
+ );
2393
+
2394
+ // 4b. app/api — BFF layer
2395
+ const bffInstructions = `---
2396
+ applyTo: "app/api/**"
2397
+ ---
2398
+
2399
+ # BFF API Routes — Rules
2400
+
2401
+ Files in \`app/api/\` are the **BFF (Backend for Frontend)** layer. They exist solely to proxy requests to Azure Functions.
2402
+
2403
+ ## Rules
2404
+
2405
+ - **Never** put business logic, database access, or direct Cosmos DB calls here.
2406
+ - Use \`callFunction()\` from \`@/lib/api/call-function\` to forward requests to Azure Functions.
2407
+ - You may validate input/output with Zod schemas before forwarding.
2408
+ - Import schemas from \`@${projectName}/shared\`.
2409
+
2410
+ ## Pattern
2411
+
2412
+ \`\`\`typescript
2413
+ import { callFunction } from '@/lib/api/call-function';
2414
+ import { ModelSchema } from '@${projectName}/shared';
2415
+ import { z } from 'zod/v4';
2416
+
2417
+ export async function GET() {
2418
+ return callFunction({
2419
+ method: 'GET',
2420
+ path: '/api/{model}',
2421
+ responseSchema: z.array(ModelSchema),
2422
+ });
2423
+ }
2424
+ \`\`\`
2425
+ `;
2426
+
2427
+ fs.writeFileSync(
2428
+ path.join(instructionsDir, 'bff-routes.instructions.md'),
2429
+ bffInstructions
2430
+ );
2431
+
2432
+ // 4c. functions — Azure Functions backend layer
2433
+ const functionsInstructions = `---
2434
+ applyTo: "functions/**"
2435
+ ---
2436
+
2437
+ # Azure Functions — Backend Rules
2438
+
2439
+ Files in \`functions/\` contain all business logic and data access for this application.
2440
+
2441
+ ## Rules
2442
+
2443
+ - Keep backend contracts aligned with \`shared/models/\` by rerunning \`swallowkit scaffold\` after schema changes.
2444
+ - For TypeScript backends, use Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.
2445
+ - For C#/Python backends, consume the native-generated assets in \`functions/generated/\`.
2446
+ - Auto-generate \`id\` (UUID), \`createdAt\`, and \`updatedAt\` on the backend. Never trust client-sent values.
2447
+ - Container names are PascalCase + 's' (e.g., \`Todos\`). Partition key defaults to \`/id\` but can be customized per model.
2448
+
2449
+ ## Handler Pattern
2450
+
2451
+ \`\`\`typescript
2452
+ import { app } from '@azure/functions';
2453
+ import { ModelSchema } from '@${projectName}/shared';
2454
+
2455
+ app.http('{model}-get-all', {
2456
+ methods: ['GET'],
2457
+ route: '{model}',
2458
+ authLevel: 'function',
2459
+ extraInputs: [cosmosInput],
2460
+ handler: async (request, context) => {
2461
+ const documents = context.extraInputs.get(cosmosInput);
2462
+ const validated = z.array(ModelSchema).parse(documents);
2463
+ return { status: 200, jsonBody: validated };
2464
+ },
2465
+ });
2466
+ \`\`\`
2467
+ `;
2468
+
2469
+ fs.writeFileSync(
2470
+ path.join(instructionsDir, 'azure-functions.instructions.md'),
2471
+ functionsInstructions
2472
+ );
2473
+
2474
+ console.log(' ✅ .github/instructions/ (Copilot layer-specific instructions)');
2475
+ console.log(' - shared-models.instructions.md');
2476
+ console.log(' - bff-routes.instructions.md');
2477
+ console.log(' - azure-functions.instructions.md');
2478
+
2479
+ console.log('\n✅ AI agent files created\n');
2480
+ console.log(' Supported agents:');
2481
+ console.log(' - OpenAI Codex → AGENTS.md');
2482
+ console.log(' - Claude Code → CLAUDE.md (+ AGENTS.md)');
2483
+ console.log(' - Project MCP runtimes → .mcp.json');
2484
+ console.log(' - GitHub Copilot → .github/copilot-instructions.md');
2485
+ console.log(' - GitHub Copilot (edit) → .github/instructions/*.instructions.md');
2486
+ console.log('');
2487
+ }
2488
+
2489
+ export function buildFunctionsHostKeyBicepExpression(): string {
2490
+ return "listKeys(resourceId('Microsoft.Web/sites/host', 'func-${projectName}', 'default'), '2023-12-01').functionKeys.default";
2491
+ }
2492
+
2493
+ export function buildStaticWebAppConfigBicepSource(): string {
2494
+ return `@description('Static Web App name')
2495
+ param staticWebAppName string
2496
+
2497
+ @description('Functions App default hostname for backend API calls')
2498
+ param functionsDefaultHostname string
2499
+
2500
+ @secure()
2501
+ @description('Functions host key used only by the server-side BFF')
2502
+ param functionsHostKey string
2503
+
2504
+ @description('Application Insights connection string for SWA')
2505
+ param appInsightsConnectionString string
2506
+
2507
+ resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
2508
+ name: staticWebAppName
2509
+ }
2510
+
2511
+ resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
2512
+ parent: staticWebApp
2513
+ name: 'appsettings'
2514
+ properties: {
2515
+ APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
2516
+ ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
2517
+ BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
2518
+ BACKEND_FUNCTIONS_KEY: functionsHostKey
2519
+ }
2520
+ }
2521
+
2522
+ output configName string = staticWebAppConfig.name
2523
+ `;
2524
+ }
2525
+
2526
+ export async function createInfrastructure(
2527
+ projectDir: string,
2528
+ projectName: string,
2529
+ azureConfig: AzureConfig,
2530
+ backendLanguage: BackendLanguage
2531
+ ) {
2532
+ console.log('📦 Creating infrastructure files (Bicep)...\n');
2533
+
2534
+ const infraDir = path.join(projectDir, 'infra');
2535
+ const modulesDir = path.join(infraDir, 'modules');
2536
+ fs.mkdirSync(modulesDir, { recursive: true });
2537
+
2538
+ const enableVNet = azureConfig.vnetOption !== 'none';
2539
+ const functionsRuntime = getFunctionsRuntimeConfig(backendLanguage);
2540
+ const staticWebAppSku = getStaticWebAppSku(azureConfig.swaPlan);
2541
+
2542
+ // main.bicep
2543
+ const mainBicep = `targetScope = 'resourceGroup'
2544
+
2545
+ @description('Project name')
2546
+ param projectName string
2547
+
2548
+ @description('Location for Functions and Cosmos DB')
2549
+ param location string = resourceGroup().location
2550
+
2551
+ @description('Location for Static Web App (must be explicitly provided)')
2552
+ param swaLocation string
2553
+
2554
+ @description('Static Web Apps hosting plan')
2555
+ @allowed([
2556
+ 'Free'
2557
+ 'Standard'
2558
+ ])
2559
+ param staticWebAppSku string = '${staticWebAppSku}'
2560
+
2561
+ @description('Cosmos DB mode')
2562
+ @allowed(['freetier', 'serverless'])
2563
+ param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
2564
+
2565
+ @description('Enable VNet integration')
2566
+ param enableVNet bool = ${enableVNet}
2567
+
2568
+ // Shared Log Analytics Workspace (in Functions region for data residency)
2569
+ module logAnalytics 'modules/loganalytics.bicep' = {
2570
+ name: 'logAnalytics'
2571
+ params: {
2572
+ name: 'log-\${projectName}'
2573
+ location: location
2574
+ }
2575
+ }
2576
+
2577
+ // Application Insights for Static Web App (must be in same region as SWA)
2578
+ module appInsightsSwa 'modules/appinsights.bicep' = {
2579
+ name: 'appInsightsSwa'
2580
+ params: {
2581
+ name: 'appi-\${projectName}-swa'
2582
+ location: swaLocation
2583
+ logAnalyticsWorkspaceId: logAnalytics.outputs.id
2584
+ }
2585
+ }
2586
+
2587
+ // Application Insights for Functions (in same region as Functions)
2588
+ module appInsightsFunctions 'modules/appinsights.bicep' = {
2589
+ name: 'appInsightsFunctions'
2590
+ params: {
2591
+ name: 'appi-\${projectName}-func'
2592
+ location: location
2593
+ logAnalyticsWorkspaceId: logAnalytics.outputs.id
2594
+ }
2595
+ }
2596
+
2597
+ // Static Web App
2598
+ module staticWebApp 'modules/staticwebapp.bicep' = {
2599
+ name: 'staticWebApp'
2600
+ params: {
2601
+ name: 'swa-\${projectName}'
2602
+ location: swaLocation
2603
+ sku: staticWebAppSku
2604
+ appInsightsConnectionString: appInsightsSwa.outputs.connectionString
2605
+ }
2606
+ }
2607
+
2608
+ // VNet (conditional)
2609
+ module vnet 'modules/vnet.bicep' = if (enableVNet) {
2610
+ name: 'vnet'
2611
+ params: {
2612
+ name: 'vnet-\${projectName}'
2613
+ location: location
2614
+ }
2615
+ }
2616
+
2617
+ // Cosmos DB (conditional based on mode) - Deploy BEFORE Functions
2618
+ module cosmosDbFreeTier 'modules/cosmosdb-freetier.bicep' = if (cosmosDbMode == 'freetier') {
2619
+ name: 'cosmosDb'
2620
+ params: {
2621
+ accountName: 'cosmos-\${projectName}'
2622
+ databaseName: '\${projectName}Database'
2623
+ location: location
2624
+ publicNetworkAccess: enableVNet ? 'Disabled' : 'Enabled'
2625
+ }
2626
+ }
2627
+
2628
+ module cosmosDbServerless 'modules/cosmosdb-serverless.bicep' = if (cosmosDbMode == 'serverless') {
2629
+ name: 'cosmosDb'
2630
+ params: {
2631
+ accountName: 'cosmos-\${projectName}'
2632
+ databaseName: '\${projectName}Database'
2633
+ location: location
2634
+ publicNetworkAccess: enableVNet ? 'Disabled' : 'Enabled'
2635
+ }
2636
+ }
2637
+
2638
+ // Cosmos DB Private Endpoint (conditional)
2639
+ module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enableVNet) {
2640
+ name: 'cosmosPrivateEndpoint'
2641
+ params: {
2642
+ name: 'pe-cosmos-\${projectName}'
2643
+ location: location
2644
+ cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.id : cosmosDbServerless.outputs.id
2645
+ cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
2646
+ subnetId: vnet.outputs.privateEndpointSubnetId
2647
+ vnetId: vnet.outputs.id
2648
+ }
2649
+ dependsOn: [
2650
+ cosmosDbFreeTier
2651
+ cosmosDbServerless
2652
+ vnet
2653
+ ]
2654
+ }
2655
+
2656
+ // Azure Functions (Flex Consumption) - Deploy AFTER Cosmos DB
2657
+ module functionsFlex 'modules/functions-flex.bicep' = {
2658
+ name: 'functionsApp'
2659
+ params: {
2660
+ name: 'func-\${projectName}'
2661
+ location: location
2662
+ storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
2663
+ appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
2664
+ swaDefaultHostname: staticWebApp.outputs.defaultHostname
2665
+ cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
2666
+ cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
2667
+ functionsRuntimeName: '${functionsRuntime.name}'
2668
+ functionsRuntimeVersion: '${functionsRuntime.version}'
2669
+ enableVNet: enableVNet
2670
+ vnetSubnetId: enableVNet ? vnet.outputs.functionsSubnetId : ''
2671
+ }
2672
+ dependsOn: [
2673
+ cosmosDbFreeTier
2674
+ cosmosDbServerless
2675
+ cosmosPrivateEndpoint
2676
+ ]
2677
+ }
2678
+
2679
+ // Cosmos DB role assignment for Functions (after Functions is created)
2680
+ module cosmosDbRoleAssignmentFreeTier 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'freetier') {
2681
+ name: 'cosmosDbRoleAssignment'
2682
+ params: {
2683
+ cosmosAccountName: cosmosDbFreeTier.outputs.accountName
2684
+ functionsPrincipalId: functionsFlex.outputs.principalId
2685
+ }
2686
+ dependsOn: [
2687
+ functionsFlex
2688
+ ]
2689
+ }
2690
+
2691
+ module cosmosDbRoleAssignmentServerless 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'serverless') {
2692
+ name: 'cosmosDbRoleAssignment'
2693
+ params: {
2694
+ cosmosAccountName: cosmosDbServerless.outputs.accountName
2695
+ functionsPrincipalId: functionsFlex.outputs.principalId
2696
+ }
2697
+ dependsOn: [
2698
+ functionsFlex
2699
+ ]
2700
+ }
2701
+
2702
+ // Update SWA config with Functions hostname (after Functions deployment)
2703
+ module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
2704
+ name: 'staticWebAppConfig'
2705
+ params: {
2706
+ staticWebAppName: staticWebApp.outputs.name
2707
+ functionsDefaultHostname: functionsFlex.outputs.defaultHostname
2708
+ functionsHostKey: ${buildFunctionsHostKeyBicepExpression()}
2709
+ appInsightsConnectionString: appInsightsSwa.outputs.connectionString
2710
+ }
2711
+ dependsOn: [
2712
+ functionsFlex
2713
+ ]
2714
+ }
2715
+
2716
+ output staticWebAppName string = staticWebApp.outputs.name
2717
+ output staticWebAppUrl string = staticWebApp.outputs.defaultHostname
2718
+ output functionsAppName string = functionsFlex.outputs.name
2719
+ output functionsAppUrl string = functionsFlex.outputs.defaultHostname
2720
+ output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
2721
+ output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
2722
+ output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
2723
+ output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
2724
+ output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
2725
+ output appInsightsSwaName string = appInsightsSwa.outputs.name
2726
+ output appInsightsSwaConnectionString string = appInsightsSwa.outputs.connectionString
2727
+ output appInsightsFunctionsName string = appInsightsFunctions.outputs.name
2728
+ output appInsightsFunctionsConnectionString string = appInsightsFunctions.outputs.connectionString
2729
+ output vnetEnabled bool = enableVNet
2730
+ output vnetName string = enableVNet ? vnet.outputs.name : ''
2731
+ `;
2732
+
2733
+ fs.writeFileSync(path.join(infraDir, 'main.bicep'), mainBicep);
2734
+
2735
+ // main.parameters.json
2736
+ const params = `{
2737
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
2738
+ "contentVersion": "1.0.0.0",
2739
+ "parameters": {
2740
+ "projectName": {
2741
+ "value": "${projectName}"
2742
+ },
2743
+ "cosmosDbMode": {
2744
+ "value": "${azureConfig.cosmosDbMode}"
2745
+ },
2746
+ "staticWebAppSku": {
2747
+ "value": "${staticWebAppSku}"
2748
+ },
2749
+ "enableVNet": {
2750
+ "value": ${enableVNet}
2751
+ }
2752
+ }
2753
+ }
2754
+ `;
2755
+ fs.writeFileSync(path.join(infraDir, 'main.parameters.json'), params);
2756
+
2757
+ // modules/staticwebapp.bicep
2758
+ const staticWebAppBicep = `@description('Static Web App name')
2759
+ param name string
2760
+
2761
+ @description('Location for the Static Web App')
2762
+ param location string
2763
+
2764
+ @description('SKU name (Free or Standard)')
2765
+ @allowed([
2766
+ 'Free'
2767
+ 'Standard'
2768
+ ])
2769
+ param sku string = 'Standard'
2770
+
2771
+ @description('Application Insights connection string')
2772
+ param appInsightsConnectionString string
2773
+
2774
+ resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' = {
2775
+ name: name
2776
+ location: location
2777
+ sku: {
2778
+ name: sku
2779
+ tier: sku
2780
+ }
2781
+ properties: {
2782
+ buildProperties: {
2783
+ skipGithubActionWorkflowGeneration: true
2784
+ }
2785
+ }
2786
+ }
2787
+
2788
+ // Link Application Insights to Static Web App (for both client and server-side telemetry)
2789
+ resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
2790
+ parent: staticWebApp
2791
+ name: 'appsettings'
2792
+ properties: {
2793
+ APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
2794
+ ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
2795
+ }
2796
+ }
2797
+
2798
+ output id string = staticWebApp.id
2799
+ output name string = staticWebApp.name
2800
+ output defaultHostname string = staticWebApp.properties.defaultHostname
2801
+ `;
2802
+ fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
2803
+
2804
+ // modules/staticwebapp-config.bicep (for updating config after Functions deployment)
2805
+ const staticWebAppConfigBicep = buildStaticWebAppConfigBicepSource();
2806
+ fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
2807
+
2808
+ // modules/loganalytics.bicep (Shared Log Analytics Workspace)
2809
+ const logAnalyticsBicep = `@description('Log Analytics workspace name')
2810
+ param name string
2811
+
2812
+ @description('Location for Log Analytics workspace')
2813
+ param location string
2814
+
2815
+ resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
2816
+ name: name
2817
+ location: location
2818
+ properties: {
2819
+ sku: {
2820
+ name: 'PerGB2018'
2821
+ }
2822
+ retentionInDays: 30
2823
+ }
2824
+ }
2825
+
2826
+ output id string = logAnalytics.id
2827
+ output name string = logAnalytics.name
2828
+ `;
2829
+ fs.writeFileSync(path.join(modulesDir, 'loganalytics.bicep'), logAnalyticsBicep);
2830
+
2831
+ // modules/appinsights.bicep (Application Insights only, connects to shared Log Analytics)
2832
+ const appInsightsBicep = `@description('Application Insights name')
2833
+ param name string
2834
+
2835
+ @description('Location for Application Insights')
2836
+ param location string
2837
+
2838
+ @description('Log Analytics workspace resource ID')
2839
+ param logAnalyticsWorkspaceId string
2840
+
2841
+ // Application Insights
2842
+ resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
2843
+ name: name
2844
+ location: location
2845
+ kind: 'web'
2846
+ properties: {
2847
+ Application_Type: 'web'
2848
+ WorkspaceResourceId: logAnalyticsWorkspaceId
2849
+ RetentionInDays: 30
2850
+ }
2851
+ }
2852
+
2853
+ output id string = appInsights.id
2854
+ output name string = appInsights.name
2855
+ output connectionString string = appInsights.properties.ConnectionString
2856
+ output instrumentationKey string = appInsights.properties.InstrumentationKey
2857
+ `;
2858
+ fs.writeFileSync(path.join(modulesDir, 'appinsights.bicep'), appInsightsBicep);
2859
+
2860
+ // modules/functions-flex.bicep (Flex Consumption)
2861
+ const functionsFlexBicep = `@description('Functions App name')
2862
+ param name string
2863
+
2864
+ @description('Location for the Functions App')
2865
+ param location string
2866
+
2867
+ @description('Storage account name for Functions')
2868
+ param storageAccountName string
2869
+
2870
+ @description('Application Insights connection string')
2871
+ param appInsightsConnectionString string
2872
+
2873
+ @description('Static Web App default hostname for CORS')
2874
+ param swaDefaultHostname string
2875
+
2876
+ @description('Cosmos DB endpoint')
2877
+ param cosmosDbEndpoint string
2878
+
2879
+ @description('Cosmos DB database name')
2880
+ param cosmosDbDatabaseName string
2881
+
2882
+ @description('Enable VNet integration')
2883
+ param enableVNet bool = false
2884
+
2885
+ @description('VNet subnet ID for Functions (required if enableVNet is true)')
2886
+ param vnetSubnetId string = ''
2887
+
2888
+ @description('Functions runtime name')
2889
+ param functionsRuntimeName string
2890
+
2891
+ @description('Functions runtime version')
2892
+ param functionsRuntimeVersion string
2893
+
2894
+ // Storage Account for Functions
2895
+ resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
2896
+ name: storageAccountName
2897
+ location: location
2898
+ sku: {
2899
+ name: 'Standard_LRS'
2900
+ }
2901
+ kind: 'StorageV2'
2902
+ properties: {
2903
+ supportsHttpsTrafficOnly: true
2904
+ minimumTlsVersion: 'TLS1_2'
2905
+ }
2906
+ }
2907
+
2908
+ // Blob Service for deployment package container
2909
+ resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
2910
+ parent: storageAccount
2911
+ name: 'default'
2912
+ }
2913
+
2914
+ // Deployment package container
2915
+ resource deploymentContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
2916
+ parent: blobService
2917
+ name: 'deploymentpackage'
2918
+ properties: {
2919
+ publicAccess: 'None'
2920
+ }
2921
+ }
2922
+
2923
+ // App Service Plan (Flex Consumption)
2924
+ resource hostingPlan 'Microsoft.Web/serverfarms@2023-12-01' = {
2925
+ name: '\${name}-plan'
2926
+ location: location
2927
+ sku: {
2928
+ name: 'FC1'
2929
+ tier: 'FlexConsumption'
2930
+ }
2931
+ properties: {
2932
+ reserved: true // Required for Linux
2933
+ }
2934
+ }
2935
+
2936
+ // Azure Functions App
2937
+ resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
2938
+ name: name
2939
+ location: location
2940
+ kind: 'functionapp,linux'
2941
+ identity: {
2942
+ type: 'SystemAssigned'
2943
+ }
2944
+ properties: {
2945
+ serverFarmId: hostingPlan.id
2946
+ reserved: true
2947
+ virtualNetworkSubnetId: enableVNet ? vnetSubnetId : null
2948
+ vnetContentShareEnabled: enableVNet
2949
+ functionAppConfig: {
2950
+ deployment: {
2951
+ storage: {
2952
+ type: 'blobContainer'
2953
+ value: '\${storageAccount.properties.primaryEndpoints.blob}deploymentpackage'
2954
+ authentication: {
2955
+ type: 'SystemAssignedIdentity'
2956
+ }
2957
+ }
2958
+ }
2959
+ scaleAndConcurrency: {
2960
+ maximumInstanceCount: 100
2961
+ instanceMemoryMB: 2048
2962
+ }
2963
+ runtime: {
2964
+ name: functionsRuntimeName
2965
+ version: functionsRuntimeVersion
2966
+ }
2967
+ }
2968
+ siteConfig: {
2969
+ appSettings: [
2970
+ {
2971
+ name: 'AzureWebJobsStorage__accountName'
2972
+ value: storageAccount.name
2973
+ }
2974
+ {
2975
+ name: 'FUNCTIONS_EXTENSION_VERSION'
2976
+ value: '~4'
2977
+ }
2978
+ {
2979
+ name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
2980
+ value: appInsightsConnectionString
2981
+ }
2982
+ {
2983
+ name: 'CosmosDBConnection__accountEndpoint'
2984
+ value: cosmosDbEndpoint
2985
+ }
2986
+ {
2987
+ name: 'COSMOS_DB_DATABASE_NAME'
2988
+ value: cosmosDbDatabaseName
2989
+ }
2990
+ ]
2991
+ cors: {
2992
+ allowedOrigins: [
2993
+ 'https://\${swaDefaultHostname}'
2994
+ ]
2995
+ }
2996
+ ipSecurityRestrictions: [
2997
+ {
2998
+ action: 'Allow'
2999
+ ipAddress: 'AzureCloud'
3000
+ tag: 'ServiceTag'
3001
+ priority: 100
3002
+ }
3003
+ ]
3004
+ }
3005
+ httpsOnly: true
3006
+ }
3007
+ }
3008
+
3009
+ // Role Assignment: Storage Blob Data Contributor
3010
+ resource blobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
3011
+ name: guid(functionApp.id, storageAccount.id, 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
3012
+ scope: storageAccount
3013
+ properties: {
3014
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
3015
+ principalId: functionApp.identity.principalId
3016
+ principalType: 'ServicePrincipal'
3017
+ }
3018
+ }
3019
+
3020
+ output id string = functionApp.id
3021
+ output name string = functionApp.name
3022
+ output defaultHostname string = functionApp.properties.defaultHostName
3023
+ output principalId string = functionApp.identity.principalId
3024
+ `;
3025
+ fs.writeFileSync(path.join(modulesDir, 'functions-flex.bicep'), functionsFlexBicep);
3026
+
3027
+ // modules/cosmosdb-freetier.bicep (Free Tier)
3028
+ const cosmosDbFreeTierBicep = buildCosmosDbFreeTierBicepSource();
3029
+ fs.writeFileSync(path.join(modulesDir, 'cosmosdb-freetier.bicep'), cosmosDbFreeTierBicep);
3030
+
3031
+ // modules/cosmosdb-serverless.bicep (Serverless)
3032
+ const cosmosDbServerlessBicep = `@description('Cosmos DB account name')
3033
+ param accountName string
3034
+
3035
+ @description('Database name')
3036
+ param databaseName string
3037
+
3038
+ @description('Location for Cosmos DB')
3039
+ param location string
3040
+
3041
+ @description('Public network access')
3042
+ @allowed(['Enabled', 'Disabled'])
3043
+ param publicNetworkAccess string = 'Enabled'
3044
+
3045
+ // Cosmos DB Account (Serverless)
3046
+ resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
3047
+ name: accountName
3048
+ location: location
3049
+ kind: 'GlobalDocumentDB'
3050
+ properties: {
3051
+ databaseAccountOfferType: 'Standard'
3052
+ enableAutomaticFailover: false
3053
+ publicNetworkAccess: publicNetworkAccess
3054
+ disableLocalAuth: true
3055
+ consistencyPolicy: {
3056
+ defaultConsistencyLevel: 'Session'
3057
+ }
3058
+ locations: [
3059
+ {
3060
+ locationName: location
3061
+ failoverPriority: 0
3062
+ isZoneRedundant: false
3063
+ }
3064
+ ]
3065
+ capabilities: [
3066
+ {
3067
+ name: 'EnableServerless'
3068
+ }
3069
+ ]
3070
+ disableKeyBasedMetadataWriteAccess: true
3071
+ }
3072
+ }
3073
+
3074
+ // Cosmos DB Database
3075
+ resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
3076
+ parent: cosmosAccount
3077
+ name: databaseName
3078
+ properties: {
3079
+ resource: {
3080
+ id: databaseName
3081
+ }
3082
+ }
3083
+ }
3084
+
3085
+ output id string = cosmosAccount.id
3086
+ output accountName string = cosmosAccount.name
3087
+ output endpoint string = cosmosAccount.properties.documentEndpoint
3088
+ output databaseName string = database.name
3089
+ `;
3090
+ fs.writeFileSync(path.join(modulesDir, 'cosmosdb-serverless.bicep'), cosmosDbServerlessBicep);
3091
+
3092
+ // modules/cosmosdb-role-assignment.bicep (Role Assignment Module)
3093
+ const cosmosDbRoleAssignmentBicep = `@description('Cosmos DB account name')
3094
+ param cosmosAccountName string
3095
+
3096
+ @description('Functions App Managed Identity Principal ID')
3097
+ param functionsPrincipalId string
3098
+
3099
+ resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' existing = {
3100
+ name: cosmosAccountName
3101
+ }
3102
+
3103
+ // Built-in Cosmos DB Data Contributor role definition
3104
+ var cosmosDbDataContributorRoleId = '00000000-0000-0000-0000-000000000002'
3105
+
3106
+ // Role assignment for Functions to access Cosmos DB
3107
+ resource roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2023-11-15' = {
3108
+ parent: cosmosAccount
3109
+ name: guid(cosmosAccount.id, functionsPrincipalId, cosmosDbDataContributorRoleId)
3110
+ properties: {
3111
+ roleDefinitionId: '\${cosmosAccount.id}/sqlRoleDefinitions/\${cosmosDbDataContributorRoleId}'
3112
+ principalId: functionsPrincipalId
3113
+ scope: cosmosAccount.id
3114
+ }
3115
+ }
3116
+
3117
+ output roleAssignmentId string = roleAssignment.id
3118
+ `;
3119
+ fs.writeFileSync(path.join(modulesDir, 'cosmosdb-role-assignment.bicep'), cosmosDbRoleAssignmentBicep);
3120
+
3121
+ // Bicep resolves module files at compile time even when their deployment condition is false,
3122
+ // so these definitions must always be generated. main.bicep still controls deployment with
3123
+ // `if (enableVNet)`.
3124
+ // modules/vnet.bicep
3125
+ const vnetBicep = `@description('VNet name')
3126
+ param name string
3127
+
3128
+ @description('Location for VNet')
3129
+ param location string
3130
+
3131
+ resource vnet 'Microsoft.Network/virtualNetworks@2023-09-01' = {
3132
+ name: name
3133
+ location: location
3134
+ properties: {
3135
+ addressSpace: {
3136
+ addressPrefixes: [
3137
+ '10.0.0.0/16'
3138
+ ]
3139
+ }
3140
+ subnets: [
3141
+ {
3142
+ name: 'snet-functions'
3143
+ properties: {
3144
+ addressPrefix: '10.0.1.0/24'
3145
+ delegations: [
3146
+ {
3147
+ name: 'delegation'
3148
+ properties: {
3149
+ serviceName: 'Microsoft.App/environments'
3150
+ }
3151
+ }
3152
+ ]
3153
+ }
3154
+ }
3155
+ {
3156
+ name: 'snet-private-endpoints'
3157
+ properties: {
3158
+ addressPrefix: '10.0.2.0/24'
3159
+ privateEndpointNetworkPolicies: 'Disabled'
3160
+ }
3161
+ }
3162
+ ]
3163
+ }
3164
+ }
3165
+
3166
+ output id string = vnet.id
3167
+ output name string = vnet.name
3168
+ output functionsSubnetId string = vnet.properties.subnets[0].id
3169
+ output privateEndpointSubnetId string = vnet.properties.subnets[1].id
3170
+ `;
3171
+ fs.writeFileSync(path.join(modulesDir, 'vnet.bicep'), vnetBicep);
3172
+
3173
+ // modules/private-endpoint-cosmos.bicep
3174
+ const cosmosPrivateEndpointBicep = `@description('Private endpoint name')
3175
+ param name string
3176
+
3177
+ @description('Location')
3178
+ param location string
3179
+
3180
+ @description('Cosmos DB account resource ID')
3181
+ param cosmosAccountId string
3182
+
3183
+ @description('Cosmos DB account name')
3184
+ param cosmosAccountName string
3185
+
3186
+ @description('Subnet ID for private endpoint')
3187
+ param subnetId string
3188
+
3189
+ @description('VNet ID for DNS zone link')
3190
+ param vnetId string
3191
+
3192
+ // Private DNS Zone for Cosmos DB
3193
+ resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
3194
+ name: 'privatelink.documents.azure.com'
3195
+ location: 'global'
3196
+ }
3197
+
3198
+ // Link DNS Zone to VNet
3199
+ resource privateDnsZoneVnetLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
3200
+ parent: privateDnsZone
3201
+ name: '\${cosmosAccountName}-vnet-link'
3202
+ location: 'global'
3203
+ properties: {
3204
+ virtualNetwork: {
3205
+ id: vnetId
3206
+ }
3207
+ registrationEnabled: false
3208
+ }
3209
+ }
3210
+
3211
+ // Private Endpoint for Cosmos DB
3212
+ resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-09-01' = {
3213
+ name: name
3214
+ location: location
3215
+ properties: {
3216
+ subnet: {
3217
+ id: subnetId
3218
+ }
3219
+ privateLinkServiceConnections: [
3220
+ {
3221
+ name: '\${cosmosAccountName}-connection'
3222
+ properties: {
3223
+ privateLinkServiceId: cosmosAccountId
3224
+ groupIds: [
3225
+ 'Sql'
3226
+ ]
3227
+ }
3228
+ }
3229
+ ]
3230
+ }
3231
+ }
3232
+
3233
+ // DNS Zone Group
3234
+ resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-09-01' = {
3235
+ parent: privateEndpoint
3236
+ name: 'default'
3237
+ properties: {
3238
+ privateDnsZoneConfigs: [
3239
+ {
3240
+ name: 'cosmos-dns-config'
3241
+ properties: {
3242
+ privateDnsZoneId: privateDnsZone.id
3243
+ }
3244
+ }
3245
+ ]
3246
+ }
3247
+ }
3248
+
3249
+ output privateEndpointId string = privateEndpoint.id
3250
+ output privateDnsZoneId string = privateDnsZone.id
3251
+ `;
3252
+ fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
3253
+
3254
+ console.log('✅ VNet module definitions created\n');
3255
+
3256
+ console.log('✅ Infrastructure files created\n');
3257
+ }
3258
+
3259
+ export function getGitHubFunctionsWorkflow(pm: PackageManager, backendLanguage: BackendLanguage, projectPm: PackageManager = pm): string {
3260
+ const pmCmd = getCommands(pm);
3261
+ const pnpmSetupStep = getCiSetupStep(pm);
3262
+ const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
3263
+
3264
+ const commonSetup = ` - uses: actions/checkout@v4
3265
+
3266
+ - name: Setup Node.js
3267
+ uses: actions/setup-node@v4
3268
+ with:
3269
+ node-version: '22'
3270
+ ${pnpmSetupStep ? `\n${pnpmSetupStep}\n` : ''}
3271
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
3272
+ - name: Install dependencies
3273
+ run: |
3274
+ ${getCiInstallCommand(pm, projectPm)}
3275
+
3276
+ - name: Build shared package
3277
+ run: |
3278
+ ${pmCmd.runFilter('shared')} build
3279
+ `;
3280
+
3281
+ if (backendLanguage === 'typescript') {
3282
+ return `name: Deploy Azure Functions
3283
+
3284
+ on:
3285
+ push:
3286
+ branches:
3287
+ - main
3288
+ paths:
3289
+ - 'functions/**'
3290
+ - 'shared/**'
3291
+ pull_request:
3292
+ branches:
3293
+ - main
3294
+ paths:
3295
+ - 'functions/**'
3296
+ - 'shared/**'
3297
+ workflow_dispatch:
3298
+
3299
+ jobs:
3300
+ build-and-deploy:
3301
+ runs-on: ubuntu-latest
3302
+ name: Build and Deploy Functions
3303
+
3304
+ steps:
3305
+ ${commonSetup} - name: Build Functions
3306
+ run: |
3307
+ ${pmCmd.runFilter('functions')} build
3308
+
3309
+ - name: Prepare functions for deployment
3310
+ run: |
3311
+ SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
3312
+ mkdir -p /tmp/fn-deps
3313
+ 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));"
3314
+ cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
3315
+ rm -rf ./functions/node_modules
3316
+ mv /tmp/fn-deps/node_modules ./functions/node_modules
3317
+ SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
3318
+ mkdir -p "$SHARED_DEST"
3319
+ cp -r ./shared/dist "$SHARED_DEST/dist"
3320
+ cp ./shared/package.json "$SHARED_DEST/package.json"
3321
+
3322
+ - name: Deploy to Azure Functions
3323
+ if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
3324
+ uses: Azure/functions-action@v1
3325
+ with:
3326
+ app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
3327
+ package: './functions'
3328
+ publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
3329
+ sku: flexconsumption
3330
+ `;
3331
+ }
3332
+
3333
+ if (backendLanguage === 'csharp') {
3334
+ return `name: Deploy Azure Functions
3335
+
3336
+ on:
3337
+ push:
3338
+ branches:
3339
+ - main
3340
+ paths:
3341
+ - 'functions/**'
3342
+ - 'shared/**'
3343
+ pull_request:
3344
+ branches:
3345
+ - main
3346
+ paths:
3347
+ - 'functions/**'
3348
+ - 'shared/**'
3349
+ workflow_dispatch:
3350
+
3351
+ jobs:
3352
+ build-and-deploy:
3353
+ runs-on: ubuntu-latest
3354
+ name: Build and Deploy Functions
3355
+
3356
+ steps:
3357
+ ${commonSetup} - name: Setup .NET
3358
+ uses: actions/setup-dotnet@v4
3359
+ with:
3360
+ dotnet-version: '10.0.x'
3361
+
3362
+ - name: Publish Functions
3363
+ run: |
3364
+ dotnet publish ./functions -c Release -o ./functions/publish
3365
+
3366
+ - name: Deploy to Azure Functions
3367
+ if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
3368
+ uses: Azure/functions-action@v1
3369
+ with:
3370
+ app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
3371
+ package: './functions/publish'
3372
+ publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
3373
+ sku: flexconsumption
3374
+ `;
3375
+ }
3376
+
3377
+ return `name: Deploy Azure Functions
3378
+
3379
+ on:
3380
+ push:
3381
+ branches:
3382
+ - main
3383
+ paths:
3384
+ - 'functions/**'
3385
+ - 'shared/**'
3386
+ pull_request:
3387
+ branches:
3388
+ - main
3389
+ paths:
3390
+ - 'functions/**'
3391
+ - 'shared/**'
3392
+ workflow_dispatch:
3393
+
3394
+ jobs:
3395
+ build-and-deploy:
3396
+ runs-on: ubuntu-latest
3397
+ name: Build and Deploy Functions
3398
+
3399
+ steps:
3400
+ ${commonSetup} - name: Setup Python
3401
+ uses: actions/setup-python@v5
3402
+ with:
3403
+ python-version: '3.11'
3404
+
3405
+ - name: Install Functions dependencies
3406
+ run: |
3407
+ python -m pip install --upgrade pip
3408
+ python -m pip install -r ./functions/requirements.txt --target "./functions/.python_packages/lib/site-packages"
3409
+
3410
+ - name: Deploy to Azure Functions
3411
+ if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
3412
+ uses: Azure/functions-action@v1
3413
+ with:
3414
+ app-name: \${{ secrets.AZURE_FUNCTIONAPP_NAME }}
3415
+ package: './functions'
3416
+ publish-profile: \${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
3417
+ sku: flexconsumption
3418
+ `;
3419
+ }
3420
+
3421
+ export function getAzureFunctionsPipeline(pm: PackageManager, backendLanguage: BackendLanguage, projectPm: PackageManager = pm): string {
3422
+ const pmCmd = getCommands(pm);
3423
+ const azPipelinesSetup = getAzurePipelinesSetup(pm);
3424
+ const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
3425
+ const commonSetup = ` - task: NodeTool@0
3426
+ inputs:
3427
+ versionSpec: '22.x'
3428
+ displayName: 'Install Node.js'
3429
+ ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
3430
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
3431
+ - script: |
3432
+ ${getCiInstallCommand(pm, projectPm)}
3433
+ displayName: 'Install workspace dependencies'
3434
+
3435
+ - script: |
3436
+ ${pmCmd.runFilter('shared')} build
3437
+ displayName: 'Build shared package'
3438
+ `;
3439
+
3440
+ if (backendLanguage === 'typescript') {
3441
+ return `trigger:
3442
+ branches:
3443
+ include:
3444
+ - main
3445
+ paths:
3446
+ include:
3447
+ - functions/**
3448
+ - shared/**
3449
+
3450
+ pr:
3451
+ branches:
3452
+ include:
3453
+ - main
3454
+ paths:
3455
+ include:
3456
+ - functions/**
3457
+ - shared/**
3458
+
3459
+ pool:
3460
+ vmImage: 'ubuntu-latest'
3461
+
3462
+ variables:
3463
+ - group: azure-deployment
3464
+
3465
+ steps:
3466
+ ${commonSetup} - script: |
3467
+ ${pmCmd.runFilter('functions')} build
3468
+ displayName: 'Build Functions'
3469
+
3470
+ - script: |
3471
+ SHARED_PKG_NAME=$(node -p "require('./shared/package.json').name")
3472
+ mkdir -p /tmp/fn-deps
3473
+ 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));"
3474
+ cd /tmp/fn-deps && ${pmCmd.installProd} && cd -
3475
+ rm -rf ./functions/node_modules
3476
+ mv /tmp/fn-deps/node_modules ./functions/node_modules
3477
+ SHARED_DEST="./functions/node_modules/$SHARED_PKG_NAME"
3478
+ mkdir -p "$SHARED_DEST"
3479
+ cp -r ./shared/dist "$SHARED_DEST/dist"
3480
+ cp ./shared/package.json "$SHARED_DEST/package.json"
3481
+ displayName: 'Prepare functions for deployment'
3482
+
3483
+ - task: ArchiveFiles@2
3484
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
3485
+ inputs:
3486
+ rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions'
3487
+ includeRootFolder: false
3488
+ archiveType: 'zip'
3489
+ archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
3490
+ displayName: 'Archive Functions'
3491
+
3492
+ - task: PublishBuildArtifacts@1
3493
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
3494
+ inputs:
3495
+ PathtoPublish: '$(Build.ArtifactStagingDirectory)/functions.zip'
3496
+ ArtifactName: 'functions'
3497
+ displayName: 'Publish Functions artifact'
3498
+
3499
+ - task: AzureFunctionApp@2
3500
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
3501
+ inputs:
3502
+ azureSubscription: '$(AZURE_SUBSCRIPTION)'
3503
+ appType: 'functionAppLinux'
3504
+ appName: '$(AZURE_FUNCTIONAPP_NAME)'
3505
+ package: '$(Build.ArtifactStagingDirectory)/functions.zip'
3506
+ displayName: 'Deploy to Azure Functions'
3507
+ `;
3508
+ }
3509
+
3510
+ if (backendLanguage === 'csharp') {
3511
+ return `trigger:
3512
+ branches:
3513
+ include:
3514
+ - main
3515
+ paths:
3516
+ include:
3517
+ - functions/**
3518
+ - shared/**
3519
+
3520
+ pr:
3521
+ branches:
3522
+ include:
3523
+ - main
3524
+ paths:
3525
+ include:
3526
+ - functions/**
3527
+ - shared/**
3528
+
3529
+ pool:
3530
+ vmImage: 'ubuntu-latest'
3531
+
3532
+ variables:
3533
+ - group: azure-deployment
3534
+
3535
+ steps:
3536
+ ${commonSetup} - task: UseDotNet@2
3537
+ inputs:
3538
+ version: '10.0.x'
3539
+ displayName: 'Install .NET SDK'
3540
+
3541
+ - script: |
3542
+ dotnet publish ./functions -c Release -o ./functions/publish
3543
+ displayName: 'Publish Functions'
3544
+
3545
+ - task: ArchiveFiles@2
3546
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
3547
+ inputs:
3548
+ rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions/publish'
3549
+ includeRootFolder: false
3550
+ archiveType: 'zip'
3551
+ archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
3552
+ displayName: 'Archive Functions'
3553
+
3554
+ - task: AzureFunctionApp@2
3555
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
3556
+ inputs:
3557
+ azureSubscription: '$(AZURE_SUBSCRIPTION)'
3558
+ appType: 'functionAppLinux'
3559
+ appName: '$(AZURE_FUNCTIONAPP_NAME)'
3560
+ package: '$(Build.ArtifactStagingDirectory)/functions.zip'
3561
+ displayName: 'Deploy to Azure Functions'
3562
+ `;
3563
+ }
3564
+
3565
+ return `trigger:
3566
+ branches:
3567
+ include:
3568
+ - main
3569
+ paths:
3570
+ include:
3571
+ - functions/**
3572
+ - shared/**
3573
+
3574
+ pr:
3575
+ branches:
3576
+ include:
3577
+ - main
3578
+ paths:
3579
+ include:
3580
+ - functions/**
3581
+ - shared/**
3582
+
3583
+ pool:
3584
+ vmImage: 'ubuntu-latest'
3585
+
3586
+ variables:
3587
+ - group: azure-deployment
3588
+
3589
+ steps:
3590
+ ${commonSetup} - task: UsePythonVersion@0
3591
+ inputs:
3592
+ versionSpec: '3.11'
3593
+ displayName: 'Install Python'
3594
+
3595
+ - script: |
3596
+ python -m pip install --upgrade pip
3597
+ python -m pip install -r ./functions/requirements.txt --target "./functions/.python_packages/lib/site-packages"
3598
+ displayName: 'Install Functions dependencies'
3599
+
3600
+ - task: ArchiveFiles@2
3601
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
3602
+ inputs:
3603
+ rootFolderOrFile: '$(System.DefaultWorkingDirectory)/functions'
3604
+ includeRootFolder: false
3605
+ archiveType: 'zip'
3606
+ archiveFile: '$(Build.ArtifactStagingDirectory)/functions.zip'
3607
+ displayName: 'Archive Functions'
3608
+
3609
+ - task: AzureFunctionApp@2
3610
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
3611
+ inputs:
3612
+ azureSubscription: '$(AZURE_SUBSCRIPTION)'
3613
+ appType: 'functionAppLinux'
3614
+ appName: '$(AZURE_FUNCTIONAPP_NAME)'
3615
+ package: '$(Build.ArtifactStagingDirectory)/functions.zip'
3616
+ displayName: 'Deploy to Azure Functions'
3617
+ `;
3618
+ }
3619
+
3620
+ export function buildAzureSwaPipeline(pm: PackageManager, projectPm: PackageManager = pm): string {
3621
+ const pmCmd = getCommands(pm);
3622
+ const azPipelinesSetup = getAzurePipelinesSetup(pm);
3623
+ const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
3624
+
3625
+ return `trigger:
3626
+ branches:
3627
+ include:
3628
+ - main
3629
+ paths:
3630
+ include:
3631
+ - app/**
3632
+ - components/**
3633
+ - lib/**
3634
+ - shared/**
3635
+ - public/**
3636
+ - package.json
3637
+ - next.config.js
3638
+
3639
+ pr:
3640
+ branches:
3641
+ include:
3642
+ - main
3643
+ paths:
3644
+ include:
3645
+ - app/**
3646
+ - components/**
3647
+ - lib/**
3648
+ - shared/**
3649
+ - public/**
3650
+ - package.json
3651
+ - next.config.js
3652
+
3653
+ pool:
3654
+ vmImage: 'ubuntu-latest'
3655
+
3656
+ variables:
3657
+ - group: azure-deployment
3658
+
3659
+ steps:
3660
+ - task: NodeTool@0
3661
+ inputs:
3662
+ versionSpec: '22.x'
3663
+ displayName: 'Install Node.js'
3664
+ ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
3665
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
3666
+ - script: |
3667
+ ${getCiInstallCommand(pm, projectPm)}
3668
+ displayName: 'Install dependencies'
3669
+
3670
+ - script: |
3671
+ ${pmCmd.run} build
3672
+ env:
3673
+ NODE_ENV: production
3674
+ displayName: 'Build Next.js app'
3675
+
3676
+ - task: AzureStaticWebApp@0
3677
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
3678
+ inputs:
3679
+ app_location: '.'
3680
+ output_location: '.next/standalone'
3681
+ skip_app_build: true
3682
+ azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
3683
+ displayName: 'Deploy to Azure Static Web Apps'
3684
+ `;
3685
+ }
3686
+
3687
+ function getAzurePipelinesNpmWorkspaceNormalizationStep(pm: PackageManager, projectPm: PackageManager): string {
3688
+ if (pm !== 'npm' || projectPm !== 'pnpm') {
3689
+ return '';
3690
+ }
3691
+
3692
+ return ` - script: |
3693
+ node <<'NODE'
3694
+ const fs = require('fs');
3695
+
3696
+ function readJson(path) {
3697
+ return JSON.parse(fs.readFileSync(path, 'utf8'));
3698
+ }
3699
+
3700
+ function writeJson(path, value) {
3701
+ fs.writeFileSync(path, JSON.stringify(value, null, 2) + '\\n');
3702
+ }
3703
+
3704
+ const sharedPackagePath = 'shared/package.json';
3705
+ const sharedPackageName = fs.existsSync(sharedPackagePath) ? readJson(sharedPackagePath).name : undefined;
3706
+
3707
+ function replaceSharedWorkspaceDep(pkg, replacement) {
3708
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
3709
+ if (!pkg[section]) continue;
3710
+
3711
+ for (const [name, version] of Object.entries(pkg[section])) {
3712
+ if (typeof version === 'string' && version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
3713
+ pkg[section][name] = replacement;
3714
+ }
3715
+ }
3716
+ }
3717
+ }
3718
+
3719
+ const root = readJson('package.json');
3720
+ root.workspaces = ['shared', 'functions'];
3721
+ replaceSharedWorkspaceDep(root, 'file:shared');
3722
+ root.scripts = root.scripts || {};
3723
+ root.scripts.build = ${JSON.stringify(getBuildScript('npm'))};
3724
+ writeJson('package.json', root);
3725
+
3726
+ if (fs.existsSync('functions/package.json')) {
3727
+ const functionsPkg = readJson('functions/package.json');
3728
+ replaceSharedWorkspaceDep(functionsPkg, 'file:../shared');
3729
+ if (functionsPkg.scripts && functionsPkg.scripts.prestart) {
3730
+ functionsPkg.scripts.prestart = 'npm run build';
3731
+ }
3732
+ writeJson('functions/package.json', functionsPkg);
3733
+ }
3734
+ NODE
3735
+
3736
+ npm install --package-lock-only --ignore-scripts
3737
+ displayName: 'Normalize pnpm workspace for npm CI'`;
3738
+ }
3739
+
3740
+ function getGitHubNpmWorkspaceNormalizationStep(pm: PackageManager, projectPm: PackageManager): string {
3741
+ if (pm !== 'npm' || projectPm !== 'pnpm') {
3742
+ return '';
3743
+ }
3744
+
3745
+ return ` - name: Normalize pnpm workspace for npm CI
3746
+ shell: bash
3747
+ run: |
3748
+ node <<'NODE'
3749
+ const fs = require('fs');
3750
+
3751
+ function readJson(path) {
3752
+ return JSON.parse(fs.readFileSync(path, 'utf8'));
3753
+ }
3754
+
3755
+ function writeJson(path, value) {
3756
+ fs.writeFileSync(path, JSON.stringify(value, null, 2) + '\\n');
3757
+ }
3758
+
3759
+ const sharedPackagePath = 'shared/package.json';
3760
+ const sharedPackageName = fs.existsSync(sharedPackagePath) ? readJson(sharedPackagePath).name : undefined;
3761
+
3762
+ function replaceSharedWorkspaceDep(pkg, replacement) {
3763
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
3764
+ if (!pkg[section]) continue;
3765
+
3766
+ for (const [name, version] of Object.entries(pkg[section])) {
3767
+ if (typeof version === 'string' && version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
3768
+ pkg[section][name] = replacement;
3769
+ }
3770
+ }
3771
+ }
3772
+ }
3773
+
3774
+ const root = readJson('package.json');
3775
+ root.workspaces = ['shared', 'functions'];
3776
+ replaceSharedWorkspaceDep(root, 'file:shared');
3777
+ root.scripts = root.scripts || {};
3778
+ root.scripts.build = ${JSON.stringify(getBuildScript('npm'))};
3779
+ writeJson('package.json', root);
3780
+
3781
+ if (fs.existsSync('functions/package.json')) {
3782
+ const functionsPkg = readJson('functions/package.json');
3783
+ replaceSharedWorkspaceDep(functionsPkg, 'file:../shared');
3784
+ if (functionsPkg.scripts && functionsPkg.scripts.prestart) {
3785
+ functionsPkg.scripts.prestart = 'npm run build';
3786
+ }
3787
+ writeJson('functions/package.json', functionsPkg);
3788
+ }
3789
+ NODE
3790
+
3791
+ npm install --package-lock-only --ignore-scripts
3792
+ `;
3793
+ }
3794
+
3795
+ async function createGitHubActionsWorkflows(
3796
+ projectDir: string,
3797
+ azureConfig: AzureConfig,
3798
+ pm: PackageManager,
3799
+ backendLanguage: BackendLanguage
3800
+ ) {
3801
+ console.log('📦 Creating GitHub Actions workflows...\n');
3802
+ const workflowsDir = path.join(projectDir, '.github', 'workflows');
3803
+ fs.mkdirSync(workflowsDir, { recursive: true });
3804
+
3805
+ const actionsPm: PackageManager = 'npm';
3806
+
3807
+ const swaWorkflow = buildGitHubSwaWorkflow(actionsPm, pm);
3808
+ fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
3809
+
3810
+ // deploy-functions.yml
3811
+ const functionsWorkflow = getGitHubFunctionsWorkflow(actionsPm, backendLanguage, pm);
3812
+ fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
3813
+
3814
+ console.log('✅ GitHub Actions workflows created\n');
3815
+ }
3816
+
3817
+ export function buildGitHubSwaWorkflow(pm: PackageManager, projectPm: PackageManager = pm): string {
3818
+ const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
3819
+
3820
+ return `name: Deploy Static Web App
3821
+
3822
+ on:
3823
+ push:
3824
+ branches:
3825
+ - main
3826
+ paths:
3827
+ - 'app/**'
3828
+ - 'components/**'
3829
+ - 'lib/**'
3830
+ - 'shared/**'
3831
+ - 'public/**'
3832
+ - 'package.json'
3833
+ - 'next.config.js'
3834
+ - 'next.config.ts'
3835
+ workflow_dispatch:
3836
+ pull_request:
3837
+ branches:
3838
+ - main
3839
+ paths:
3840
+ - 'app/**'
3841
+ - 'components/**'
3842
+ - 'lib/**'
3843
+ - 'shared/**'
3844
+ - 'public/**'
3845
+ - 'package.json'
3846
+ - 'next.config.js'
3847
+ - 'next.config.ts'
3848
+
3849
+ jobs:
3850
+ build-and-deploy:
3851
+ runs-on: ubuntu-latest
3852
+ name: Build and Deploy Static Web App
3853
+
3854
+ steps:
3855
+ - uses: actions/checkout@v4
3856
+ with:
3857
+ submodules: true
3858
+
3859
+ - name: Setup Node.js
3860
+ uses: actions/setup-node@v4
3861
+ with:
3862
+ node-version: '22'
3863
+ ${getCiSetupStep(pm) ? `\n${getCiSetupStep(pm)}\n` : ''}
3864
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
3865
+ - name: Install and build app
3866
+ run: |
3867
+ ${getSwaAppBuildCommand(pm, projectPm)}
3868
+
3869
+ - name: Deploy to Azure Static Web Apps
3870
+ if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
3871
+ uses: Azure/static-web-apps-deploy@v1
3872
+ with:
3873
+ azure_static_web_apps_api_token: \${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
3874
+ repo_token: \${{ secrets.GITHUB_TOKEN }}
3875
+ action: 'upload'
3876
+ app_location: '/'
3877
+ api_location: ''
3878
+ output_location: ''
3879
+ app_build_command: '${getSwaAppBuildCommand(pm, projectPm)}'
3880
+ env:
3881
+ NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
3882
+ `;
3883
+ }
3884
+
3885
+ async function createAzurePipelines(projectDir: string, pm: PackageManager, backendLanguage: BackendLanguage) {
3886
+ console.log('📦 Creating Azure Pipelines...\n');
3887
+
3888
+ const pipelinesPm: PackageManager = 'npm';
3889
+ const pipelinesDir = path.join(projectDir, 'pipelines');
3890
+ fs.mkdirSync(pipelinesDir, { recursive: true });
3891
+
3892
+ // swa.yml
3893
+ const swaPipeline = buildAzureSwaPipeline(pipelinesPm, pm);
3894
+ fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
3895
+
3896
+ // functions.yml
3897
+ const functionsPipeline = getAzureFunctionsPipeline(pipelinesPm, backendLanguage, pm);
3898
+ fs.writeFileSync(path.join(pipelinesDir, 'functions.yml'), functionsPipeline);
3899
+
3900
+ console.log('✅ Azure Pipelines created\n');
3901
+ }
3902
+