swallowkit 1.0.0-beta.9 → 1.0.1

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 (272) hide show
  1. package/README.ja.md +87 -181
  2. package/README.md +87 -238
  3. package/dist/__tests__/fixtures.d.ts +12 -0
  4. package/dist/__tests__/fixtures.d.ts.map +1 -1
  5. package/dist/__tests__/fixtures.js +101 -0
  6. package/dist/__tests__/fixtures.js.map +1 -1
  7. package/dist/cli/commands/add-auth.d.ts +15 -0
  8. package/dist/cli/commands/add-auth.d.ts.map +1 -0
  9. package/dist/cli/commands/add-auth.js +790 -0
  10. package/dist/cli/commands/add-auth.js.map +1 -0
  11. package/dist/cli/commands/add-connector.d.ts +20 -0
  12. package/dist/cli/commands/add-connector.d.ts.map +1 -0
  13. package/dist/cli/commands/add-connector.js +164 -0
  14. package/dist/cli/commands/add-connector.js.map +1 -0
  15. package/dist/cli/commands/cosmos-worktree-isolation.d.ts +25 -0
  16. package/dist/cli/commands/cosmos-worktree-isolation.d.ts.map +1 -0
  17. package/dist/cli/commands/cosmos-worktree-isolation.js +124 -0
  18. package/dist/cli/commands/cosmos-worktree-isolation.js.map +1 -0
  19. package/dist/cli/commands/create-model.d.ts +1 -4
  20. package/dist/cli/commands/create-model.d.ts.map +1 -1
  21. package/dist/cli/commands/create-model.js +20 -82
  22. package/dist/cli/commands/create-model.js.map +1 -1
  23. package/dist/cli/commands/dev-seeds.d.ts +27 -0
  24. package/dist/cli/commands/dev-seeds.d.ts.map +1 -1
  25. package/dist/cli/commands/dev-seeds.js +188 -6
  26. package/dist/cli/commands/dev-seeds.js.map +1 -1
  27. package/dist/cli/commands/dev.d.ts +30 -0
  28. package/dist/cli/commands/dev.d.ts.map +1 -1
  29. package/dist/cli/commands/dev.js +495 -129
  30. package/dist/cli/commands/dev.js.map +1 -1
  31. package/dist/cli/commands/init.d.ts +69 -1
  32. package/dist/cli/commands/init.d.ts.map +1 -1
  33. package/dist/cli/commands/init.js +788 -766
  34. package/dist/cli/commands/init.js.map +1 -1
  35. package/dist/cli/commands/scaffold.d.ts +24 -3
  36. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  37. package/dist/cli/commands/scaffold.js +408 -189
  38. package/dist/cli/commands/scaffold.js.map +1 -1
  39. package/dist/cli/index.d.ts +5 -1
  40. package/dist/cli/index.d.ts.map +1 -1
  41. package/dist/cli/index.js +335 -58
  42. package/dist/cli/index.js.map +1 -1
  43. package/dist/core/config.d.ts +13 -3
  44. package/dist/core/config.d.ts.map +1 -1
  45. package/dist/core/config.js +151 -8
  46. package/dist/core/config.js.map +1 -1
  47. package/dist/core/mock/connector-mock-server.d.ts +101 -0
  48. package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
  49. package/dist/core/mock/connector-mock-server.js +480 -0
  50. package/dist/core/mock/connector-mock-server.js.map +1 -0
  51. package/dist/core/mock/zod-mock-generator.d.ts +14 -0
  52. package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
  53. package/dist/core/mock/zod-mock-generator.js +163 -0
  54. package/dist/core/mock/zod-mock-generator.js.map +1 -0
  55. package/dist/core/operations/auth-operations.d.ts +61 -0
  56. package/dist/core/operations/auth-operations.d.ts.map +1 -0
  57. package/dist/core/operations/auth-operations.js +272 -0
  58. package/dist/core/operations/auth-operations.js.map +1 -0
  59. package/dist/core/operations/create-model.d.ts +15 -0
  60. package/dist/core/operations/create-model.d.ts.map +1 -0
  61. package/dist/core/operations/create-model.js +185 -0
  62. package/dist/core/operations/create-model.js.map +1 -0
  63. package/dist/core/operations/file-session.d.ts +56 -0
  64. package/dist/core/operations/file-session.d.ts.map +1 -0
  65. package/dist/core/operations/file-session.js +203 -0
  66. package/dist/core/operations/file-session.js.map +1 -0
  67. package/dist/core/operations/provision-operations.d.ts +52 -0
  68. package/dist/core/operations/provision-operations.d.ts.map +1 -0
  69. package/dist/core/operations/provision-operations.js +211 -0
  70. package/dist/core/operations/provision-operations.js.map +1 -0
  71. package/dist/core/operations/runtime.d.ts +37 -0
  72. package/dist/core/operations/runtime.d.ts.map +1 -0
  73. package/dist/core/operations/runtime.js +248 -0
  74. package/dist/core/operations/runtime.js.map +1 -0
  75. package/dist/core/operations/scaffold-apply.d.ts +21 -0
  76. package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
  77. package/dist/core/operations/scaffold-apply.js +57 -0
  78. package/dist/core/operations/scaffold-apply.js.map +1 -0
  79. package/dist/core/operations/scaffold-machine.d.ts +16 -0
  80. package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
  81. package/dist/core/operations/scaffold-machine.js +51 -0
  82. package/dist/core/operations/scaffold-machine.js.map +1 -0
  83. package/dist/core/operations/scaffold-plan.d.ts +43 -0
  84. package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
  85. package/dist/core/operations/scaffold-plan.js +187 -0
  86. package/dist/core/operations/scaffold-plan.js.map +1 -0
  87. package/dist/core/project/agent-instructions.d.ts +29 -0
  88. package/dist/core/project/agent-instructions.d.ts.map +1 -0
  89. package/dist/core/project/agent-instructions.js +160 -0
  90. package/dist/core/project/agent-instructions.js.map +1 -0
  91. package/dist/core/project/artifacts.d.ts +58 -0
  92. package/dist/core/project/artifacts.d.ts.map +1 -0
  93. package/dist/core/project/artifacts.js +179 -0
  94. package/dist/core/project/artifacts.js.map +1 -0
  95. package/dist/core/project/boundaries.d.ts +50 -0
  96. package/dist/core/project/boundaries.d.ts.map +1 -0
  97. package/dist/core/project/boundaries.js +117 -0
  98. package/dist/core/project/boundaries.js.map +1 -0
  99. package/dist/core/project/capabilities.d.ts +58 -0
  100. package/dist/core/project/capabilities.d.ts.map +1 -0
  101. package/dist/core/project/capabilities.js +135 -0
  102. package/dist/core/project/capabilities.js.map +1 -0
  103. package/dist/core/project/drift.d.ts +29 -0
  104. package/dist/core/project/drift.d.ts.map +1 -0
  105. package/dist/core/project/drift.js +190 -0
  106. package/dist/core/project/drift.js.map +1 -0
  107. package/dist/core/project/infra.d.ts +49 -0
  108. package/dist/core/project/infra.d.ts.map +1 -0
  109. package/dist/core/project/infra.js +167 -0
  110. package/dist/core/project/infra.js.map +1 -0
  111. package/dist/core/project/manifest.d.ts +92 -0
  112. package/dist/core/project/manifest.d.ts.map +1 -0
  113. package/dist/core/project/manifest.js +321 -0
  114. package/dist/core/project/manifest.js.map +1 -0
  115. package/dist/core/project/state.d.ts +15 -0
  116. package/dist/core/project/state.d.ts.map +1 -0
  117. package/dist/core/project/state.js +106 -0
  118. package/dist/core/project/state.js.map +1 -0
  119. package/dist/core/project/validation.d.ts +20 -0
  120. package/dist/core/project/validation.d.ts.map +1 -0
  121. package/dist/core/project/validation.js +274 -0
  122. package/dist/core/project/validation.js.map +1 -0
  123. package/dist/core/project/workflows.d.ts +43 -0
  124. package/dist/core/project/workflows.d.ts.map +1 -0
  125. package/dist/core/project/workflows.js +299 -0
  126. package/dist/core/project/workflows.js.map +1 -0
  127. package/dist/core/scaffold/auth-generator.d.ts +63 -0
  128. package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
  129. package/dist/core/scaffold/auth-generator.js +1774 -0
  130. package/dist/core/scaffold/auth-generator.js.map +1 -0
  131. package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
  132. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
  133. package/dist/core/scaffold/connector-functions-generator.js +1029 -0
  134. package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
  135. package/dist/core/scaffold/functions-generator.d.ts +4 -3
  136. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  137. package/dist/core/scaffold/functions-generator.js +405 -134
  138. package/dist/core/scaffold/functions-generator.js.map +1 -1
  139. package/dist/core/scaffold/model-parser.d.ts +20 -0
  140. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  141. package/dist/core/scaffold/model-parser.js +229 -78
  142. package/dist/core/scaffold/model-parser.js.map +1 -1
  143. package/dist/core/scaffold/native-schema-generator.d.ts +19 -0
  144. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
  145. package/dist/core/scaffold/native-schema-generator.js +753 -0
  146. package/dist/core/scaffold/native-schema-generator.js.map +1 -0
  147. package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
  148. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  149. package/dist/core/scaffold/nextjs-generator.js +152 -8
  150. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  151. package/dist/core/scaffold/ui-generator.d.ts +17 -4
  152. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  153. package/dist/core/scaffold/ui-generator.js +174 -34
  154. package/dist/core/scaffold/ui-generator.js.map +1 -1
  155. package/dist/core/verify/index.d.ts +69 -0
  156. package/dist/core/verify/index.d.ts.map +1 -0
  157. package/dist/core/verify/index.js +412 -0
  158. package/dist/core/verify/index.js.map +1 -0
  159. package/dist/machine/contracts.d.ts +24 -0
  160. package/dist/machine/contracts.d.ts.map +1 -0
  161. package/dist/machine/contracts.js +3 -0
  162. package/dist/machine/contracts.js.map +1 -0
  163. package/dist/machine/errors.d.ts +14 -0
  164. package/dist/machine/errors.d.ts.map +1 -0
  165. package/dist/machine/errors.js +42 -0
  166. package/dist/machine/errors.js.map +1 -0
  167. package/dist/machine/index.d.ts +3 -0
  168. package/dist/machine/index.d.ts.map +1 -0
  169. package/dist/machine/index.js +566 -0
  170. package/dist/machine/index.js.map +1 -0
  171. package/dist/mcp/index.d.ts +25 -0
  172. package/dist/mcp/index.d.ts.map +1 -0
  173. package/dist/mcp/index.js +425 -0
  174. package/dist/mcp/index.js.map +1 -0
  175. package/dist/types/index.d.ts +105 -0
  176. package/dist/types/index.d.ts.map +1 -1
  177. package/dist/utils/package-manager.d.ts +7 -11
  178. package/dist/utils/package-manager.d.ts.map +1 -1
  179. package/dist/utils/package-manager.js +42 -17
  180. package/dist/utils/package-manager.js.map +1 -1
  181. package/dist/utils/python-uv.d.ts +21 -0
  182. package/dist/utils/python-uv.d.ts.map +1 -0
  183. package/dist/utils/python-uv.js +111 -0
  184. package/dist/utils/python-uv.js.map +1 -0
  185. package/dist/version.d.ts +2 -0
  186. package/dist/version.d.ts.map +1 -0
  187. package/dist/version.js +49 -0
  188. package/dist/version.js.map +1 -0
  189. package/package.json +15 -17
  190. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +716 -22
  191. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +19 -7
  192. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +20 -12
  193. package/src/__tests__/agent-instructions.test.ts +237 -0
  194. package/src/__tests__/agent-loop-phase34.test.ts +619 -0
  195. package/src/__tests__/auth.test.ts +770 -0
  196. package/src/__tests__/bff-swa-auth.test.ts +275 -0
  197. package/src/__tests__/config.test.ts +176 -3
  198. package/src/__tests__/connector-functions-generator.test.ts +288 -0
  199. package/src/__tests__/connector-mock-server.test.ts +459 -0
  200. package/src/__tests__/connector-model-bff.test.ts +162 -0
  201. package/src/__tests__/cosmos-worktree-isolation.test.ts +149 -0
  202. package/src/__tests__/dev-seeds.test.ts +124 -0
  203. package/src/__tests__/dev.test.ts +254 -0
  204. package/src/__tests__/file-session.test.ts +98 -0
  205. package/src/__tests__/fixtures.ts +101 -0
  206. package/src/__tests__/functions-generator.test.ts +137 -1
  207. package/src/__tests__/init.test.ts +366 -2
  208. package/src/__tests__/jest-globals.d.ts +8 -0
  209. package/src/__tests__/machine.test.ts +371 -0
  210. package/src/__tests__/mcp.test.ts +253 -0
  211. package/src/__tests__/model-parser.test.ts +142 -0
  212. package/src/__tests__/multi-auth.test.ts +160 -0
  213. package/src/__tests__/nextjs-generator.test.ts +8 -0
  214. package/src/__tests__/package-manager.test.ts +59 -4
  215. package/src/__tests__/plan-apply.test.ts +499 -0
  216. package/src/__tests__/python-uv.test.ts +48 -0
  217. package/src/__tests__/scaffold.test.ts +287 -24
  218. package/src/__tests__/tsconfig.json +15 -0
  219. package/src/__tests__/ui-generator.test.ts +104 -0
  220. package/src/__tests__/zod-mock-generator.test.ts +132 -0
  221. package/src/cli/commands/add-auth.ts +853 -0
  222. package/src/cli/commands/add-connector.ts +159 -0
  223. package/src/cli/commands/cosmos-worktree-isolation.ts +130 -0
  224. package/src/cli/commands/create-model.ts +21 -100
  225. package/src/cli/commands/dev-seeds.ts +278 -7
  226. package/src/cli/commands/dev.ts +607 -160
  227. package/src/cli/commands/init.ts +851 -786
  228. package/src/cli/commands/scaffold.ts +531 -236
  229. package/src/cli/index.ts +338 -60
  230. package/src/core/config.ts +145 -9
  231. package/src/core/mock/connector-mock-server.ts +555 -0
  232. package/src/core/mock/zod-mock-generator.ts +205 -0
  233. package/src/core/operations/auth-operations.ts +360 -0
  234. package/src/core/operations/create-model.ts +191 -0
  235. package/src/core/operations/file-session.ts +222 -0
  236. package/src/core/operations/provision-operations.ts +297 -0
  237. package/src/core/operations/runtime.ts +258 -0
  238. package/src/core/operations/scaffold-apply.ts +106 -0
  239. package/src/core/operations/scaffold-machine.ts +77 -0
  240. package/src/core/operations/scaffold-plan.ts +215 -0
  241. package/src/core/project/agent-instructions.ts +167 -0
  242. package/src/core/project/agent-templates/AGENTS.md +504 -0
  243. package/src/core/project/agent-templates/CLAUDE.md +31 -0
  244. package/src/core/project/agent-templates/azure-functions.instructions.md +22 -0
  245. package/src/core/project/agent-templates/bff-routes.instructions.md +18 -0
  246. package/src/core/project/agent-templates/copilot-instructions.md +27 -0
  247. package/src/core/project/agent-templates/shared-models.instructions.md +72 -0
  248. package/src/core/project/artifacts.ts +205 -0
  249. package/src/core/project/boundaries.ts +170 -0
  250. package/src/core/project/capabilities.ts +193 -0
  251. package/src/core/project/drift.ts +203 -0
  252. package/src/core/project/infra.ts +196 -0
  253. package/src/core/project/manifest.ts +402 -0
  254. package/src/core/project/state.ts +77 -0
  255. package/src/core/project/validation.ts +295 -0
  256. package/src/core/project/workflows.ts +295 -0
  257. package/src/core/scaffold/auth-generator.ts +1824 -0
  258. package/src/core/scaffold/connector-functions-generator.ts +1133 -0
  259. package/src/core/scaffold/functions-generator.ts +432 -134
  260. package/src/core/scaffold/model-parser.ts +244 -90
  261. package/src/core/scaffold/native-schema-generator.ts +904 -0
  262. package/src/core/scaffold/nextjs-generator.ts +174 -9
  263. package/src/core/scaffold/ui-generator.ts +191 -36
  264. package/src/core/verify/index.ts +468 -0
  265. package/src/machine/contracts.ts +33 -0
  266. package/src/machine/errors.ts +45 -0
  267. package/src/machine/index.ts +690 -0
  268. package/src/mcp/index.ts +423 -0
  269. package/src/types/index.ts +137 -0
  270. package/src/utils/package-manager.ts +41 -19
  271. package/src/utils/python-uv.ts +96 -0
  272. package/src/version.ts +13 -0
