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

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