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

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