@@ -9,11 +9,21 @@ import {
9
9
  getCommands,
10
10
  getWorkspaceConfig,
11
11
  getCiSetupStep,
12
+ getCiInstallCommand,
12
13
  getAzurePipelinesSetup,
13
14
  getBuildScript,
14
15
  getFunctionsPrestart,
15
16
  getFunctionsStartScript,
17
+ getSwaAppBuildCommand,
16
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";
17
27
 
18
28
  interface InitOptions {
19
29
  name: string;
@@ -23,15 +33,23 @@ interface InitOptions {
23
33
  backendLanguage?: BackendLanguage;
24
34
  cosmosDbMode?: CosmosDbMode;
25
35
  vnet?: VNetOption;
36
+ swaPlan?: StaticWebAppPlan;
37
+ packageManager?: PackageManager;
26
38
  }
27
39
 
28
40
  type CiCdProvider = 'github' | 'azure' | 'skip';
29
41
  type CosmosDbMode = 'freetier' | 'serverless';
30
42
  type VNetOption = 'none' | 'outbound';
43
+ type StaticWebAppPlan = 'free' | 'standard';
44
+
45
+ export function getStaticWebAppSku(plan: StaticWebAppPlan): 'Free' | 'Standard' {
46
+ return plan === 'free' ? 'Free' : 'Standard';
47
+ }
31
48
 
32
- interface AzureConfig {
49
+ export interface AzureConfig {
33
50
  cosmosDbMode: CosmosDbMode;
34
51
  vnetOption: VNetOption;
52
+ swaPlan: StaticWebAppPlan;
35
53
  }
36
54
 
37
55
  const BACKEND_LANGUAGE_CHOICES: Array<{ title: string; value: BackendLanguage }> = [
@@ -48,6 +66,66 @@ function getBackendLanguageLabel(backendLanguage: BackendLanguage): string {
48
66
  return BACKEND_LANGUAGE_CHOICES.find((choice) => choice.value === backendLanguage)?.title || backendLanguage;
49
67
  }
50
68
 
69
+ export function buildCosmosDbFreeTierBicepSource(): string {
70
+ return `@description('Cosmos DB account name')
71
+ param accountName string
72
+
73
+ @description('Database name')
74
+ param databaseName string
75
+
76
+ @description('Location for Cosmos DB')
77
+ param location string
78
+
79
+ @description('Public network access')
80
+ @allowed(['Enabled', 'Disabled'])
81
+ param publicNetworkAccess string = 'Enabled'
82
+
83
+ // Cosmos DB Account (Free Tier)
84
+ resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
85
+ name: accountName
86
+ location: location
87
+ kind: 'GlobalDocumentDB'
88
+ properties: {
89
+ databaseAccountOfferType: 'Standard'
90
+ enableAutomaticFailover: false
91
+ enableFreeTier: true
92
+ publicNetworkAccess: publicNetworkAccess
93
+ disableLocalAuth: true
94
+ consistencyPolicy: {
95
+ defaultConsistencyLevel: 'Session'
96
+ }
97
+ locations: [
98
+ {
99
+ locationName: location
100
+ failoverPriority: 0
101
+ isZoneRedundant: false
102
+ }
103
+ ]
104
+ disableKeyBasedMetadataWriteAccess: true
105
+ }
106
+ }
107
+
108
+ // Cosmos DB Database
109
+ resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
110
+ parent: cosmosAccount
111
+ name: databaseName
112
+ properties: {
113
+ resource: {
114
+ id: databaseName
115
+ }
116
+ options: {
117
+ throughput: 1000
118
+ }
119
+ }
120
+ }
121
+
122
+ output id string = cosmosAccount.id
123
+ output accountName string = cosmosAccount.name
124
+ output endpoint string = cosmosAccount.properties.documentEndpoint
125
+ output databaseName string = database.name
126
+ `;
127
+ }
128
+
51
129
  function getFunctionsWorkerRuntime(backendLanguage: BackendLanguage): string {
52
130
  if (backendLanguage === "csharp") {
53
131
  return "dotnet-isolated";
@@ -60,7 +138,7 @@ function getFunctionsWorkerRuntime(backendLanguage: BackendLanguage): string {
60
138
 
61
139
  function getFunctionsRuntimeConfig(backendLanguage: BackendLanguage): { name: string; version: string } {
62
140
  if (backendLanguage === "csharp") {
63
- return { name: "dotnet-isolated", version: "8.0" };
141
+ return { name: "dotnet-isolated", version: "10.0" };
64
142
  }
65
143
  if (backendLanguage === "python") {
66
144
  return { name: "python", version: "3.11" };
@@ -68,6 +146,28 @@ function getFunctionsRuntimeConfig(backendLanguage: BackendLanguage): { name: st
68
146
  return { name: "node", version: "22" };
69
147
  }
70
148
 
149
+ export function buildSharedTsConfig() {
150
+ return {
151
+ compilerOptions: {
152
+ target: "ES2020",
153
+ module: "Node16",
154
+ moduleResolution: "node16",
155
+ lib: ["ES2020"],
156
+ outDir: "dist",
157
+ rootDir: ".",
158
+ declaration: true,
159
+ declarationMap: true,
160
+ sourceMap: true,
161
+ strict: true,
162
+ esModuleInterop: true,
163
+ skipLibCheck: true,
164
+ forceConsistentCasingInFileNames: true,
165
+ },
166
+ include: ["index.ts", "models/**/*"],
167
+ exclude: ["node_modules", "dist"],
168
+ };
169
+ }
170
+
71
171
  async function promptBackendLanguage(): Promise<BackendLanguage> {
72
172
  const response = await prompts({
73
173
  type: "select",
@@ -96,32 +196,56 @@ async function promptCiCd(): Promise<CiCdProvider> {
96
196
  return response.cicd || 'skip';
97
197
  }
98
198
 
99
- async function promptAzureConfig(): Promise<AzureConfig> {
100
- const cosmosResponse = await prompts({
101
- type: 'select',
102
- name: 'mode',
103
- message: 'Cosmos DB mode (affects cost):',
104
- choices: [
105
- { title: 'Free Tier (1000 RU/s free, best for first project)', value: 'freetier' },
106
- { title: 'Serverless (pay-per-use, flexible)', value: 'serverless' }
107
- ],
108
- initial: 0
109
- });
110
-
111
- const vnetResponse = await prompts({
112
- type: 'select',
113
- name: 'vnet',
114
- message: 'Network security:',
115
- choices: [
116
- { title: 'VNet Integration (recommended) - Cosmos DB via Private Endpoint', value: 'outbound' },
117
- { title: 'None - Public endpoints, simplest but least secure', value: 'none' }
118
- ],
119
- initial: 0
120
- });
199
+ async function promptAzureConfig(options: InitOptions): Promise<AzureConfig> {
200
+ let swaPlan = options.swaPlan;
201
+ if (!swaPlan) {
202
+ const response = await prompts({
203
+ type: 'select',
204
+ name: 'plan',
205
+ message: 'Azure Static Web Apps plan:',
206
+ choices: [
207
+ { title: 'Standard (production, SLA and advanced auth/networking features)', value: 'standard' },
208
+ { title: 'Free (personal projects, no SLA and lower quotas)', value: 'free' }
209
+ ],
210
+ initial: 0
211
+ });
212
+ swaPlan = response.plan || 'standard';
213
+ }
214
+
215
+ let cosmosDbMode = options.cosmosDbMode;
216
+ if (!cosmosDbMode) {
217
+ const response = await prompts({
218
+ type: 'select',
219
+ name: 'mode',
220
+ message: 'Cosmos DB mode (affects cost):',
221
+ choices: [
222
+ { title: 'Free Tier (1000 RU/s free, best for first project)', value: 'freetier' },
223
+ { title: 'Serverless (pay-per-use, flexible)', value: 'serverless' }
224
+ ],
225
+ initial: 0
226
+ });
227
+ cosmosDbMode = response.mode || 'freetier';
228
+ }
229
+
230
+ let vnetOption = options.vnet;
231
+ if (!vnetOption) {
232
+ const response = await prompts({
233
+ type: 'select',
234
+ name: 'vnet',
235
+ message: 'Network security:',
236
+ choices: [
237
+ { title: 'VNet Integration (recommended) - Cosmos DB via Private Endpoint', value: 'outbound' },
238
+ { title: 'None - Public endpoints, simplest but least secure', value: 'none' }
239
+ ],
240
+ initial: 0
241
+ });
242
+ vnetOption = response.vnet || 'outbound';
243
+ }
121
244
 
122
245
  return {
123
- cosmosDbMode: cosmosResponse.mode || 'freetier',
124
- vnetOption: vnetResponse.vnet || 'outbound'
246
+ cosmosDbMode: cosmosDbMode || 'freetier',
247
+ vnetOption: vnetOption || 'outbound',
248
+ swaPlan: swaPlan || 'standard'
125
249
  };
126
250
  }
127
251
 
@@ -129,8 +253,12 @@ const VALID_CICD: CiCdProvider[] = ['github', 'azure', 'skip'];
129
253
  const VALID_BACKEND_LANGUAGE: BackendLanguage[] = ['typescript', 'csharp', 'python'];
130
254
  const VALID_COSMOS_DB_MODE: CosmosDbMode[] = ['freetier', 'serverless'];
131
255
  const VALID_VNET: VNetOption[] = ['none', 'outbound'];
256
+ const VALID_SWA_PLAN: StaticWebAppPlan[] = ['free', 'standard'];
132
257
 
133
258
  function validateInitFlags(options: InitOptions): void {
259
+ if (options.packageManager && !['npm', 'pnpm'].includes(options.packageManager)) {
260
+ throw new Error('Invalid --package-manager: expected npm or pnpm');
261
+ }
134
262
  if (options.cicd && !VALID_CICD.includes(options.cicd)) {
135
263
  console.error(`❌ Invalid --cicd value: "${options.cicd}". Must be: ${VALID_CICD.join(', ')}`);
136
264
  process.exit(1);
@@ -147,6 +275,10 @@ function validateInitFlags(options: InitOptions): void {
147
275
  console.error(`❌ Invalid --vnet value: "${options.vnet}". Must be: ${VALID_VNET.join(', ')}`);
148
276
  process.exit(1);
149
277
  }
278
+ if (options.swaPlan && !VALID_SWA_PLAN.includes(options.swaPlan)) {
279
+ console.error(`❌ Invalid --swa-plan value: "${options.swaPlan}". Must be: ${VALID_SWA_PLAN.join(', ')}`);
280
+ process.exit(1);
281
+ }
150
282
  }
151
283
 
152
284
  export async function initCommand(options: InitOptions) {
@@ -157,7 +289,7 @@ export async function initCommand(options: InitOptions) {
157
289
  console.log(`📋 Template: ${options.template}`);
158
290
 
159
291
  // Detect package manager from invocation context (npx → npm, pnpm dlx → pnpm)
160
- const pm: PackageManager = detectFromUserAgent();
292
+ const pm: PackageManager = options.packageManager || detectFromUserAgent();
161
293
  const pmCmd = getCommands(pm);
162
294
  console.log(`📦 Package manager: ${pm}`);
163
295
 
@@ -170,13 +302,30 @@ export async function initCommand(options: InitOptions) {
170
302
  process.exit(1);
171
303
  }
172
304
 
305
+ // 非対話環境でプロンプトに入るとハングするため、不足フラグを明示エラーで案内する
306
+ const interactive =
307
+ process.stdin.isTTY === true && process.stdout.isTTY === true && !process.env.CI;
308
+ if (!interactive) {
309
+ const missing: string[] = [];
310
+ if (!options.cicd) missing.push('--cicd <github|azure|skip>');
311
+ if (!options.backendLanguage) missing.push('--backend-language <typescript|csharp|python>');
312
+ if (!options.cosmosDbMode) missing.push('--cosmos-db-mode <freetier|serverless>');
313
+ if (!options.vnet) missing.push('--vnet <outbound|none>');
314
+ if (!options.swaPlan) missing.push('--swa-plan <free|standard>');
315
+ if (missing.length > 0) {
316
+ console.error(
317
+ `❌ Non-interactive environment detected. Pass all configuration flags to run init without prompts:\n` +
318
+ missing.map((flag) => ` ${flag}`).join('\n')
319
+ );
320
+ process.exit(1);
321
+ }
322
+ }
323
+
173
324
  // Use flag values if provided, otherwise prompt interactively
174
325
  const cicdProvider: CiCdProvider = options.cicd || await promptCiCd();
175
326
  const backendLanguage: BackendLanguage = options.backendLanguage || await promptBackendLanguage();
176
327
 
177
- const azureConfig: AzureConfig = (options.cosmosDbMode && options.vnet)
178
- ? { cosmosDbMode: options.cosmosDbMode, vnetOption: options.vnet }
179
- : await promptAzureConfig();
328
+ const azureConfig = await promptAzureConfig(options);
180
329
 
181
330
  // Create Next.js project with create-next-app
182
331
  await createNextJsProject(options.name, pm);
@@ -197,6 +346,8 @@ export async function initCommand(options: InitOptions) {
197
346
  await createAzurePipelines(projectDir, pm, backendLanguage);
198
347
  }
199
348
 
349
+ await syncProjectManifest(projectDir);
350
+
200
351
  // Initialize Git repository and create initial commit
201
352
  try {
202
353
  // Try git init with -b main (Git 2.28+), fallback to git init
@@ -241,11 +392,11 @@ export async function initCommand(options: InitOptions) {
241
392
  console.log(`\n✅ Project "${options.name}" created successfully!`);
242
393
  console.log("\n📝 Next steps:");
243
394
  console.log(` cd ${options.name}`);
244
- console.log(` ${pmCmd.dlx} swallowkit create-model <name> # Create your first model`);
245
- console.log(` ${pmCmd.dlx} swallowkit scaffold shared/models/<name>.ts # Generate CRUD code`);
246
- console.log(` ${pmCmd.dlx} swallowkit dev # Start development servers`);
395
+ console.log(` ${pmCmd.exec} swallowkit create-model <name> # Create your first model`);
396
+ console.log(` ${pmCmd.exec} swallowkit scaffold shared/models/<name>.ts # Generate CRUD code`);
397
+ console.log(` ${pmCmd.exec} swallowkit dev # Start development servers`);
247
398
  console.log("\n🚀 Deploy to Azure:");
248
- console.log(` ${pmCmd.dlx} swallowkit provision --resource-group <name>`);
399
+ console.log(` ${pmCmd.exec} swallowkit provision --resource-group <name>`);
249
400
  if (cicdProvider !== 'skip') {
250
401
  console.log(" Configure CI/CD secrets and push to repository");
251
402
  }
@@ -269,7 +420,7 @@ async function createNextJsProject(projectName: string, pm: PackageManager): Pro
269
420
  // for npm use "npx create-next-app@latest ..."
270
421
  const baseArgs = pm === 'pnpm'
271
422
  ? ['dlx', 'create-next-app@latest']
272
- : ['create-next-app@latest'];
423
+ : ['--yes', '--package', 'create-next-app@latest', '--', 'create-next-app'];
273
424
 
274
425
  const args = [
275
426
  ...baseArgs,
@@ -279,6 +430,7 @@ async function createNextJsProject(projectName: string, pm: PackageManager): Pro
279
430
  '--app',
280
431
  '--no-src',
281
432
  '--disable-git',
433
+ '--skip-install',
282
434
  '--import-alias',
283
435
  '@/*',
284
436
  ...(pmCmd.createNextAppFlag ? [pmCmd.createNextAppFlag] : []),
@@ -311,84 +463,211 @@ async function createNextJsProject(projectName: string, pm: PackageManager): Pro
311
463
  }
312
464
 
313
465
  async function upgradeNextJs(projectDir: string, version: string, pm: PackageManager): Promise<void> {
314
- return new Promise((resolve, reject) => {
315
- console.log(`\n📦 Installing Next.js ${version} (to ensure latest security patches)...\n`);
316
-
317
- // pnpm: pnpm add next@... ; npm: npm install next@...
318
- const args = pm === 'pnpm'
319
- ? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact']
320
- : ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact'];
466
+ console.log(`\n📦 Installing Next.js ${version} (to ensure latest security patches)...\n`);
321
467
 
322
- const child = spawn(
323
- pm,
324
- args,
325
- {
326
- cwd: projectDir,
327
- stdio: 'inherit',
328
- shell: true,
329
- }
330
- );
468
+ // pnpm: pnpm add next@... ; npm: npm install next@...
469
+ const args = pm === 'pnpm'
470
+ ? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact', '--lockfile-only']
471
+ : ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact', '--package-lock-only'];
331
472
 
332
- child.on('close', (code: number | null) => {
333
- if (code !== 0) {
334
- reject(new Error(`${pm} add next@${version} exited with code ${code}`));
335
- } else {
336
- console.log(`\n✅ Next.js ${version} installed\n`);
337
- resolve();
338
- }
339
- });
473
+ await runPackageManagerCommand(pm, args, projectDir, `${pm} add next@${version}`);
340
474
 
341
- child.on('error', (error: Error) => {
342
- reject(error);
343
- });
344
- });
475
+ console.log(`\n✅ Next.js ${version} installed\n`);
345
476
  }
346
477
 
347
478
  async function installDependencies(projectDir: string, pm: PackageManager = 'pnpm'): Promise<void> {
348
- return new Promise((resolve, reject) => {
349
- console.log('\n📦 Installing dependencies...\n');
350
-
351
- const child = spawn(
352
- pm,
353
- ['install'],
354
- {
355
- cwd: projectDir,
356
- stdio: 'inherit',
357
- shell: true,
479
+ console.log('\n📦 Installing dependencies...\n');
480
+ await runPackageManagerCommand(pm, pm === 'pnpm' ? ['install', '--no-frozen-lockfile'] : ['install'], projectDir, `${pm} install`);
481
+
482
+ console.log('\n✅ Dependencies installed\n');
483
+ }
484
+
485
+ type PackageJson = {
486
+ dependencies?: Record<string, string>;
487
+ devDependencies?: Record<string, string>;
488
+ optionalDependencies?: Record<string, string>;
489
+ [key: string]: unknown;
490
+ };
491
+
492
+ export async function withNpmLockfileSafeManifests(projectDir: string, action: () => Promise<void>): Promise<void> {
493
+ const packageJsonPath = path.join(projectDir, 'package.json');
494
+ const functionsPackageJsonPath = path.join(projectDir, 'functions', 'package.json');
495
+ const sharedPackageJsonPath = path.join(projectDir, 'shared', 'package.json');
496
+ const originalFiles = new Map<string, string>();
497
+ const hiddenNodeModules = new Map<string, string>();
498
+
499
+ function rememberOriginal(filePath: string): void {
500
+ if (!originalFiles.has(filePath)) {
501
+ originalFiles.set(filePath, fs.readFileSync(filePath, 'utf-8'));
502
+ }
503
+ }
504
+
505
+ function readJson(filePath: string): PackageJson {
506
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
507
+ }
508
+
509
+ function writeJson(filePath: string, value: PackageJson): void {
510
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
511
+ }
512
+
513
+ const sharedPackageName = fs.existsSync(sharedPackageJsonPath)
514
+ ? (readJson(sharedPackageJsonPath).name as string | undefined)
515
+ : undefined;
516
+
517
+ function replaceSharedWorkspaceDependency(packageJson: PackageJson, replacement: string): boolean {
518
+ let changed = false;
519
+
520
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies'] as const) {
521
+ const dependencies = packageJson[section];
522
+ if (!dependencies) continue;
523
+
524
+ for (const [name, version] of Object.entries(dependencies)) {
525
+ if (version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
526
+ dependencies[name] = replacement;
527
+ changed = true;
528
+ }
358
529
  }
359
- );
530
+ }
360
531
 
361
- child.on('close', (code: number | null) => {
362
- if (code !== 0) {
363
- reject(new Error(`${pm} install exited with code ${code}`));
364
- } else {
365
- console.log('\n✅ Dependencies installed\n');
366
- resolve();
532
+ return changed;
533
+ }
534
+
535
+ function normalizePackageJson(filePath: string, replacement: string): void {
536
+ if (!fs.existsSync(filePath)) return;
537
+
538
+ const packageJson = readJson(filePath);
539
+ if (!replaceSharedWorkspaceDependency(packageJson, replacement)) return;
540
+
541
+ rememberOriginal(filePath);
542
+ writeJson(filePath, packageJson);
543
+ }
544
+
545
+ function hideNodeModules(directory: string): void {
546
+ const nodeModulesPath = path.join(directory, 'node_modules');
547
+ if (!fs.existsSync(nodeModulesPath)) return;
548
+
549
+ const hiddenPath = path.join(directory, `.node_modules.swallowkit-npm-lockfile-${process.pid}`);
550
+ fs.renameSync(nodeModulesPath, hiddenPath);
551
+ hiddenNodeModules.set(nodeModulesPath, hiddenPath);
552
+ }
553
+
554
+ try {
555
+ normalizePackageJson(packageJsonPath, 'file:shared');
556
+ normalizePackageJson(functionsPackageJsonPath, 'file:../shared');
557
+ hideNodeModules(projectDir);
558
+ hideNodeModules(path.join(projectDir, 'shared'));
559
+ hideNodeModules(path.join(projectDir, 'functions'));
560
+ await action();
561
+ } finally {
562
+ for (const [nodeModulesPath, hiddenPath] of hiddenNodeModules) {
563
+ if (fs.existsSync(nodeModulesPath)) {
564
+ fs.rmSync(nodeModulesPath, { recursive: true, force: true });
367
565
  }
368
- });
566
+ fs.renameSync(hiddenPath, nodeModulesPath);
567
+ }
369
568
 
370
- child.on('error', (error: Error) => {
371
- reject(error);
569
+ for (const [filePath, content] of originalFiles) {
570
+ fs.writeFileSync(filePath, content);
571
+ }
572
+ }
573
+ }
574
+
575
+ /** Run installs without changing the project security policy. */
576
+ async function runPackageManagerCommand(
577
+ pm: PackageManager,
578
+ args: string[],
579
+ projectDir: string,
580
+ label: string,
581
+ ): Promise<void> {
582
+ const { code, output } = await spawnAndCapture(pm, args, projectDir, pm === 'pnpm');
583
+
584
+ if (code === 0) return;
585
+
586
+ if (pm === 'pnpm') {
587
+ if (parseIgnoredBuilds(output).length > 0) {
588
+ throw new Error(
589
+ `${label} exited with code ${code}: pnpm still reports unapproved build scripts. ` +
590
+ 'Review the named dependencies and project security policy before allowing any scripts. SwallowKit does not auto-approve them.'
591
+ );
592
+ }
593
+ }
594
+
595
+ throw new Error(`${label} exited with code ${code}`);
596
+ }
597
+
598
+ /**
599
+ * Spawn a child process inheriting stdin (so interactive prompts still work)
600
+ * while either inheriting or capturing+teeing stdout/stderr.
601
+ */
602
+ function spawnAndCapture(
603
+ pm: PackageManager,
604
+ args: string[],
605
+ projectDir: string,
606
+ capture: boolean,
607
+ ): Promise<{ code: number; output: string }> {
608
+ return new Promise((resolve, reject) => {
609
+ const stdio: ('inherit' | 'pipe')[] = capture
610
+ ? ['inherit', 'pipe', 'pipe']
611
+ : ['inherit', 'inherit', 'inherit'];
612
+
613
+ const child = spawn(pm, args, {
614
+ cwd: projectDir,
615
+ stdio,
616
+ shell: true,
372
617
  });
618
+
619
+ let combined = '';
620
+ if (capture) {
621
+ child.stdout?.on('data', (chunk: Buffer) => {
622
+ process.stdout.write(chunk);
623
+ combined += chunk.toString('utf8');
624
+ });
625
+ child.stderr?.on('data', (chunk: Buffer) => {
626
+ process.stderr.write(chunk);
627
+ combined += chunk.toString('utf8');
628
+ });
629
+ }
630
+
631
+ child.on('close', (code) => resolve({ code: code ?? 0, output: combined }));
632
+ child.on('error', reject);
373
633
  });
374
634
  }
375
635
 
636
+ /**
637
+ * Parse pnpm's `Ignored build scripts: foo@1.0.0, bar@2.0.0` warning/error and
638
+ * return the bare package names (without versions), de-duplicated.
639
+ */
640
+ export function parseIgnoredBuilds(output: string): string[] {
641
+ const match = output.match(/Ignored build scripts:\s*([^\n\r]+)/i);
642
+ if (!match) return [];
643
+ const list = match[1]
644
+ .split(',')
645
+ .map((entry) => entry.trim())
646
+ // strip version suffix: `sharp@0.34.5` -> `sharp`, `@scope/pkg@1.0.0` -> `@scope/pkg`
647
+ .map((entry) => entry.replace(/@[^@]+$/, ''))
648
+ .filter((name) => name.length > 0);
649
+ return Array.from(new Set(list));
650
+ }
651
+
376
652
  export function injectSwallowKitNextConfig(nextConfigContent: string, projectName: string): string {
377
653
  return nextConfigContent.replace(
378
654
  /(const\s+nextConfig[:\s]*(?::\s*NextConfig\s*)?=\s*\{)(\s*\/\*[^*]*\*\/)?/,
379
- `$1\n output: 'standalone',\n transpilePackages: ['@${projectName}/shared'],\n serverExternalPackages: ['applicationinsights', 'diagnostic-channel-publishers'],$2`
655
+ `$1\n output: 'standalone',\n agentRules: false, // SwallowKit owns the canonical root AGENTS.md contract\n transpilePackages: ['@${projectName}/shared'],\n serverExternalPackages: ['applicationinsights', 'diagnostic-channel-publishers'],$2`
380
656
  );
381
657
  }
382
658
 
383
659
  export function buildCSharpFunctionsProgramSource(): string {
384
- return `using Microsoft.Extensions.DependencyInjection;
660
+ return `using Microsoft.Azure.Functions.Worker;
661
+ using Microsoft.Extensions.DependencyInjection;
385
662
  using Microsoft.Extensions.Hosting;
663
+ using Microsoft.Azure.Functions.Worker.ApplicationInsights;
386
664
 
387
665
  var host = new HostBuilder()
388
666
  .ConfigureFunctionsWorkerDefaults()
389
667
  .ConfigureServices(services =>
390
668
  {
391
669
  services.AddApplicationInsightsTelemetryWorkerService();
670
+ services.ConfigureFunctionsApplicationInsights();
392
671
  })
393
672
  .Build();
394
673
 
@@ -399,7 +678,7 @@ host.Run();
399
678
  export function buildCSharpFunctionsProjectSource(): string {
400
679
  return `<Project Sdk="Microsoft.NET.Sdk">
401
680
  <PropertyGroup>
402
- <TargetFramework>net8.0</TargetFramework>
681
+ <TargetFramework>net10.0</TargetFramework>
403
682
  <AzureFunctionsVersion>v4</AzureFunctionsVersion>
404
683
  <OutputType>Exe</OutputType>
405
684
  <ImplicitUsings>enable</ImplicitUsings>
@@ -413,12 +692,12 @@ export function buildCSharpFunctionsProjectSource(): string {
413
692
  <ItemGroup>
414
693
  <PackageReference Include="Microsoft.Azure.Cosmos" Version="3.47.0" />
415
694
  <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
416
- <PackageReference Include="Azure.Identity" Version="1.13.2" />
417
- <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.23.0" />
418
- <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.2.0" />
419
- <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.18.0" OutputItemType="Analyzer" />
695
+ <PackageReference Include="Azure.Identity" Version="1.17.0" />
696
+ <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" />
697
+ <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
698
+ <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" OutputItemType="Analyzer" />
420
699
  <PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" />
421
- <PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="1.2.0" />
700
+ <PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
422
701
  </ItemGroup>
423
702
  </Project>
424
703
  `;
@@ -440,14 +719,70 @@ export function buildSwallowKitConfigSource(backendLanguage: BackendLanguage): s
440
719
  `;
441
720
  }
442
721
 
443
- export function buildGeneratedProjectDependencies(projectName: string): Record<string, string> {
722
+ export function buildGeneratedProjectDependencies(
723
+ projectName: string,
724
+ pm: PackageManager = 'pnpm',
725
+ ): Record<string, string> {
444
726
  return {
445
727
  '@azure/cosmos': '^4.0.0',
446
728
  'applicationinsights': '^3.3.0',
447
- [`@${projectName}/shared`]: '*',
729
+ 'zod': '^4.0.0',
730
+ [`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:shared',
731
+ };
732
+ }
733
+
734
+ function getSwallowKitPackageMetadata(): { name: string; version: string } {
735
+ const packageJsonPath = path.resolve(__dirname, "..", "..", "..", "package.json");
736
+
737
+ try {
738
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as {
739
+ name?: string;
740
+ version?: string;
741
+ };
742
+
743
+ return {
744
+ name: packageJson.name || "swallowkit",
745
+ version: packageJson.version || "latest",
746
+ };
747
+ } catch {
748
+ // Fall back to the published package name when package metadata is unavailable.
749
+ }
750
+
751
+ return {
752
+ name: "swallowkit",
753
+ version: "latest",
448
754
  };
449
755
  }
450
756
 
757
+ export function buildGeneratedProjectDevDependencies(): Record<string, string> {
758
+ const { name, version } = getSwallowKitPackageMetadata();
759
+ return {
760
+ [name]: version,
761
+ };
762
+ }
763
+
764
+ export function buildSwallowKitMcpProjectConfigSource(pm: PackageManager = 'pnpm'): string {
765
+ const { name } = getSwallowKitPackageMetadata();
766
+ return JSON.stringify(
767
+ {
768
+ mcpServers: {
769
+ swallowkit: {
770
+ command: pm === 'pnpm' ? 'pnpm' : 'npx',
771
+ args: pm === 'pnpm'
772
+ ? ["dlx", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"]
773
+ : ["--yes", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"],
774
+ cwd: ".",
775
+ env: {
776
+ SWALLOWKIT_MCP_VERSION: "latest",
777
+ },
778
+ },
779
+ },
780
+ },
781
+ null,
782
+ 2
783
+ );
784
+ }
785
+
451
786
  async function addSwallowKitFiles(
452
787
  projectDir: string,
453
788
  options: InitOptions,
@@ -464,19 +799,16 @@ async function addSwallowKitFiles(
464
799
  const packageJsonPath = path.join(projectDir, 'package.json');
465
800
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
466
801
 
467
- // zod is in the shared workspace package, not here
802
+ // Root BFF/auth code imports Zod directly; declare it as well as the shared package.
468
803
  packageJson.dependencies = {
469
804
  ...packageJson.dependencies,
470
- ...buildGeneratedProjectDependencies(projectName),
805
+ ...buildGeneratedProjectDependencies(projectName, pm),
806
+ };
807
+ packageJson.devDependencies = {
808
+ ...packageJson.devDependencies,
809
+ ...buildGeneratedProjectDevDependencies(),
471
810
  };
472
811
 
473
- if (backendLanguage !== "typescript") {
474
- packageJson.devDependencies = {
475
- ...packageJson.devDependencies,
476
- '@openapitools/openapi-generator-cli': '^2.21.0',
477
- };
478
- }
479
-
480
812
  packageJson.scripts = {
481
813
  ...packageJson.scripts,
482
814
  'build': getBuildScript(pm),
@@ -484,28 +816,27 @@ async function addSwallowKitFiles(
484
816
  'functions:start': getFunctionsStartScript(pm, backendLanguage),
485
817
  };
486
818
 
487
- if (pm === 'pnpm') {
488
- packageJson.packageManager = 'pnpm@latest';
489
- }
490
-
491
819
  packageJson.engines = {
492
- node: '20.x',
820
+ node: '>=22.14.0',
821
+ ...(pm === 'pnpm' ? { pnpm: '>=11 <13' } : {}),
493
822
  };
823
+ packageJson.devEngines = {
824
+ packageManager: { name: pm, version: pm === 'pnpm' ? '>=11 <13' : '>=10', onFail: 'error' },
825
+ };
826
+ delete packageJson.packageManager;
494
827
 
495
- // Workspace configuration depends on package manager
828
+ // Keep package.json workspaces for npm-based CI/CD even when local development uses pnpm.
496
829
  const workspacePackages = usesNodeFunctionsProject(backendLanguage) ? ['shared', 'functions'] : ['shared'];
497
830
  const wsConfig = getWorkspaceConfig(pm, workspacePackages);
498
- if (wsConfig.type === 'file') {
499
- // pnpm: workspaces are defined in pnpm-workspace.yaml
500
- delete packageJson.workspaces;
501
- } else {
502
- // npm: workspaces are defined in package.json
831
+ packageJson.workspaces = workspacePackages;
832
+
833
+ if (wsConfig.type !== 'file') {
503
834
  packageJson.workspaces = wsConfig.value;
504
835
  }
505
836
 
506
837
  fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
507
838
 
508
- // Create workspace config file if needed (pnpm-workspace.yaml)
839
+ // Create pnpm-workspace.yaml for local pnpm development when needed.
509
840
  if (wsConfig.type === 'file') {
510
841
  fs.writeFileSync(path.join(projectDir, wsConfig.filename), wsConfig.content);
511
842
  }
@@ -565,6 +896,13 @@ function getFunctionsBaseUrl(): string {
565
896
  return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
566
897
  }
567
898
 
899
+ function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string> {
900
+ const key = process.env.BACKEND_FUNCTIONS_KEY;
901
+ if (key) return { 'x-functions-key': key };
902
+ if (functionsBaseUrl.startsWith('http://localhost:') || functionsBaseUrl.startsWith('http://127.0.0.1:')) return {};
903
+ throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
904
+ }
905
+
568
906
  /**
569
907
  * Simple HTTP client for calling backend APIs
570
908
  * Use this to make requests to BFF API routes (which forward to Azure Functions)
@@ -572,10 +910,11 @@ function getFunctionsBaseUrl(): string {
572
910
  async function request<T>(
573
911
  endpoint: string,
574
912
  method: 'GET' | 'POST' | 'PUT' | 'DELETE',
575
- body?: any,
913
+ body?: unknown,
576
914
  queryParams?: Record<string, string>
577
915
  ): Promise<T> {
578
916
  const functionsBaseUrl = getFunctionsBaseUrl();
917
+ const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
579
918
  let url = \`\${functionsBaseUrl}\${endpoint}\`;
580
919
  if (queryParams) {
581
920
  const params = new URLSearchParams(queryParams);
@@ -587,6 +926,7 @@ async function request<T>(
587
926
  method,
588
927
  headers: {
589
928
  'Content-Type': 'application/json',
929
+ ...keyHeaders,
590
930
  },
591
931
  body: body ? JSON.stringify(body) : undefined,
592
932
  });
@@ -642,14 +982,14 @@ export const api = {
642
982
  /**
643
983
  * Make a POST request
644
984
  */
645
- post: <T>(endpoint: string, body?: any): Promise<T> => {
985
+ post: <T>(endpoint: string, body?: unknown): Promise<T> => {
646
986
  return request<T>(endpoint, 'POST', body);
647
987
  },
648
988
 
649
989
  /**
650
990
  * Make a PUT request
651
991
  */
652
- put: <T>(endpoint: string, body?: any): Promise<T> => {
992
+ put: <T>(endpoint: string, body?: unknown): Promise<T> => {
653
993
  return request<T>(endpoint, 'PUT', body);
654
994
  },
655
995
 
@@ -670,6 +1010,7 @@ export const api = {
670
1010
  // 6. Create .env.example
671
1011
  const envExample = `# Azure Functions Backend URL
672
1012
  BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071
1013
+ BACKEND_FUNCTIONS_KEY=
673
1014
 
674
1015
  # Azure Configuration
675
1016
  AZURE_RESOURCE_GROUP=your-resource-group
@@ -709,7 +1050,7 @@ export async function register() {
709
1050
  const originalConsoleError = console.error;
710
1051
  const originalConsoleWarn = console.warn;
711
1052
 
712
- console.log = function(...args: any[]) {
1053
+ console.log = function(...args: unknown[]) {
713
1054
  originalConsoleLog.apply(console, args);
714
1055
  const message = args.map(arg =>
715
1056
  typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
@@ -720,7 +1061,7 @@ export async function register() {
720
1061
  });
721
1062
  };
722
1063
 
723
- console.error = function(...args: any[]) {
1064
+ console.error = function(...args: unknown[]) {
724
1065
  originalConsoleError.apply(console, args);
725
1066
  const message = args.map(arg =>
726
1067
  typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
@@ -731,7 +1072,7 @@ export async function register() {
731
1072
  });
732
1073
  };
733
1074
 
734
- console.warn = function(...args: any[]) {
1075
+ console.warn = function(...args: unknown[]) {
735
1076
  originalConsoleWarn.apply(console, args);
736
1077
  const message = args.map(arg =>
737
1078
  typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
@@ -755,6 +1096,7 @@ export async function register() {
755
1096
  const envLocalContent = [
756
1097
  '# Azure Functions Backend URL (Local)',
757
1098
  'BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071',
1099
+ 'BACKEND_FUNCTIONS_KEY=',
758
1100
  ''
759
1101
  ].join('\n');
760
1102
  fs.writeFileSync(path.join(projectDir, '.env.local'), envLocalContent);
@@ -803,7 +1145,7 @@ export async function register() {
803
1145
  createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage);
804
1146
 
805
1147
  // 19. Create AI agent instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
806
- createAiAgentFiles(projectDir, projectName, backendLanguage);
1148
+ createAiAgentFiles(projectDir, projectName, backendLanguage, pm);
807
1149
  }
808
1150
 
809
1151
  async function createSharedPackage(projectDir: string, projectName: string) {
@@ -815,6 +1157,7 @@ async function createSharedPackage(projectDir: string, projectName: string) {
815
1157
 
816
1158
  // shared/package.json
817
1159
  const sharedPackageJson = {
1160
+ private: true,
818
1161
  name: `@${projectName}/shared`,
819
1162
  version: '1.0.0',
820
1163
  description: 'Shared Zod models — Single Source of Truth for validation schemas',
@@ -837,28 +1180,9 @@ async function createSharedPackage(projectDir: string, projectName: string) {
837
1180
  );
838
1181
 
839
1182
  // shared/tsconfig.json
840
- const sharedTsConfig = {
841
- compilerOptions: {
842
- target: 'ES2020',
843
- module: 'commonjs',
844
- moduleResolution: 'node',
845
- lib: ['ES2020'],
846
- outDir: 'dist',
847
- rootDir: '.',
848
- declaration: true,
849
- declarationMap: true,
850
- sourceMap: true,
851
- strict: true,
852
- esModuleInterop: true,
853
- skipLibCheck: true,
854
- forceConsistentCasingInFileNames: true,
855
- },
856
- include: ['index.ts', 'models/**/*'],
857
- exclude: ['node_modules', 'dist'],
858
- };
859
1183
  fs.writeFileSync(
860
1184
  path.join(sharedDir, 'tsconfig.json'),
861
- JSON.stringify(sharedTsConfig, null, 2)
1185
+ JSON.stringify(buildSharedTsConfig(), null, 2)
862
1186
  );
863
1187
 
864
1188
  // shared/index.ts (empty re-export file, scaffold will add entries)
@@ -952,8 +1276,9 @@ tsconfig.json
952
1276
  !dist/**/*.js
953
1277
  `);
954
1278
  } else if (backendLanguage === 'python') {
955
- gitignoreLines.unshift('.venv', '__pycache__', '.python_packages');
1279
+ gitignoreLines.unshift('.venv', '.codegen-venv', '__pycache__', '.python_packages');
956
1280
  fs.writeFileSync(path.join(functionsDir, '.funcignore'), `.venv
1281
+ .codegen-venv
957
1282
  __pycache__
958
1283
  .pytest_cache
959
1284
  .mypy_cache
@@ -973,8 +1298,8 @@ tests
973
1298
  fs.writeFileSync(path.join(functionsDir, '.gitignore'), `${gitignoreLines.join('\n')}\n`);
974
1299
  }
975
1300
 
976
- function createTypeScriptFunctionsProject(projectDir: string, functionsDir: string, pm: PackageManager): void {
977
- const functionsPackageJson = {
1301
+ export function buildTypeScriptFunctionsPackageJson(projectName: string, pm: PackageManager = 'pnpm') {
1302
+ return {
978
1303
  name: 'functions',
979
1304
  version: '1.0.0',
980
1305
  description: 'Azure Functions backend',
@@ -989,13 +1314,17 @@ function createTypeScriptFunctionsProject(projectDir: string, functionsDir: stri
989
1314
  '@azure/cosmos': '^4.0.0',
990
1315
  '@azure/identity': '^4.0.0',
991
1316
  zod: '>=3.25.0',
992
- [`@${path.basename(projectDir)}/shared`]: '*',
1317
+ [`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:../shared',
993
1318
  },
994
1319
  devDependencies: {
995
1320
  '@types/node': '^20.0.0',
996
1321
  typescript: '^5.0.0',
997
1322
  },
998
1323
  };
1324
+ }
1325
+
1326
+ function createTypeScriptFunctionsProject(projectDir: string, functionsDir: string, pm: PackageManager): void {
1327
+ const functionsPackageJson = buildTypeScriptFunctionsPackageJson(path.basename(projectDir), pm);
999
1328
  fs.writeFileSync(path.join(functionsDir, 'package.json'), JSON.stringify(functionsPackageJson, null, 2));
1000
1329
 
1001
1330
  const sharedPkgName = `@${path.basename(projectDir)}/shared`;
@@ -1068,7 +1397,7 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
1068
1397
 
1069
1398
  app.http('greet', {
1070
1399
  methods: ['GET', 'POST'],
1071
- authLevel: 'anonymous',
1400
+ authLevel: 'function',
1072
1401
  handler: greet
1073
1402
  });
1074
1403
  `);
@@ -1080,6 +1409,11 @@ function createCSharpFunctionsProject(projectDir: string, functionsDir: string):
1080
1409
  const csprojName = `${projectPascal}.Functions.csproj`;
1081
1410
 
1082
1411
  fs.writeFileSync(path.join(functionsDir, csprojName), buildCSharpFunctionsProjectSource());
1412
+ fs.mkdirSync(path.join(functionsDir, '.config'), { recursive: true });
1413
+ fs.writeFileSync(
1414
+ path.join(functionsDir, '.config', 'dotnet-tools.json'),
1415
+ buildCSharpCodegenToolManifestSource()
1416
+ );
1083
1417
 
1084
1418
  fs.writeFileSync(path.join(functionsDir, 'Program.cs'), buildCSharpFunctionsProgramSource());
1085
1419
 
@@ -1095,7 +1429,7 @@ public sealed class GreetFunction
1095
1429
  {
1096
1430
  [Function("greet")]
1097
1431
  public async Task<HttpResponseData> Run(
1098
- [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "greet")] HttpRequestData request)
1432
+ [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "greet")] HttpRequestData request)
1099
1433
  {
1100
1434
  var query = request.Url.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
1101
1435
  var name = "SwallowKit";
@@ -1122,11 +1456,16 @@ public sealed class GreetFunction
1122
1456
 
1123
1457
  function createPythonFunctionsProject(projectDir: string, functionsDir: string): void {
1124
1458
  fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
1459
+ ensureProjectGitignoreEntry(projectDir, '.uv');
1125
1460
 
1126
1461
  fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
1127
1462
  azure-cosmos>=4.9.0
1128
1463
  azure-identity>=1.19.0
1129
1464
  `);
1465
+ fs.writeFileSync(
1466
+ path.join(functionsDir, 'requirements.codegen.txt'),
1467
+ buildPythonCodegenRequirementsSource()
1468
+ );
1130
1469
 
1131
1470
  const blueprintsDir = path.join(functionsDir, 'blueprints');
1132
1471
  fs.mkdirSync(blueprintsDir, { recursive: true });
@@ -1158,13 +1497,29 @@ def greet(req: func.HttpRequest) -> func.HttpResponse:
1158
1497
 
1159
1498
  from blueprints.greet import bp as greet_bp
1160
1499
 
1161
- app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
1500
+ app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
1162
1501
 
1163
1502
  app.register_blueprint(greet_bp)
1164
1503
  # SwallowKit scaffold registrations
1165
1504
  `);
1166
1505
  }
1167
1506
 
1507
+ function ensureProjectGitignoreEntry(projectDir: string, entry: string): void {
1508
+ const gitignorePath = path.join(projectDir, '.gitignore');
1509
+ if (!fs.existsSync(gitignorePath)) {
1510
+ return;
1511
+ }
1512
+
1513
+ const current = fs.readFileSync(gitignorePath, 'utf8');
1514
+ const lines = current.split(/\r?\n/);
1515
+ if (lines.includes(entry)) {
1516
+ return;
1517
+ }
1518
+
1519
+ const normalized = current.endsWith('\n') ? current : `${current}\n`;
1520
+ fs.writeFileSync(gitignorePath, `${normalized}${entry}\n`);
1521
+ }
1522
+
1168
1523
  async function createBffApiRoute(projectDir: string) {
1169
1524
  console.log('📦 Creating BFF API route...\n');
1170
1525
 
@@ -1335,7 +1690,7 @@ export default function Home() {
1335
1690
  Create your first model with Zod and generate CRUD operations automatically.
1336
1691
  </p>
1337
1692
  <code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
1338
- ${pmCmd.dlx} swallowkit scaffold shared/models/your-model.ts
1693
+ ${pmCmd.exec} swallowkit scaffold shared/models/your-model.ts
1339
1694
  </code>
1340
1695
  </div>
1341
1696
  </section>
@@ -1395,17 +1750,18 @@ function createReadme(
1395
1750
  const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
1396
1751
  const schemaBridgeDescription = backendLanguage === 'typescript'
1397
1752
  ? 'Zod (shared between frontend and backend)'
1398
- : `Zod + OpenAPI bridge (Zod in shared/, generated ${backendLanguageLabel} schemas in functions/generated/)`;
1753
+ : `Zod + OpenAPI export (Zod in shared/, native-generated ${backendLanguageLabel} schemas in functions/generated/)`;
1399
1754
  const functionsTree = backendLanguage === 'typescript'
1400
1755
  ? `│ └── src/\n│ └── greet.ts # Sample function`
1401
1756
  : backendLanguage === 'csharp'
1402
- ? `│ ├── Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ # OpenAPI-derived C# models`
1403
- : `│ ├── blueprints/\n│ │ └── greet.py\n│ └── generated/ # OpenAPI-derived Python models`;
1757
+ ? `│ ├── Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ # Native-generated C# schema assets`
1758
+ : `│ ├── blueprints/\n│ │ └── greet.py\n│ └── generated/ # Native-generated Python schema assets`;
1404
1759
  const backendScaffoldNote = backendLanguage === 'typescript'
1405
1760
  ? '- Azure Functions CRUD endpoints'
1406
- : `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI spec + generated ${backendLanguageLabel} schema assets`;
1761
+ : `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI export + native-generated ${backendLanguageLabel} schema assets`;
1762
+ const pythonUvPaths = getProjectLocalUvPaths(projectDir);
1407
1763
  const pythonLocalDevNote = backendLanguage === 'python'
1408
- ? `\n**Python local dev note**: SwallowKit uses \`functions/.venv\` for local Azure Functions development. If \`uv\` is installed, \`swallowkit dev\` uses it to create/manage that virtual environment; otherwise it falls back to the standard \`venv\` + \`pip\` workflow. Keep \`functions/requirements.txt\` as the dependency source of truth for Azure Functions compatibility.\n`
1764
+ ? `\n**Python local dev note**: SwallowKit uses \`uv\` for Python backends and keeps the managed Python runtime under \`${path.relative(projectDir, pythonUvPaths.pythonInstallDir)}\`. Local Azure Functions runs from \`functions/.venv\`, schema generation uses \`functions/.codegen-venv\`, and \`swallowkit dev\` bootstraps a project-local \`uv\` binary automatically when needed. Keep \`functions/requirements.txt\` and \`functions/requirements.codegen.txt\` as the dependency sources of truth.\n`
1409
1765
  : '';
1410
1766
 
1411
1767
  const readme = `# ${projectName}
@@ -1435,7 +1791,7 @@ This project was initialized with the following settings:
1435
1791
 
1436
1792
  Before you begin, ensure you have the following installed:
1437
1793
 
1438
- 1. **Node.js 18+**: [Download](https://nodejs.org/)${pm === 'pnpm' ? `\n2. **pnpm**: \`corepack enable\` or \`npm install -g pnpm\`` : ''}
1794
+ 1. **Node.js 22.14+**: [Download](https://nodejs.org/)${pm === 'pnpm' ? `\n2. **pnpm**: \`corepack enable\` or \`npm install -g pnpm\`` : ''}
1439
1795
  ${pm === 'pnpm' ? '3' : '2'}. **Azure CLI**: Required for provisioning Azure resources
1440
1796
  - Install: \`winget install Microsoft.AzureCLI\` (Windows)
1441
1797
  - Or: [Download](https://aka.ms/installazurecliwindows)
@@ -1443,7 +1799,7 @@ ${pm === 'pnpm' ? '4' : '3'}. **Azure Cosmos DB Emulator**: Required for local d
1443
1799
  - Windows: \`winget install Microsoft.Azure.CosmosEmulator\`
1444
1800
  - Or: [Download](https://aka.ms/cosmosdb-emulator)
1445
1801
  - Docker: \`docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator\`
1446
- ${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools**: Automatically installed with project dependencies
1802
+ ${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools v4**: Install separately for local Azure Functions execution
1447
1803
 
1448
1804
  ## 📁 Project Structure
1449
1805
 
@@ -1469,7 +1825,7 @@ ${functionsTree}
1469
1825
  Define your data model with Zod schema:
1470
1826
 
1471
1827
  \`\`\`bash
1472
- ${pmCmd.dlx} swallowkit create-model <model-name>
1828
+ ${pmCmd.exec} swallowkit create-model <model-name>
1473
1829
  \`\`\`
1474
1830
 
1475
1831
  This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to define your schema.
@@ -1479,7 +1835,7 @@ This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to defin
1479
1835
  Generate complete CRUD operations (Functions, API routes, UI):
1480
1836
 
1481
1837
  \`\`\`bash
1482
- ${pmCmd.dlx} swallowkit scaffold shared/models/<model-name>.ts
1838
+ ${pmCmd.exec} swallowkit scaffold shared/models/<model-name>.ts
1483
1839
  \`\`\`
1484
1840
 
1485
1841
  This generates:
@@ -1491,7 +1847,7 @@ ${backendScaffoldNote}
1491
1847
  ### 3. Start Development Servers
1492
1848
 
1493
1849
  \`\`\`bash
1494
- ${pmCmd.dlx} swallowkit dev
1850
+ ${pmCmd.exec} swallowkit dev
1495
1851
  \`\`\`
1496
1852
 
1497
1853
  This starts:
@@ -1509,7 +1865,7 @@ ${pythonLocalDevNote}
1509
1865
  Create all required Azure resources using Bicep:
1510
1866
 
1511
1867
  \`\`\`bash
1512
- ${pmCmd.dlx} swallowkit provision --resource-group <rg-name>
1868
+ ${pmCmd.exec} swallowkit provision --resource-group <rg-name>
1513
1869
  \`\`\`
1514
1870
 
1515
1871
  This creates:
@@ -1565,10 +1921,10 @@ func azure functionapp publish func-${projectName}
1565
1921
 
1566
1922
  ## 🔧 Available Commands
1567
1923
 
1568
- - \`${pmCmd.dlx} swallowkit create-model <name>\` - Create a new data model
1569
- - \`${pmCmd.dlx} swallowkit scaffold <model-file>\` - Generate CRUD code
1570
- - \`${pmCmd.dlx} swallowkit dev\` - Start development servers
1571
- - \`${pmCmd.dlx} swallowkit provision -g <rg-name>\` - Provision Azure resources
1924
+ - \`${pmCmd.exec} swallowkit create-model <name>\` - Create a new data model
1925
+ - \`${pmCmd.exec} swallowkit scaffold <model-file>\` - Generate CRUD code
1926
+ - \`${pmCmd.exec} swallowkit dev\` - Start development servers
1927
+ - \`${pmCmd.exec} swallowkit provision -g <rg-name>\` - Provision Azure resources
1572
1928
  ${azureConfig.vnetOption !== 'none' ? `
1573
1929
  ## 🔒 Network Security (VNet Configuration)
1574
1930
 
@@ -1617,448 +1973,79 @@ This project was generated by SwallowKit. If you encounter any issues or have su
1617
1973
  console.log('✅ README.md created\n');
1618
1974
  }
1619
1975
 
1620
- function createAiAgentFiles(projectDir: string, projectName: string, backendLanguage: BackendLanguage) {
1976
+ function createAiAgentFiles(
1977
+ projectDir: string,
1978
+ projectName: string,
1979
+ backendLanguage: BackendLanguage,
1980
+ pm: PackageManager
1981
+ ) {
1621
1982
  console.log('🤖 Creating AI agent instruction files...\n');
1622
- const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
1623
- const functionsStructureLine = backendLanguage === 'typescript'
1624
- ? `│ └── src/ # HTTP trigger handlers with Cosmos DB bindings`
1625
- : backendLanguage === 'csharp'
1626
- ? `│ ├── Crud/ # C# HTTP trigger handlers\n│ └── generated/ # OpenAPI-derived C# schema assets`
1627
- : `│ ├── blueprints/ # Python HTTP trigger handlers\n│ └── generated/ # OpenAPI-derived Python schema assets`;
1628
- const backendSchemaNote = backendLanguage === 'typescript'
1629
- ? `- The shared package (\`@${projectName}/shared\`) is consumed by both Next.js and Azure Functions as a workspace dependency.`
1630
- : `- The frontend/BFF source of truth stays in \`shared/models/\` as Zod schemas.\n- \`swallowkit scaffold\` exports OpenAPI into \`functions/openapi/\` and generates ${backendLanguageLabel} schema assets into \`functions/generated/\` for backend use.`;
1631
- const backendRulesNote = backendLanguage === 'typescript'
1632
- ? `- All CRUD operations and business logic live in \`functions/src/\`.\n- Use Azure Functions Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.\n- Use the Cosmos DB SDK client directly **only** for delete operations (bindings do not support delete).\n- Validate all data against Zod schemas before writing to Cosmos DB.\n- The backend auto-generates \`id\` (UUID), \`createdAt\`, and \`updatedAt\` — never trust client-sent values for these fields.`
1633
- : `- All business logic lives in \`functions/\` and the generated handlers perform real Cosmos DB CRUD.\n- Keep Zod schemas in \`shared/models/\` as the source of truth.\n- Regenerate backend contracts with \`swallowkit scaffold shared/models/<name>.ts\` whenever a schema changes.\n- Use the generated OpenAPI-derived models in \`functions/generated/\` to keep backend contracts aligned.\n- The backend should still own \`id\`, \`createdAt\`, and \`updatedAt\`.`;
1983
+ const runCmd = getCommands(pm).exec;
1634
1984
 
1635
- // ── 1. AGENTS.md (Codex / generic agents) ──────────────────────────
1985
+ writeAgentInstructionFiles(projectDir, {
1986
+ projectName,
1987
+ backendLanguage,
1988
+ packageManager: pm,
1989
+ });
1990
+ console.log(' ✅ AGENTS.md (canonical project contract)');
1991
+ console.log(' ✅ CLAUDE.md (Claude Code adapter)');
1992
+ console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot adapter)');
1993
+ console.log(' ✅ .github/instructions/ (path-specific instructions)');
1636
1994
 
1637
- const agentsMd = `# AGENTS.md
1995
+ fs.writeFileSync(
1996
+ path.join(projectDir, '.mcp.json'),
1997
+ buildSwallowKitMcpProjectConfigSource(pm)
1998
+ );
1999
+ console.log(' ✅ .mcp.json (project-scoped MCP bootstrap)');
1638
2000
 
1639
- This project was generated by **SwallowKit**.
1640
- All coding agents **must** follow the architecture and conventions described below.
2001
+ writeWorkflowDocs(projectDir, runCmd);
2002
+ console.log(' ✅ .swallowkit/workflows/ (agent-agnostic autonomous-loop runbooks)');
1641
2003
 
1642
- ## Architecture Overview
2004
+ const skillPaths = writeAgentSkills(projectDir, runCmd);
2005
+ console.log(' ✅ .github/skills/ (Agent Skills — agentskills.io open standard)');
2006
+ for (const skillPath of skillPaths) {
2007
+ console.log(` - ${skillPath}`);
2008
+ }
1643
2009
 
1644
- This is a full-stack application deployed on Azure with a TypeScript frontend/BFF and an Azure Functions backend in ${backendLanguageLabel}.
1645
-
1646
- \`\`\`
1647
- Frontend (React / Next.js App Router)
1648
- ↓ fetch('/api/{model}', ...)
1649
- BFF Layer (Next.js API Routes)
1650
- ↓ HTTP → Azure Functions
1651
- Backend (Azure Functions)
1652
- ↓
1653
- Azure Cosmos DB (Document Database)
1654
- \`\`\`
1655
-
1656
- ### Project Structure
1657
-
1658
- \`\`\`
1659
- ${projectName}/
1660
- ├── app/ # Next.js App Router
1661
- │ ├── api/ # BFF API routes (proxy to Azure Functions)
1662
- │ └── {model}/ # UI pages per model (list, detail, create, edit)
1663
- ├── functions/ # Azure Functions (backend)
1664
- ${functionsStructureLine}
1665
- ├── shared/ # Shared workspace package
1666
- │ ├── models/ # Zod schema definitions (single source of truth)
1667
- │ └── index.ts # Re-exports all models
1668
- ├── lib/
1669
- │ └── api/ # API client utilities (backend.ts, call-function.ts)
1670
- ├── components/ # Shared React components
1671
- ├── infra/ # Bicep infrastructure-as-code files
1672
- │ ├── main.bicep
1673
- │ └── modules/
1674
- └── .github/workflows/ # CI/CD workflows (if configured)
1675
- \`\`\`
1676
-
1677
- ## Critical Design Principles
1678
-
1679
- ### 1. Next.js API Routes Are Strictly a BFF (Backend for Frontend)
1680
-
1681
- - \`app/api/\` routes exist **only** to proxy requests to Azure Functions.
1682
- - **Never** place business logic, database access, or direct Cosmos DB calls in Next.js API routes.
1683
- - The BFF layer may validate input/output with Zod schemas before forwarding to Functions.
1684
- - Use the \`callFunction\` helper (\`lib/api/call-function.ts\`) or the \`api\` client (\`lib/api/backend.ts\`) to call Azure Functions.
1685
-
1686
- Example BFF route pattern:
1687
-
1688
- \`\`\`typescript
1689
- // app/api/{model}/route.ts
1690
- import { callFunction } from '@/lib/api/call-function';
1691
- import { ModelSchema } from '@${projectName}/shared';
1692
- import { z } from 'zod/v4';
1693
-
1694
- export async function GET() {
1695
- return callFunction({
1696
- method: 'GET',
1697
- path: '/api/{model}',
1698
- responseSchema: z.array(ModelSchema),
1699
- });
2010
+ console.log('\n✅ AI agent files created\n');
1700
2011
  }
1701
-
1702
- export async function POST(request: NextRequest) {
1703
- const body = await request.json();
1704
- return callFunction({
1705
- method: 'POST',
1706
- path: '/api/{model}',
1707
- body,
1708
- inputSchema: ModelSchema.omit({ id: true, createdAt: true, updatedAt: true }),
1709
- responseSchema: ModelSchema,
1710
- successStatus: 201,
1711
- });
2012
+ export function buildFunctionsHostKeyBicepExpression(): string {
2013
+ return "listKeys('${resourceId('Microsoft.Web/sites', functionsAppName)}/host/default', '2023-12-01').functionKeys.default";
1712
2014
  }
1713
- \`\`\`
1714
-
1715
- ### 2. Zod Schemas Are the Single Source of Truth
1716
-
1717
- - All data models are defined **once** as Zod schemas in \`shared/models/\`.
1718
- - TypeScript types are derived with \`z.infer<typeof Schema>\` — never define types separately.
1719
- - ${backendSchemaNote}
1720
-
1721
- Model definition pattern:
1722
-
1723
- \`\`\`typescript
1724
- // shared/models/{model}.ts
1725
- import { z } from 'zod/v4';
1726
-
1727
- export const Todo = z.object({
1728
- id: z.string(),
1729
- name: z.string().min(1),
1730
- // ... your fields
1731
- createdAt: z.string().optional(),
1732
- updatedAt: z.string().optional(),
1733
- });
1734
-
1735
- export type Todo = z.infer<typeof Todo>;
1736
- export const displayName = 'Todo';
1737
- \`\`\`
1738
-
1739
- Key rules:
1740
- - Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
1741
- - \`id\`, \`createdAt\`, and \`updatedAt\` are auto-managed by the backend. Mark them as \`optional()\` in the schema.
1742
- - Always re-export models from \`shared/index.ts\`.
1743
-
1744
- ### 3. Azure Functions Own All Business Logic and Data Access
1745
-
1746
- - ${backendRulesNote}
1747
-
1748
- ${backendLanguage === 'typescript' ? 'Azure Functions handler pattern:' : `Generated ${backendLanguageLabel} handlers live under \`functions/\`. Re-run \`swallowkit scaffold shared/models/<name>.ts\` after schema changes to keep generated CRUD handlers and \`functions/generated/\` in sync.`}
1749
-
1750
- ${backendLanguage === 'typescript' ? `\`\`\`typescript
1751
- // functions/src/{model}.ts
1752
- import { app } from '@azure/functions';
1753
- import { ModelSchema } from '@${projectName}/shared';
1754
-
1755
- const containerName = 'Models'; // PascalCase + 's'
1756
-
1757
- app.http('{model}-get-all', {
1758
- methods: ['GET'],
1759
- route: '{model}',
1760
- authLevel: 'anonymous',
1761
- extraInputs: [{ type: 'cosmosDB', name: 'cosmosInput', containerName, ... }],
1762
- handler: async (request, context) => {
1763
- const documents = context.extraInputs.get('cosmosInput');
1764
- const validated = z.array(ModelSchema).parse(documents);
1765
- return { status: 200, jsonBody: validated };
1766
- },
1767
- });
1768
- \`\`\`` : ''}
1769
-
1770
- ## Naming Conventions
1771
-
1772
- | Item | Convention | Example |
1773
- |------|-----------|---------|
1774
- | Model schema file | \`shared/models/{kebab-case}.ts\` | \`shared/models/todo.ts\` |
1775
- | Schema/type name | PascalCase (same name for both) | \`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\` |
1776
- | Functions handler file | backend-language specific under \`functions/\` | \`${backendLanguage === 'typescript' ? 'functions/src/todo.ts' : backendLanguage === 'csharp' ? 'functions/Crud/TodoFunctions.cs' : 'functions/blueprints/todo.py'}\` |
1777
- | Functions handler name | \`{camelCase}-{operation}\` | \`todo-get-all\`, \`todo-create\` |
1778
- | API route path | \`/api/{camelCase}\` | \`/api/todo\`, \`/api/todo/{id}\` |
1779
- | BFF route file | \`app/api/{kebab-case}/route.ts\` | \`app/api/todo/route.ts\` |
1780
- | BFF detail route | \`app/api/{kebab-case}/[id]/route.ts\` | \`app/api/todo/[id]/route.ts\` |
1781
- | UI page directory | \`app/{kebab-case}/\` | \`app/todo/page.tsx\` |
1782
- | React component | PascalCase | \`TodoForm.tsx\` |
1783
- | Cosmos DB container | PascalCase + 's' | \`Todos\` |
1784
- | Cosmos DB partition key | \`/id\` | Always \`/id\` |
1785
- | Bicep container file | \`infra/containers/{kebab-case}-container.bicep\` | \`infra/containers/todo-container.bicep\` |
1786
-
1787
- ## Adding New Models (SwallowKit CLI Skills)
1788
-
1789
- Use the SwallowKit CLI — do **not** manually create model files or CRUD boilerplate.
1790
-
1791
- ### Skill: Create a new data model
1792
-
1793
- \`\`\`bash
1794
- npx swallowkit create-model <name>
1795
- # Multiple models at once:
1796
- npx swallowkit create-model user post comment
1797
- \`\`\`
1798
-
1799
- Creates \`shared/models/<name>.ts\` with a Zod schema template including \`id\`, \`createdAt\`, \`updatedAt\`.
1800
- Edit the generated file to add your domain-specific fields, then run scaffold.
1801
-
1802
- ### Skill: Generate full CRUD from a model
1803
-
1804
- \`\`\`bash
1805
- npx swallowkit scaffold shared/models/<name>.ts
1806
- \`\`\`
1807
-
1808
- Generates:
1809
- - Azure Functions handlers (${backendLanguage === 'typescript' ? '\`functions/src/<name>.ts\`' : '\`functions/\` language-specific CRUD files + \`functions/generated/\` schema assets'})
1810
- - BFF API routes (\`app/api/<name>/route.ts\`, \`app/api/<name>/[id]/route.ts\`)
1811
- - UI pages (\`app/<name>/page.tsx\`, detail, create, edit pages)
1812
- - Cosmos DB Bicep container config (\`infra/containers/<name>-container.bicep\`)
1813
2015
 
1814
- ### Skill: Start development servers
1815
-
1816
- \`\`\`bash
1817
- npx swallowkit dev
1818
- \`\`\`
1819
-
1820
- Runs Next.js (http://localhost:3000) and Azure Functions (http://localhost:7071) concurrently.
1821
- Checks for Cosmos DB Emulator availability.
1822
-
1823
- ### Skill: Provision Azure resources
1824
-
1825
- \`\`\`bash
1826
- npx swallowkit provision --resource-group <name> --location <region>
1827
- \`\`\`
1828
-
1829
- Deploys Bicep infrastructure: Static Web Apps, Functions, Cosmos DB, Storage, Managed Identity.
1830
-
1831
- ### Typical workflow for "add a new feature/model"
1832
-
1833
- 1. \`npx swallowkit create-model <name>\`
1834
- 2. Edit \`shared/models/<name>.ts\` — add fields
1835
- 3. \`npx swallowkit scaffold shared/models/<name>.ts\`
1836
- 4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
1837
-
1838
- ## Do NOT
1839
-
1840
- - **Do not** put business logic or database calls in \`app/api/\` routes. They are BFF only.
1841
- - **Do not** define TypeScript interfaces/types separately from Zod schemas. Always derive types with \`z.infer<>\`.
1842
- - **Do not** manually duplicate model definitions across layers. Use the shared package.
1843
- - **Do not** manually create CRUD boilerplate. Use \`swallowkit scaffold\`.
1844
- - **Do not** hardcode Cosmos DB connection strings. Use Managed Identity (\`CosmosDBConnection__accountEndpoint\`) in production and emulator settings locally.
1845
- - **Do not** change the partition key strategy. All containers use \`/id\` as the partition key.
1846
-
1847
- ## Technology Stack
1848
-
1849
- - **Frontend**: Next.js (App Router), React, TypeScript, Tailwind CSS
1850
- - **BFF**: Next.js API Routes (proxy only)
1851
- - **Backend**: Azure Functions (${backendLanguageLabel})
1852
- - **Database**: Azure Cosmos DB (NoSQL)
1853
- - **Schema**: Zod (shared across all layers via workspace package)
1854
- - **Infrastructure**: Bicep (IaC)
1855
- - **Hosting**: Azure Static Web Apps (frontend) + Azure Functions Flex Consumption (backend)
1856
- - **Auth**: Azure Managed Identity (no connection strings in production)
1857
- - **Monitoring**: Application Insights
1858
- `;
1859
-
1860
- fs.writeFileSync(path.join(projectDir, 'AGENTS.md'), agentsMd);
1861
- console.log(' ✅ AGENTS.md (Codex / generic agents)');
1862
-
1863
- // ── 2. CLAUDE.md (Claude Code) ─────────────────────────────────────
1864
-
1865
- const claudeMd = `# CLAUDE.md
1866
-
1867
- This file is for Claude Code. Read AGENTS.md in the project root for the full architecture, conventions, and rules.
1868
-
1869
- ## Quick Reference
1870
-
1871
- - **Architecture**: Next.js (frontend) → BFF (API routes, proxy only) → Azure Functions (backend) → Cosmos DB
1872
- - **Schema**: Zod schemas in \`shared/models/\` are the single source of truth. Never define types separately.
1873
- - **BFF rule**: \`app/api/\` routes must ONLY proxy to Azure Functions via \`callFunction()\`. No business logic.
1874
- - **Backend language**: ${backendLanguageLabel}
1875
- - **Backend rule**: Regenerate backend contracts with \`swallowkit scaffold\` after schema changes and keep \`functions/generated/\` in sync.
1876
-
1877
- ## SwallowKit CLI Commands
1878
-
1879
- | Task | Command |
1880
- |------|---------|
1881
- | Create model | \`npx swallowkit create-model <name>\` |
1882
- | Generate CRUD | \`npx swallowkit scaffold shared/models/<name>.ts\` |
1883
- | Dev servers | \`npx swallowkit dev\` |
1884
- | Provision Azure | \`npx swallowkit provision --resource-group <rg> --location <region>\` |
1885
-
1886
- ## Workflow: Add a new model
1887
-
1888
- 1. \`npx swallowkit create-model <name>\`
1889
- 2. Edit \`shared/models/<name>.ts\` — add your fields
1890
- 3. \`npx swallowkit scaffold shared/models/<name>.ts\`
1891
- 4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
1892
- `;
1893
-
1894
- fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), claudeMd);
1895
- console.log(' ✅ CLAUDE.md (Claude Code)');
1896
-
1897
- // ── 3. .github/copilot-instructions.md (GitHub Copilot) ────────────
1898
-
1899
- const ghDir = path.join(projectDir, '.github');
1900
- fs.mkdirSync(ghDir, { recursive: true });
1901
-
1902
- const copilotInstructions = `# Copilot Instructions
1903
-
1904
- This project was generated by **SwallowKit**. See \`AGENTS.md\` in the project root for the full specification.
1905
-
1906
- ## Architecture (3-layer)
1907
-
1908
- \`\`\`
1909
- Frontend (Next.js App Router) → BFF (Next.js API Routes) → Backend (Azure Functions) → Cosmos DB
1910
- \`\`\`
1911
-
1912
- ## Key Rules
1913
-
1914
- 1. **BFF is proxy only** — \`app/api/\` routes call Azure Functions via \`callFunction()\`. No business logic, no direct DB access.
1915
- 2. **Zod = single source of truth** — Models live in \`shared/models/\`. Types are derived with \`z.infer<>\`. Never define types separately.
1916
- 3. **Backend owns data** — All CRUD and business logic stay in \`functions/\`, and generated contract assets under \`functions/generated/\` must stay aligned with \`shared/models/\`.
1917
- 4. **Use the CLI** — Run \`npx swallowkit create-model <name>\` then \`npx swallowkit scaffold shared/models/<name>.ts\` to add models. Do not create boilerplate manually.
1918
-
1919
- ## Naming
1920
-
1921
- - Schema/type: PascalCase, same name for both (\`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\`)
1922
- - Files: kebab-case (\`shared/models/todo.ts\`, backend handlers under \`functions/\`)
1923
- - Cosmos DB containers: PascalCase + 's' (\`Todos\`), partition key always \`/id\`
1924
-
1925
- ## Managed Fields
1926
-
1927
- \`id\`, \`createdAt\`, \`updatedAt\` are auto-managed by the backend. Define them as \`optional()\` in schemas. Never trust client-sent values.
1928
- `;
1929
-
1930
- fs.writeFileSync(path.join(ghDir, 'copilot-instructions.md'), copilotInstructions);
1931
- console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot)');
1932
-
1933
- // ── 4. .github/instructions/*.instructions.md (Copilot layer-specific) ──
1934
-
1935
- const instructionsDir = path.join(ghDir, 'instructions');
1936
- fs.mkdirSync(instructionsDir, { recursive: true });
1937
-
1938
- // 4a. shared/models — Zod schema layer
1939
- const sharedModelsInstructions = `---
1940
- applyTo: "shared/models/**"
1941
- ---
1942
-
1943
- # Shared Models — Zod Schema Rules
1944
-
1945
- Files in this directory are the **single source of truth** for data models across the entire application.
1946
-
1947
- ## Rules
1948
-
1949
- - Define Zod schemas using \`zod/v4\` (\`import { z } from 'zod/v4'\`).
1950
- - Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
1951
- \`\`\`typescript
1952
- export const Todo = z.object({ ... });
1953
- export type Todo = z.infer<typeof Todo>;
1954
- \`\`\`
1955
- - Always include \`id: z.string()\`, \`createdAt: z.string().optional()\`, \`updatedAt: z.string().optional()\`. These are managed by the backend.
1956
- - Export a \`displayName\` string constant for UI display.
1957
- - Re-export every model from \`shared/index.ts\`.
1958
- - For relationships, use **nested schemas** (import and embed the related schema), not ID references.
1959
- - After editing a model, run \`npx swallowkit scaffold shared/models/<name>.ts\` to regenerate CRUD code.
1960
- `;
1961
-
1962
- fs.writeFileSync(
1963
- path.join(instructionsDir, 'shared-models.instructions.md'),
1964
- sharedModelsInstructions
1965
- );
1966
-
1967
- // 4b. app/api — BFF layer
1968
- const bffInstructions = `---
1969
- applyTo: "app/api/**"
1970
- ---
1971
-
1972
- # BFF API Routes — Rules
1973
-
1974
- Files in \`app/api/\` are the **BFF (Backend for Frontend)** layer. They exist solely to proxy requests to Azure Functions.
1975
-
1976
- ## Rules
2016
+ export function buildStaticWebAppConfigBicepSource(): string {
2017
+ return `@description('Static Web App name')
2018
+ param staticWebAppName string
1977
2019
 
1978
- - **Never** put business logic, database access, or direct Cosmos DB calls here.
1979
- - Use \`callFunction()\` from \`@/lib/api/call-function\` to forward requests to Azure Functions.
1980
- - You may validate input/output with Zod schemas before forwarding.
1981
- - Import schemas from \`@${projectName}/shared\`.
2020
+ @description('Functions App default hostname for backend API calls')
2021
+ param functionsDefaultHostname string
1982
2022
 
1983
- ## Pattern
2023
+ @description('Functions App name used to resolve its host key after deployment')
2024
+ param functionsAppName string
1984
2025
 
1985
- \`\`\`typescript
1986
- import { callFunction } from '@/lib/api/call-function';
1987
- import { ModelSchema } from '@${projectName}/shared';
1988
- import { z } from 'zod/v4';
2026
+ @description('Application Insights connection string for SWA')
2027
+ param appInsightsConnectionString string
1989
2028
 
1990
- export async function GET() {
1991
- return callFunction({
1992
- method: 'GET',
1993
- path: '/api/{model}',
1994
- responseSchema: z.array(ModelSchema),
1995
- });
2029
+ resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
2030
+ name: staticWebAppName
1996
2031
  }
1997
- \`\`\`
1998
- `;
1999
-
2000
- fs.writeFileSync(
2001
- path.join(instructionsDir, 'bff-routes.instructions.md'),
2002
- bffInstructions
2003
- );
2004
-
2005
- // 4c. functions — Azure Functions backend layer
2006
- const functionsInstructions = `---
2007
- applyTo: "functions/**"
2008
- ---
2009
-
2010
- # Azure Functions — Backend Rules
2011
2032
 
2012
- Files in \`functions/\` contain all business logic and data access for this application.
2013
-
2014
- ## Rules
2015
-
2016
- - Keep backend contracts aligned with \`shared/models/\` by rerunning \`swallowkit scaffold\` after schema changes.
2017
- - For TypeScript backends, use Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.
2018
- - For C#/Python backends, consume the generated OpenAPI-derived assets in \`functions/generated/\`.
2019
- - Auto-generate \`id\` (UUID), \`createdAt\`, and \`updatedAt\` on the backend. Never trust client-sent values.
2020
- - Container names are PascalCase + 's' (e.g., \`Todos\`). Partition key is always \`/id\`.
2021
-
2022
- ## Handler Pattern
2023
-
2024
- \`\`\`typescript
2025
- import { app } from '@azure/functions';
2026
- import { ModelSchema } from '@${projectName}/shared';
2033
+ resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
2034
+ parent: staticWebApp
2035
+ name: 'appsettings'
2036
+ properties: {
2037
+ APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
2038
+ ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
2039
+ BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
2040
+ BACKEND_FUNCTIONS_KEY: ${buildFunctionsHostKeyBicepExpression()}
2041
+ }
2042
+ }
2027
2043
 
2028
- app.http('{model}-get-all', {
2029
- methods: ['GET'],
2030
- route: '{model}',
2031
- authLevel: 'anonymous',
2032
- extraInputs: [cosmosInput],
2033
- handler: async (request, context) => {
2034
- const documents = context.extraInputs.get(cosmosInput);
2035
- const validated = z.array(ModelSchema).parse(documents);
2036
- return { status: 200, jsonBody: validated };
2037
- },
2038
- });
2039
- \`\`\`
2044
+ output configName string = staticWebAppConfig.name
2040
2045
  `;
2041
-
2042
- fs.writeFileSync(
2043
- path.join(instructionsDir, 'azure-functions.instructions.md'),
2044
- functionsInstructions
2045
- );
2046
-
2047
- console.log(' ✅ .github/instructions/ (Copilot layer-specific instructions)');
2048
- console.log(' - shared-models.instructions.md');
2049
- console.log(' - bff-routes.instructions.md');
2050
- console.log(' - azure-functions.instructions.md');
2051
-
2052
- console.log('\n✅ AI agent files created\n');
2053
- console.log(' Supported agents:');
2054
- console.log(' - OpenAI Codex → AGENTS.md');
2055
- console.log(' - Claude Code → CLAUDE.md (+ AGENTS.md)');
2056
- console.log(' - GitHub Copilot → .github/copilot-instructions.md');
2057
- console.log(' - GitHub Copilot (edit) → .github/instructions/*.instructions.md');
2058
- console.log('');
2059
2046
  }
2060
2047
 
2061
- async function createInfrastructure(
2048
+ export async function createInfrastructure(
2062
2049
  projectDir: string,
2063
2050
  projectName: string,
2064
2051
  azureConfig: AzureConfig,
@@ -2072,6 +2059,7 @@ async function createInfrastructure(
2072
2059
 
2073
2060
  const enableVNet = azureConfig.vnetOption !== 'none';
2074
2061
  const functionsRuntime = getFunctionsRuntimeConfig(backendLanguage);
2062
+ const staticWebAppSku = getStaticWebAppSku(azureConfig.swaPlan);
2075
2063
 
2076
2064
  // main.bicep
2077
2065
  const mainBicep = `targetScope = 'resourceGroup'
@@ -2085,6 +2073,13 @@ param location string = resourceGroup().location
2085
2073
  @description('Location for Static Web App (must be explicitly provided)')
2086
2074
  param swaLocation string
2087
2075
 
2076
+ @description('Static Web Apps hosting plan')
2077
+ @allowed([
2078
+ 'Free'
2079
+ 'Standard'
2080
+ ])
2081
+ param staticWebAppSku string = '${staticWebAppSku}'
2082
+
2088
2083
  @description('Cosmos DB mode')
2089
2084
  @allowed(['freetier', 'serverless'])
2090
2085
  param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
@@ -2127,7 +2122,7 @@ module staticWebApp 'modules/staticwebapp.bicep' = {
2127
2122
  params: {
2128
2123
  name: 'swa-\${projectName}'
2129
2124
  location: swaLocation
2130
- sku: 'Standard'
2125
+ sku: staticWebAppSku
2131
2126
  appInsightsConnectionString: appInsightsSwa.outputs.connectionString
2132
2127
  }
2133
2128
  }
@@ -2168,16 +2163,11 @@ module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enabl
2168
2163
  params: {
2169
2164
  name: 'pe-cosmos-\${projectName}'
2170
2165
  location: location
2171
- cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.id : cosmosDbServerless.outputs.id
2172
- cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
2173
- subnetId: vnet.outputs.privateEndpointSubnetId
2174
- vnetId: vnet.outputs.id
2166
+ cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.id : cosmosDbServerless!.outputs.id
2167
+ cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.accountName : cosmosDbServerless!.outputs.accountName
2168
+ subnetId: vnet!.outputs.privateEndpointSubnetId
2169
+ vnetId: vnet!.outputs.id
2175
2170
  }
2176
- dependsOn: [
2177
- cosmosDbFreeTier
2178
- cosmosDbServerless
2179
- vnet
2180
- ]
2181
2171
  }
2182
2172
 
2183
2173
  // Azure Functions (Flex Consumption) - Deploy AFTER Cosmos DB
@@ -2189,16 +2179,14 @@ module functionsFlex 'modules/functions-flex.bicep' = {
2189
2179
  storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
2190
2180
  appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
2191
2181
  swaDefaultHostname: staticWebApp.outputs.defaultHostname
2192
- cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
2193
- cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
2182
+ cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.endpoint : cosmosDbServerless!.outputs.endpoint
2183
+ cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.databaseName : cosmosDbServerless!.outputs.databaseName
2194
2184
  functionsRuntimeName: '${functionsRuntime.name}'
2195
2185
  functionsRuntimeVersion: '${functionsRuntime.version}'
2196
2186
  enableVNet: enableVNet
2197
- vnetSubnetId: enableVNet ? vnet.outputs.functionsSubnetId : ''
2187
+ vnetSubnetId: enableVNet ? vnet!.outputs.functionsSubnetId : ''
2198
2188
  }
2199
2189
  dependsOn: [
2200
- cosmosDbFreeTier
2201
- cosmosDbServerless
2202
2190
  cosmosPrivateEndpoint
2203
2191
  ]
2204
2192
  }
@@ -2207,23 +2195,17 @@ module functionsFlex 'modules/functions-flex.bicep' = {
2207
2195
  module cosmosDbRoleAssignmentFreeTier 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'freetier') {
2208
2196
  name: 'cosmosDbRoleAssignment'
2209
2197
  params: {
2210
- cosmosAccountName: cosmosDbFreeTier.outputs.accountName
2198
+ cosmosAccountName: cosmosDbFreeTier!.outputs.accountName
2211
2199
  functionsPrincipalId: functionsFlex.outputs.principalId
2212
2200
  }
2213
- dependsOn: [
2214
- functionsFlex
2215
- ]
2216
2201
  }
2217
2202
 
2218
2203
  module cosmosDbRoleAssignmentServerless 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'serverless') {
2219
2204
  name: 'cosmosDbRoleAssignment'
2220
2205
  params: {
2221
- cosmosAccountName: cosmosDbServerless.outputs.accountName
2206
+ cosmosAccountName: cosmosDbServerless!.outputs.accountName
2222
2207
  functionsPrincipalId: functionsFlex.outputs.principalId
2223
2208
  }
2224
- dependsOn: [
2225
- functionsFlex
2226
- ]
2227
2209
  }
2228
2210
 
2229
2211
  // Update SWA config with Functions hostname (after Functions deployment)
@@ -2232,20 +2214,18 @@ module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
2232
2214
  params: {
2233
2215
  staticWebAppName: staticWebApp.outputs.name
2234
2216
  functionsDefaultHostname: functionsFlex.outputs.defaultHostname
2217
+ functionsAppName: functionsFlex.outputs.name
2235
2218
  appInsightsConnectionString: appInsightsSwa.outputs.connectionString
2236
2219
  }
2237
- dependsOn: [
2238
- functionsFlex
2239
- ]
2240
2220
  }
2241
2221
 
2242
2222
  output staticWebAppName string = staticWebApp.outputs.name
2243
2223
  output staticWebAppUrl string = staticWebApp.outputs.defaultHostname
2244
2224
  output functionsAppName string = functionsFlex.outputs.name
2245
2225
  output functionsAppUrl string = functionsFlex.outputs.defaultHostname
2246
- output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
2247
- output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
2248
- output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
2226
+ output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.accountName : cosmosDbServerless!.outputs.accountName
2227
+ output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.endpoint : cosmosDbServerless!.outputs.endpoint
2228
+ output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.databaseName : cosmosDbServerless!.outputs.databaseName
2249
2229
  output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
2250
2230
  output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
2251
2231
  output appInsightsSwaName string = appInsightsSwa.outputs.name
@@ -2253,7 +2233,7 @@ output appInsightsSwaConnectionString string = appInsightsSwa.outputs.connection
2253
2233
  output appInsightsFunctionsName string = appInsightsFunctions.outputs.name
2254
2234
  output appInsightsFunctionsConnectionString string = appInsightsFunctions.outputs.connectionString
2255
2235
  output vnetEnabled bool = enableVNet
2256
- output vnetName string = enableVNet ? vnet.outputs.name : ''
2236
+ output vnetName string = enableVNet ? vnet!.outputs.name : ''
2257
2237
  `;
2258
2238
 
2259
2239
  fs.writeFileSync(path.join(infraDir, 'main.bicep'), mainBicep);
@@ -2269,6 +2249,9 @@ output vnetName string = enableVNet ? vnet.outputs.name : ''
2269
2249
  "cosmosDbMode": {
2270
2250
  "value": "${azureConfig.cosmosDbMode}"
2271
2251
  },
2252
+ "staticWebAppSku": {
2253
+ "value": "${staticWebAppSku}"
2254
+ },
2272
2255
  "enableVNet": {
2273
2256
  "value": ${enableVNet}
2274
2257
  }
@@ -2325,31 +2308,7 @@ output defaultHostname string = staticWebApp.properties.defaultHostname
2325
2308
  fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
2326
2309
 
2327
2310
  // modules/staticwebapp-config.bicep (for updating config after Functions deployment)
2328
- const staticWebAppConfigBicep = `@description('Static Web App name')
2329
- param staticWebAppName string
2330
-
2331
- @description('Functions App default hostname for backend API calls')
2332
- param functionsDefaultHostname string
2333
-
2334
- @description('Application Insights connection string for SWA')
2335
- param appInsightsConnectionString string
2336
-
2337
- resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
2338
- name: staticWebAppName
2339
- }
2340
-
2341
- resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
2342
- parent: staticWebApp
2343
- name: 'appsettings'
2344
- properties: {
2345
- APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
2346
- ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
2347
- BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
2348
- }
2349
- }
2350
-
2351
- output configName string = staticWebAppConfig.name
2352
- `;
2311
+ const staticWebAppConfigBicep = buildStaticWebAppConfigBicepSource();
2353
2312
  fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
2354
2313
 
2355
2314
  // modules/loganalytics.bicep (Shared Log Analytics Workspace)
@@ -2572,63 +2531,7 @@ output principalId string = functionApp.identity.principalId
2572
2531
  fs.writeFileSync(path.join(modulesDir, 'functions-flex.bicep'), functionsFlexBicep);
2573
2532
 
2574
2533
  // modules/cosmosdb-freetier.bicep (Free Tier)
2575
- const cosmosDbFreeTierBicep = `@description('Cosmos DB account name')
2576
- param accountName string
2577
-
2578
- @description('Database name')
2579
- param databaseName string
2580
-
2581
- @description('Location for Cosmos DB')
2582
- param location string
2583
-
2584
- @description('Public network access')
2585
- @allowed(['Enabled', 'Disabled'])
2586
- param publicNetworkAccess string = 'Enabled'
2587
-
2588
- // Cosmos DB Account (Free Tier)
2589
- resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
2590
- name: accountName
2591
- location: location
2592
- kind: 'GlobalDocumentDB'
2593
- properties: {
2594
- databaseAccountOfferType: 'Standard'
2595
- enableAutomaticFailover: false
2596
- enableFreeTier: true
2597
- publicNetworkAccess: publicNetworkAccess
2598
- disableLocalAuth: true
2599
- consistencyPolicy: {
2600
- defaultConsistencyLevel: 'Session'
2601
- }
2602
- locations: [
2603
- {
2604
- locationName: location
2605
- failoverPriority: 0
2606
- isZoneRedundant: false
2607
- }
2608
- ]
2609
- disableKeyBasedMetadataWriteAccess: true
2610
- }
2611
- }
2612
-
2613
- // Cosmos DB Database
2614
- resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
2615
- parent: cosmosAccount
2616
- name: databaseName
2617
- properties: {
2618
- resource: {
2619
- id: databaseName
2620
- }
2621
- options: {
2622
- throughput: 1000
2623
- }
2624
- }
2625
- }
2626
-
2627
- output id string = cosmosAccount.id
2628
- output accountName string = cosmosAccount.name
2629
- output endpoint string = cosmosAccount.properties.documentEndpoint
2630
- output databaseName string = database.name
2631
- `;
2534
+ const cosmosDbFreeTierBicep = buildCosmosDbFreeTierBicepSource();
2632
2535
  fs.writeFileSync(path.join(modulesDir, 'cosmosdb-freetier.bicep'), cosmosDbFreeTierBicep);
2633
2536
 
2634
2537
  // modules/cosmosdb-serverless.bicep (Serverless)
@@ -2721,9 +2624,10 @@ output roleAssignmentId string = roleAssignment.id
2721
2624
  `;
2722
2625
  fs.writeFileSync(path.join(modulesDir, 'cosmosdb-role-assignment.bicep'), cosmosDbRoleAssignmentBicep);
2723
2626
 
2724
- // VNet modules (only generate if VNet is enabled)
2725
- if (enableVNet) {
2726
- // modules/vnet.bicep
2627
+ // Bicep resolves module files at compile time even when their deployment condition is false,
2628
+ // so these definitions must always be generated. main.bicep still controls deployment with
2629
+ // `if (enableVNet)`.
2630
+ // modules/vnet.bicep
2727
2631
  const vnetBicep = `@description('VNet name')
2728
2632
  param name string
2729
2633
 
@@ -2853,26 +2757,30 @@ output privateDnsZoneId string = privateDnsZone.id
2853
2757
  `;
2854
2758
  fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
2855
2759
 
2856
- console.log('✅ VNet modules created\n');
2857
- }
2760
+ console.log('✅ VNet module definitions created\n');
2858
2761
 
2859
2762
  console.log('✅ Infrastructure files created\n');
2860
2763
  }
2861
2764
 
2862
- function getGitHubFunctionsWorkflow(pm: PackageManager, backendLanguage: BackendLanguage): string {
2765
+ export function getGitHubFunctionsWorkflow(pm: PackageManager, backendLanguage: BackendLanguage, projectPm: PackageManager = pm): string {
2863
2766
  const pmCmd = getCommands(pm);
2864
2767
  const pnpmSetupStep = getCiSetupStep(pm);
2768
+ const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
2865
2769
 
2866
2770
  const commonSetup = ` - uses: actions/checkout@v4
2867
2771
 
2772
+ ${pnpmSetupStep ? `${pnpmSetupStep}\n` : ''}
2868
2773
  - name: Setup Node.js
2869
2774
  uses: actions/setup-node@v4
2870
2775
  with:
2871
2776
  node-version: '22'
2872
- ${pnpmSetupStep ? `\n${pnpmSetupStep}\n` : ''}
2777
+ ${npmWorkspaceNormalizationStep ? '' : ` cache: ${pm}
2778
+ cache-dependency-path: ${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}
2779
+ `}
2780
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
2873
2781
  - name: Install dependencies
2874
2782
  run: |
2875
- ${pmCmd.ci}
2783
+ ${getCiInstallCommand(pm, projectPm)}
2876
2784
 
2877
2785
  - name: Build shared package
2878
2786
  run: |
@@ -2958,7 +2866,7 @@ jobs:
2958
2866
  ${commonSetup} - name: Setup .NET
2959
2867
  uses: actions/setup-dotnet@v4
2960
2868
  with:
2961
- dotnet-version: '8.0.x'
2869
+ dotnet-version: '10.0.x'
2962
2870
 
2963
2871
  - name: Publish Functions
2964
2872
  run: |
@@ -3019,16 +2927,18 @@ ${commonSetup} - name: Setup Python
3019
2927
  `;
3020
2928
  }
3021
2929
 
3022
- function getAzureFunctionsPipeline(pm: PackageManager, backendLanguage: BackendLanguage): string {
2930
+ export function getAzureFunctionsPipeline(pm: PackageManager, backendLanguage: BackendLanguage, projectPm: PackageManager = pm): string {
3023
2931
  const pmCmd = getCommands(pm);
3024
2932
  const azPipelinesSetup = getAzurePipelinesSetup(pm);
2933
+ const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
3025
2934
  const commonSetup = ` - task: NodeTool@0
3026
2935
  inputs:
3027
2936
  versionSpec: '22.x'
3028
2937
  displayName: 'Install Node.js'
3029
2938
  ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
2939
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
3030
2940
  - script: |
3031
- ${pmCmd.ci}
2941
+ ${getCiInstallCommand(pm, projectPm)}
3032
2942
  displayName: 'Install workspace dependencies'
3033
2943
 
3034
2944
  - script: |
@@ -3134,7 +3044,7 @@ variables:
3134
3044
  steps:
3135
3045
  ${commonSetup} - task: UseDotNet@2
3136
3046
  inputs:
3137
- version: '8.0.x'
3047
+ version: '10.0.x'
3138
3048
  displayName: 'Install .NET SDK'
3139
3049
 
3140
3050
  - script: |
@@ -3216,6 +3126,195 @@ ${commonSetup} - task: UsePythonVersion@0
3216
3126
  `;
3217
3127
  }
3218
3128
 
3129
+ // setup-node selects the runner runtime only; Oryx reads the app manifest separately.
3130
+ export function getSwaNodeVersionCommand(): string {
3131
+ return `node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.engines={...p.engines,node:'>=22.14.0 <23'};fs.writeFileSync('package.json',JSON.stringify(p,null,2)+String.fromCharCode(10));"`;
3132
+ }
3133
+
3134
+ export function buildAzureSwaPipeline(pm: PackageManager, projectPm: PackageManager = pm): string {
3135
+ // SWA/Oryx also invokes npm internally; normalize only the disposable CI checkout.
3136
+ pm = 'npm';
3137
+ const pmCmd = getCommands(pm);
3138
+ const azPipelinesSetup = getAzurePipelinesSetup(pm);
3139
+ const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
3140
+
3141
+ return `trigger:
3142
+ branches:
3143
+ include:
3144
+ - main
3145
+ paths:
3146
+ include:
3147
+ - app/**
3148
+ - components/**
3149
+ - lib/**
3150
+ - shared/**
3151
+ - public/**
3152
+ - package.json
3153
+ - next.config.js
3154
+
3155
+ pr:
3156
+ branches:
3157
+ include:
3158
+ - main
3159
+ paths:
3160
+ include:
3161
+ - app/**
3162
+ - components/**
3163
+ - lib/**
3164
+ - shared/**
3165
+ - public/**
3166
+ - package.json
3167
+ - next.config.js
3168
+
3169
+ pool:
3170
+ vmImage: 'ubuntu-latest'
3171
+
3172
+ variables:
3173
+ - group: azure-deployment
3174
+
3175
+ steps:
3176
+ - task: NodeTool@0
3177
+ inputs:
3178
+ versionSpec: '22.x'
3179
+ displayName: 'Install Node.js'
3180
+ ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
3181
+ - script: |
3182
+ ${getSwaNodeVersionCommand()}
3183
+ displayName: 'Select Node.js 22 for SWA'
3184
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
3185
+ - script: |
3186
+ ${getCiInstallCommand(pm, projectPm)}
3187
+ displayName: 'Install dependencies'
3188
+
3189
+ - script: |
3190
+ ${pmCmd.run} build
3191
+ env:
3192
+ NODE_ENV: production
3193
+ displayName: 'Build Next.js app'
3194
+
3195
+ - task: AzureStaticWebApp@0
3196
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
3197
+ inputs:
3198
+ app_location: '.'
3199
+ output_location: '.next/standalone'
3200
+ skip_app_build: true
3201
+ azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
3202
+ displayName: 'Deploy to Azure Static Web Apps'
3203
+ `;
3204
+ }
3205
+
3206
+ function getAzurePipelinesNpmWorkspaceNormalizationStep(pm: PackageManager, projectPm: PackageManager): string {
3207
+ if (pm !== 'npm' || projectPm !== 'pnpm') {
3208
+ return '';
3209
+ }
3210
+
3211
+ return ` - script: |
3212
+ node <<'NODE'
3213
+ const fs = require('fs');
3214
+
3215
+ function readJson(path) {
3216
+ return JSON.parse(fs.readFileSync(path, 'utf8'));
3217
+ }
3218
+
3219
+ function writeJson(path, value) {
3220
+ fs.writeFileSync(path, JSON.stringify(value, null, 2) + String.fromCharCode(10));
3221
+ }
3222
+
3223
+ const sharedPackagePath = 'shared/package.json';
3224
+ const sharedPackageName = fs.existsSync(sharedPackagePath) ? readJson(sharedPackagePath).name : undefined;
3225
+
3226
+ function replaceSharedWorkspaceDep(pkg, replacement) {
3227
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
3228
+ if (!pkg[section]) continue;
3229
+
3230
+ for (const [name, version] of Object.entries(pkg[section])) {
3231
+ if (typeof version === 'string' && version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
3232
+ pkg[section][name] = replacement;
3233
+ }
3234
+ }
3235
+ }
3236
+ }
3237
+
3238
+ const root = readJson('package.json');
3239
+ root.workspaces = ['shared', 'functions'].filter(dir => fs.existsSync(dir + '/package.json'));
3240
+ delete root.packageManager;
3241
+ root.devEngines = { ...root.devEngines, packageManager: { name: 'npm', version: '>=10', onFail: 'error' } };
3242
+ replaceSharedWorkspaceDep(root, 'file:shared');
3243
+ root.scripts = root.scripts || {};
3244
+ root.scripts.build = ${JSON.stringify(getBuildScript('npm'))};
3245
+ writeJson('package.json', root);
3246
+
3247
+ if (fs.existsSync('functions/package.json')) {
3248
+ const functionsPkg = readJson('functions/package.json');
3249
+ replaceSharedWorkspaceDep(functionsPkg, 'file:../shared');
3250
+ if (functionsPkg.scripts && functionsPkg.scripts.prestart) {
3251
+ functionsPkg.scripts.prestart = 'npm run build';
3252
+ }
3253
+ writeJson('functions/package.json', functionsPkg);
3254
+ }
3255
+ NODE
3256
+
3257
+ npm install --package-lock-only --ignore-scripts
3258
+ displayName: 'Normalize pnpm workspace for npm CI'`;
3259
+ }
3260
+
3261
+ function getGitHubNpmWorkspaceNormalizationStep(pm: PackageManager, projectPm: PackageManager): string {
3262
+ if (pm !== 'npm' || projectPm !== 'pnpm') {
3263
+ return '';
3264
+ }
3265
+
3266
+ return ` - name: Normalize pnpm workspace for npm CI
3267
+ shell: bash
3268
+ run: |
3269
+ node <<'NODE'
3270
+ const fs = require('fs');
3271
+
3272
+ function readJson(path) {
3273
+ return JSON.parse(fs.readFileSync(path, 'utf8'));
3274
+ }
3275
+
3276
+ function writeJson(path, value) {
3277
+ fs.writeFileSync(path, JSON.stringify(value, null, 2) + String.fromCharCode(10));
3278
+ }
3279
+
3280
+ const sharedPackagePath = 'shared/package.json';
3281
+ const sharedPackageName = fs.existsSync(sharedPackagePath) ? readJson(sharedPackagePath).name : undefined;
3282
+
3283
+ function replaceSharedWorkspaceDep(pkg, replacement) {
3284
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
3285
+ if (!pkg[section]) continue;
3286
+
3287
+ for (const [name, version] of Object.entries(pkg[section])) {
3288
+ if (typeof version === 'string' && version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
3289
+ pkg[section][name] = replacement;
3290
+ }
3291
+ }
3292
+ }
3293
+ }
3294
+
3295
+ const root = readJson('package.json');
3296
+ root.workspaces = ['shared', 'functions'].filter(dir => fs.existsSync(dir + '/package.json'));
3297
+ delete root.packageManager;
3298
+ root.devEngines = { ...root.devEngines, packageManager: { name: 'npm', version: '>=10', onFail: 'error' } };
3299
+ replaceSharedWorkspaceDep(root, 'file:shared');
3300
+ root.scripts = root.scripts || {};
3301
+ root.scripts.build = ${JSON.stringify(getBuildScript('npm'))};
3302
+ writeJson('package.json', root);
3303
+
3304
+ if (fs.existsSync('functions/package.json')) {
3305
+ const functionsPkg = readJson('functions/package.json');
3306
+ replaceSharedWorkspaceDep(functionsPkg, 'file:../shared');
3307
+ if (functionsPkg.scripts && functionsPkg.scripts.prestart) {
3308
+ functionsPkg.scripts.prestart = 'npm run build';
3309
+ }
3310
+ writeJson('functions/package.json', functionsPkg);
3311
+ }
3312
+ NODE
3313
+
3314
+ npm install --package-lock-only --ignore-scripts
3315
+ `;
3316
+ }
3317
+
3219
3318
  async function createGitHubActionsWorkflows(
3220
3319
  projectDir: string,
3221
3320
  azureConfig: AzureConfig,
@@ -3223,13 +3322,27 @@ async function createGitHubActionsWorkflows(
3223
3322
  backendLanguage: BackendLanguage
3224
3323
  ) {
3225
3324
  console.log('📦 Creating GitHub Actions workflows...\n');
3226
-
3227
- const pmCmd = getCommands(pm);
3228
3325
  const workflowsDir = path.join(projectDir, '.github', 'workflows');
3229
3326
  fs.mkdirSync(workflowsDir, { recursive: true });
3230
3327
 
3231
- // deploy-swa.yml
3232
- const swaWorkflow = `name: Deploy Static Web App
3328
+ const actionsPm: PackageManager = pm;
3329
+
3330
+ const swaWorkflow = buildGitHubSwaWorkflow(actionsPm, pm);
3331
+ fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
3332
+
3333
+ // deploy-functions.yml
3334
+ const functionsWorkflow = getGitHubFunctionsWorkflow(actionsPm, backendLanguage, pm);
3335
+ fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
3336
+
3337
+ console.log('✅ GitHub Actions workflows created\n');
3338
+ }
3339
+
3340
+ export function buildGitHubSwaWorkflow(pm: PackageManager, projectPm: PackageManager = pm): string {
3341
+ // SWA/Oryx also invokes npm internally; normalize only the disposable CI checkout.
3342
+ pm = 'npm';
3343
+ const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
3344
+
3345
+ return `name: Deploy Static Web App
3233
3346
 
3234
3347
  on:
3235
3348
  push:
@@ -3242,6 +3355,7 @@ on:
3242
3355
  - 'shared/**'
3243
3356
  - 'public/**'
3244
3357
  - 'package.json'
3358
+ - '${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
3245
3359
  - 'next.config.js'
3246
3360
  - 'next.config.ts'
3247
3361
  workflow_dispatch:
@@ -3255,6 +3369,7 @@ on:
3255
3369
  - 'shared/**'
3256
3370
  - 'public/**'
3257
3371
  - 'package.json'
3372
+ - '${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
3258
3373
  - 'next.config.js'
3259
3374
  - 'next.config.ts'
3260
3375
 
@@ -3267,6 +3382,22 @@ jobs:
3267
3382
  - uses: actions/checkout@v4
3268
3383
  with:
3269
3384
  submodules: true
3385
+
3386
+ ${getCiSetupStep(pm) ? `${getCiSetupStep(pm)}\n` : ''}
3387
+ - name: Setup Node.js
3388
+ uses: actions/setup-node@v4
3389
+ with:
3390
+ node-version: '22'
3391
+ ${npmWorkspaceNormalizationStep ? '' : ` cache: ${pm}
3392
+ cache-dependency-path: ${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}
3393
+ `}
3394
+ - name: Select Node.js 22 for SWA
3395
+ run: |
3396
+ ${getSwaNodeVersionCommand()}
3397
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
3398
+ - name: Install and build app
3399
+ run: |
3400
+ ${getSwaAppBuildCommand(pm, projectPm)}
3270
3401
 
3271
3402
  - name: Deploy to Azure Static Web Apps
3272
3403
  if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
@@ -3278,93 +3409,27 @@ jobs:
3278
3409
  app_location: '/'
3279
3410
  api_location: ''
3280
3411
  output_location: ''
3412
+ app_build_command: 'npm run build'
3281
3413
  env:
3282
3414
  NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
3283
3415
  `;
3284
- fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
3285
-
3286
- // deploy-functions.yml
3287
- const functionsWorkflow = getGitHubFunctionsWorkflow(pm, backendLanguage);
3288
- fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
3289
-
3290
- console.log('✅ GitHub Actions workflows created\n');
3291
3416
  }
3292
3417
 
3293
3418
  async function createAzurePipelines(projectDir: string, pm: PackageManager, backendLanguage: BackendLanguage) {
3294
3419
  console.log('📦 Creating Azure Pipelines...\n');
3295
-
3296
- const pmCmd = getCommands(pm);
3297
- const azPipelinesSetup = getAzurePipelinesSetup(pm);
3420
+
3421
+ const pipelinesPm: PackageManager = pm;
3298
3422
  const pipelinesDir = path.join(projectDir, 'pipelines');
3299
3423
  fs.mkdirSync(pipelinesDir, { recursive: true });
3300
3424
 
3301
3425
  // swa.yml
3302
- const swaPipeline = `trigger:
3303
- branches:
3304
- include:
3305
- - main
3306
- paths:
3307
- include:
3308
- - app/**
3309
- - components/**
3310
- - lib/**
3311
- - shared/**
3312
- - public/**
3313
- - package.json
3314
- - next.config.js
3315
-
3316
- pr:
3317
- branches:
3318
- include:
3319
- - main
3320
- paths:
3321
- include:
3322
- - app/**
3323
- - components/**
3324
- - lib/**
3325
- - shared/**
3326
- - public/**
3327
- - package.json
3328
- - next.config.js
3329
-
3330
- pool:
3331
- vmImage: 'ubuntu-latest'
3332
-
3333
- variables:
3334
- - group: azure-deployment
3335
-
3336
- steps:
3337
- - task: NodeTool@0
3338
- inputs:
3339
- versionSpec: '22.x'
3340
- displayName: 'Install Node.js'
3341
- ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
3342
- - script: |
3343
- ${pmCmd.ci}
3344
- displayName: 'Install dependencies'
3345
-
3346
- - script: |
3347
- ${pmCmd.run} build
3348
- env:
3349
- NODE_ENV: production
3350
- displayName: 'Build Next.js app'
3351
-
3352
- - task: AzureStaticWebApp@0
3353
- condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
3354
- inputs:
3355
- app_location: '.'
3356
- output_location: '.next/standalone'
3357
- skip_app_build: true
3358
- azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
3359
- displayName: 'Deploy to Azure Static Web Apps'
3360
- `;
3426
+ const swaPipeline = buildAzureSwaPipeline(pipelinesPm, pm);
3361
3427
  fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
3362
3428
 
3363
3429
  // functions.yml
3364
- const functionsPipeline = getAzureFunctionsPipeline(pm, backendLanguage);
3430
+ const functionsPipeline = getAzureFunctionsPipeline(pipelinesPm, backendLanguage, pm);
3365
3431
  fs.writeFileSync(path.join(pipelinesDir, 'functions.yml'), functionsPipeline);
3366
3432
 
3367
3433
  console.log('✅ Azure Pipelines created\n');
3368
3434
  }
3369
3435
 
3370
-