swallowkit 1.0.0-beta.8 → 1.0.0

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 +70 -0
  32. package/dist/cli/commands/init.d.ts.map +1 -1
  33. package/dist/cli/commands/init.js +797 -768
  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 +188 -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 +513 -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 +386 -1
  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 +880 -808
  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 +202 -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,17 +692,97 @@ 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
  `;
425
704
  }
426
705
 
706
+ export function buildSwallowKitConfigSource(backendLanguage: BackendLanguage): string {
707
+ return `module.exports = {
708
+ backend: {
709
+ language: '${backendLanguage}',
710
+ },
711
+ functions: {
712
+ baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
713
+ },
714
+ deployment: {
715
+ resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
716
+ swaName: process.env.AZURE_SWA_NAME || '',
717
+ },
718
+ }
719
+ `;
720
+ }
721
+
722
+ export function buildGeneratedProjectDependencies(
723
+ projectName: string,
724
+ pm: PackageManager = 'pnpm',
725
+ ): Record<string, string> {
726
+ return {
727
+ '@azure/cosmos': '^4.0.0',
728
+ 'applicationinsights': '^3.3.0',
729
+ 'zod': '^4.0.0',
730
+ [`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:shared',
731
+ };
732
+ }
733
+
734
+ function getSwallowKitPackageMetadata(): { name: string; version: string } {
735
+ const packageJsonPath = path.resolve(__dirname, "..", "..", "..", "package.json");
736
+
737
+ try {
738
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as {
739
+ name?: string;
740
+ version?: string;
741
+ };
742
+
743
+ return {
744
+ name: packageJson.name || "swallowkit",
745
+ version: packageJson.version || "latest",
746
+ };
747
+ } catch {
748
+ // Fall back to the published package name when package metadata is unavailable.
749
+ }
750
+
751
+ return {
752
+ name: "swallowkit",
753
+ version: "latest",
754
+ };
755
+ }
756
+
757
+ export function buildGeneratedProjectDevDependencies(): Record<string, string> {
758
+ const { name, version } = getSwallowKitPackageMetadata();
759
+ return {
760
+ [name]: version,
761
+ };
762
+ }
763
+
764
+ export function buildSwallowKitMcpProjectConfigSource(pm: PackageManager = 'pnpm'): string {
765
+ const { name } = getSwallowKitPackageMetadata();
766
+ return JSON.stringify(
767
+ {
768
+ mcpServers: {
769
+ swallowkit: {
770
+ command: pm === 'pnpm' ? 'pnpm' : 'npx',
771
+ args: pm === 'pnpm'
772
+ ? ["dlx", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"]
773
+ : ["--yes", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"],
774
+ cwd: ".",
775
+ env: {
776
+ SWALLOWKIT_MCP_VERSION: "latest",
777
+ },
778
+ },
779
+ },
780
+ },
781
+ null,
782
+ 2
783
+ );
784
+ }
785
+
427
786
  async function addSwallowKitFiles(
428
787
  projectDir: string,
429
788
  options: InitOptions,
@@ -436,27 +795,20 @@ async function addSwallowKitFiles(
436
795
 
437
796
  const projectName = options.name;
438
797
 
439
- // 1. Update package.json to add swallowkit and @azure/cosmos dependencies
798
+ // 1. Update package.json to add runtime dependencies for generated projects
440
799
  const packageJsonPath = path.join(projectDir, 'package.json');
441
800
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
442
801
 
443
- // Add SwallowKit dependencies (Next.js version already upgraded by upgradeNextJs)
444
- // 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.
445
803
  packageJson.dependencies = {
446
804
  ...packageJson.dependencies,
447
- 'swallowkit': 'latest',
448
- '@azure/cosmos': '^4.0.0',
449
- 'applicationinsights': '^3.3.0',
450
- [`@${projectName}/shared`]: '*',
805
+ ...buildGeneratedProjectDependencies(projectName, pm),
806
+ };
807
+ packageJson.devDependencies = {
808
+ ...packageJson.devDependencies,
809
+ ...buildGeneratedProjectDevDependencies(),
451
810
  };
452
811
 
453
- if (backendLanguage !== "typescript") {
454
- packageJson.devDependencies = {
455
- ...packageJson.devDependencies,
456
- '@openapitools/openapi-generator-cli': '^2.21.0',
457
- };
458
- }
459
-
460
812
  packageJson.scripts = {
461
813
  ...packageJson.scripts,
462
814
  'build': getBuildScript(pm),
@@ -464,28 +816,27 @@ async function addSwallowKitFiles(
464
816
  'functions:start': getFunctionsStartScript(pm, backendLanguage),
465
817
  };
466
818
 
467
- if (pm === 'pnpm') {
468
- packageJson.packageManager = 'pnpm@latest';
469
- }
470
-
471
819
  packageJson.engines = {
472
- node: '20.x',
820
+ node: '>=22.14.0',
821
+ ...(pm === 'pnpm' ? { pnpm: '>=11 <13' } : {}),
473
822
  };
823
+ packageJson.devEngines = {
824
+ packageManager: { name: pm, version: pm === 'pnpm' ? '>=11 <13' : '>=10', onFail: 'error' },
825
+ };
826
+ delete packageJson.packageManager;
474
827
 
475
- // Workspace configuration depends on package manager
828
+ // Keep package.json workspaces for npm-based CI/CD even when local development uses pnpm.
476
829
  const workspacePackages = usesNodeFunctionsProject(backendLanguage) ? ['shared', 'functions'] : ['shared'];
477
830
  const wsConfig = getWorkspaceConfig(pm, workspacePackages);
478
- if (wsConfig.type === 'file') {
479
- // pnpm: workspaces are defined in pnpm-workspace.yaml
480
- delete packageJson.workspaces;
481
- } else {
482
- // npm: workspaces are defined in package.json
831
+ packageJson.workspaces = workspacePackages;
832
+
833
+ if (wsConfig.type !== 'file') {
483
834
  packageJson.workspaces = wsConfig.value;
484
835
  }
485
836
 
486
837
  fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
487
838
 
488
- // Create workspace config file if needed (pnpm-workspace.yaml)
839
+ // Create pnpm-workspace.yaml for local pnpm development when needed.
489
840
  if (wsConfig.type === 'file') {
490
841
  fs.writeFileSync(path.join(projectDir, wsConfig.filename), wsConfig.content);
491
842
  }
@@ -526,20 +877,7 @@ async function addSwallowKitFiles(
526
877
  }
527
878
 
528
879
  // 3. Create SwallowKit config
529
- const swallowkitConfig = `/** @type {import('swallowkit').SwallowKitConfig} */
530
- module.exports = {
531
- backend: {
532
- language: '${backendLanguage}',
533
- },
534
- functions: {
535
- baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
536
- },
537
- deployment: {
538
- resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
539
- swaName: process.env.AZURE_SWA_NAME || '',
540
- },
541
- }
542
- `;
880
+ const swallowkitConfig = buildSwallowKitConfigSource(backendLanguage);
543
881
  fs.writeFileSync(path.join(projectDir, 'swallowkit.config.js'), swallowkitConfig);
544
882
 
545
883
  // 4. Create shared workspace package for Zod models (Single Source of Truth)
@@ -558,6 +896,13 @@ function getFunctionsBaseUrl(): string {
558
896
  return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
559
897
  }
560
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
+
561
906
  /**
562
907
  * Simple HTTP client for calling backend APIs
563
908
  * Use this to make requests to BFF API routes (which forward to Azure Functions)
@@ -565,10 +910,11 @@ function getFunctionsBaseUrl(): string {
565
910
  async function request<T>(
566
911
  endpoint: string,
567
912
  method: 'GET' | 'POST' | 'PUT' | 'DELETE',
568
- body?: any,
913
+ body?: unknown,
569
914
  queryParams?: Record<string, string>
570
915
  ): Promise<T> {
571
916
  const functionsBaseUrl = getFunctionsBaseUrl();
917
+ const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
572
918
  let url = \`\${functionsBaseUrl}\${endpoint}\`;
573
919
  if (queryParams) {
574
920
  const params = new URLSearchParams(queryParams);
@@ -580,6 +926,7 @@ async function request<T>(
580
926
  method,
581
927
  headers: {
582
928
  'Content-Type': 'application/json',
929
+ ...keyHeaders,
583
930
  },
584
931
  body: body ? JSON.stringify(body) : undefined,
585
932
  });
@@ -635,14 +982,14 @@ export const api = {
635
982
  /**
636
983
  * Make a POST request
637
984
  */
638
- post: <T>(endpoint: string, body?: any): Promise<T> => {
985
+ post: <T>(endpoint: string, body?: unknown): Promise<T> => {
639
986
  return request<T>(endpoint, 'POST', body);
640
987
  },
641
988
 
642
989
  /**
643
990
  * Make a PUT request
644
991
  */
645
- put: <T>(endpoint: string, body?: any): Promise<T> => {
992
+ put: <T>(endpoint: string, body?: unknown): Promise<T> => {
646
993
  return request<T>(endpoint, 'PUT', body);
647
994
  },
648
995
 
@@ -663,6 +1010,7 @@ export const api = {
663
1010
  // 6. Create .env.example
664
1011
  const envExample = `# Azure Functions Backend URL
665
1012
  BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071
1013
+ BACKEND_FUNCTIONS_KEY=
666
1014
 
667
1015
  # Azure Configuration
668
1016
  AZURE_RESOURCE_GROUP=your-resource-group
@@ -702,7 +1050,7 @@ export async function register() {
702
1050
  const originalConsoleError = console.error;
703
1051
  const originalConsoleWarn = console.warn;
704
1052
 
705
- console.log = function(...args: any[]) {
1053
+ console.log = function(...args: unknown[]) {
706
1054
  originalConsoleLog.apply(console, args);
707
1055
  const message = args.map(arg =>
708
1056
  typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
@@ -713,7 +1061,7 @@ export async function register() {
713
1061
  });
714
1062
  };
715
1063
 
716
- console.error = function(...args: any[]) {
1064
+ console.error = function(...args: unknown[]) {
717
1065
  originalConsoleError.apply(console, args);
718
1066
  const message = args.map(arg =>
719
1067
  typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
@@ -724,7 +1072,7 @@ export async function register() {
724
1072
  });
725
1073
  };
726
1074
 
727
- console.warn = function(...args: any[]) {
1075
+ console.warn = function(...args: unknown[]) {
728
1076
  originalConsoleWarn.apply(console, args);
729
1077
  const message = args.map(arg =>
730
1078
  typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
@@ -748,6 +1096,7 @@ export async function register() {
748
1096
  const envLocalContent = [
749
1097
  '# Azure Functions Backend URL (Local)',
750
1098
  'BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071',
1099
+ 'BACKEND_FUNCTIONS_KEY=',
751
1100
  ''
752
1101
  ].join('\n');
753
1102
  fs.writeFileSync(path.join(projectDir, '.env.local'), envLocalContent);
@@ -796,7 +1145,7 @@ export async function register() {
796
1145
  createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage);
797
1146
 
798
1147
  // 19. Create AI agent instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
799
- createAiAgentFiles(projectDir, projectName, backendLanguage);
1148
+ createAiAgentFiles(projectDir, projectName, backendLanguage, pm);
800
1149
  }
801
1150
 
802
1151
  async function createSharedPackage(projectDir: string, projectName: string) {
@@ -808,6 +1157,7 @@ async function createSharedPackage(projectDir: string, projectName: string) {
808
1157
 
809
1158
  // shared/package.json
810
1159
  const sharedPackageJson = {
1160
+ private: true,
811
1161
  name: `@${projectName}/shared`,
812
1162
  version: '1.0.0',
813
1163
  description: 'Shared Zod models — Single Source of Truth for validation schemas',
@@ -830,28 +1180,9 @@ async function createSharedPackage(projectDir: string, projectName: string) {
830
1180
  );
831
1181
 
832
1182
  // shared/tsconfig.json
833
- const sharedTsConfig = {
834
- compilerOptions: {
835
- target: 'ES2020',
836
- module: 'commonjs',
837
- moduleResolution: 'node',
838
- lib: ['ES2020'],
839
- outDir: 'dist',
840
- rootDir: '.',
841
- declaration: true,
842
- declarationMap: true,
843
- sourceMap: true,
844
- strict: true,
845
- esModuleInterop: true,
846
- skipLibCheck: true,
847
- forceConsistentCasingInFileNames: true,
848
- },
849
- include: ['index.ts', 'models/**/*'],
850
- exclude: ['node_modules', 'dist'],
851
- };
852
1183
  fs.writeFileSync(
853
1184
  path.join(sharedDir, 'tsconfig.json'),
854
- JSON.stringify(sharedTsConfig, null, 2)
1185
+ JSON.stringify(buildSharedTsConfig(), null, 2)
855
1186
  );
856
1187
 
857
1188
  // shared/index.ts (empty re-export file, scaffold will add entries)
@@ -945,8 +1276,9 @@ tsconfig.json
945
1276
  !dist/**/*.js
946
1277
  `);
947
1278
  } else if (backendLanguage === 'python') {
948
- gitignoreLines.unshift('.venv', '__pycache__', '.python_packages');
1279
+ gitignoreLines.unshift('.venv', '.codegen-venv', '__pycache__', '.python_packages');
949
1280
  fs.writeFileSync(path.join(functionsDir, '.funcignore'), `.venv
1281
+ .codegen-venv
950
1282
  __pycache__
951
1283
  .pytest_cache
952
1284
  .mypy_cache
@@ -966,8 +1298,8 @@ tests
966
1298
  fs.writeFileSync(path.join(functionsDir, '.gitignore'), `${gitignoreLines.join('\n')}\n`);
967
1299
  }
968
1300
 
969
- function createTypeScriptFunctionsProject(projectDir: string, functionsDir: string, pm: PackageManager): void {
970
- const functionsPackageJson = {
1301
+ export function buildTypeScriptFunctionsPackageJson(projectName: string, pm: PackageManager = 'pnpm') {
1302
+ return {
971
1303
  name: 'functions',
972
1304
  version: '1.0.0',
973
1305
  description: 'Azure Functions backend',
@@ -982,13 +1314,17 @@ function createTypeScriptFunctionsProject(projectDir: string, functionsDir: stri
982
1314
  '@azure/cosmos': '^4.0.0',
983
1315
  '@azure/identity': '^4.0.0',
984
1316
  zod: '>=3.25.0',
985
- [`@${path.basename(projectDir)}/shared`]: '*',
1317
+ [`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:../shared',
986
1318
  },
987
1319
  devDependencies: {
988
1320
  '@types/node': '^20.0.0',
989
1321
  typescript: '^5.0.0',
990
1322
  },
991
1323
  };
1324
+ }
1325
+
1326
+ function createTypeScriptFunctionsProject(projectDir: string, functionsDir: string, pm: PackageManager): void {
1327
+ const functionsPackageJson = buildTypeScriptFunctionsPackageJson(path.basename(projectDir), pm);
992
1328
  fs.writeFileSync(path.join(functionsDir, 'package.json'), JSON.stringify(functionsPackageJson, null, 2));
993
1329
 
994
1330
  const sharedPkgName = `@${path.basename(projectDir)}/shared`;
@@ -1061,7 +1397,7 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
1061
1397
 
1062
1398
  app.http('greet', {
1063
1399
  methods: ['GET', 'POST'],
1064
- authLevel: 'anonymous',
1400
+ authLevel: 'function',
1065
1401
  handler: greet
1066
1402
  });
1067
1403
  `);
@@ -1073,6 +1409,11 @@ function createCSharpFunctionsProject(projectDir: string, functionsDir: string):
1073
1409
  const csprojName = `${projectPascal}.Functions.csproj`;
1074
1410
 
1075
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
+ );
1076
1417
 
1077
1418
  fs.writeFileSync(path.join(functionsDir, 'Program.cs'), buildCSharpFunctionsProgramSource());
1078
1419
 
@@ -1088,7 +1429,7 @@ public sealed class GreetFunction
1088
1429
  {
1089
1430
  [Function("greet")]
1090
1431
  public async Task<HttpResponseData> Run(
1091
- [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "greet")] HttpRequestData request)
1432
+ [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "greet")] HttpRequestData request)
1092
1433
  {
1093
1434
  var query = request.Url.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
1094
1435
  var name = "SwallowKit";
@@ -1115,11 +1456,16 @@ public sealed class GreetFunction
1115
1456
 
1116
1457
  function createPythonFunctionsProject(projectDir: string, functionsDir: string): void {
1117
1458
  fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
1459
+ ensureProjectGitignoreEntry(projectDir, '.uv');
1118
1460
 
1119
1461
  fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
1120
1462
  azure-cosmos>=4.9.0
1121
1463
  azure-identity>=1.19.0
1122
1464
  `);
1465
+ fs.writeFileSync(
1466
+ path.join(functionsDir, 'requirements.codegen.txt'),
1467
+ buildPythonCodegenRequirementsSource()
1468
+ );
1123
1469
 
1124
1470
  const blueprintsDir = path.join(functionsDir, 'blueprints');
1125
1471
  fs.mkdirSync(blueprintsDir, { recursive: true });
@@ -1151,13 +1497,29 @@ def greet(req: func.HttpRequest) -> func.HttpResponse:
1151
1497
 
1152
1498
  from blueprints.greet import bp as greet_bp
1153
1499
 
1154
- app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
1500
+ app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
1155
1501
 
1156
1502
  app.register_blueprint(greet_bp)
1157
1503
  # SwallowKit scaffold registrations
1158
1504
  `);
1159
1505
  }
1160
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
+
1161
1523
  async function createBffApiRoute(projectDir: string) {
1162
1524
  console.log('📦 Creating BFF API route...\n');
1163
1525
 
@@ -1328,7 +1690,7 @@ export default function Home() {
1328
1690
  Create your first model with Zod and generate CRUD operations automatically.
1329
1691
  </p>
1330
1692
  <code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
1331
- ${pmCmd.dlx} swallowkit scaffold shared/models/your-model.ts
1693
+ ${pmCmd.exec} swallowkit scaffold shared/models/your-model.ts
1332
1694
  </code>
1333
1695
  </div>
1334
1696
  </section>
@@ -1388,17 +1750,18 @@ function createReadme(
1388
1750
  const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
1389
1751
  const schemaBridgeDescription = backendLanguage === 'typescript'
1390
1752
  ? 'Zod (shared between frontend and backend)'
1391
- : `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/)`;
1392
1754
  const functionsTree = backendLanguage === 'typescript'
1393
1755
  ? `│ └── src/\n│ └── greet.ts # Sample function`
1394
1756
  : backendLanguage === 'csharp'
1395
- ? `│ ├── Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ # OpenAPI-derived C# models`
1396
- : `│ ├── 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`;
1397
1759
  const backendScaffoldNote = backendLanguage === 'typescript'
1398
1760
  ? '- Azure Functions CRUD endpoints'
1399
- : `- 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);
1400
1763
  const pythonLocalDevNote = backendLanguage === 'python'
1401
- ? `\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`
1402
1765
  : '';
1403
1766
 
1404
1767
  const readme = `# ${projectName}
@@ -1428,7 +1791,7 @@ This project was initialized with the following settings:
1428
1791
 
1429
1792
  Before you begin, ensure you have the following installed:
1430
1793
 
1431
- 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\`` : ''}
1432
1795
  ${pm === 'pnpm' ? '3' : '2'}. **Azure CLI**: Required for provisioning Azure resources
1433
1796
  - Install: \`winget install Microsoft.AzureCLI\` (Windows)
1434
1797
  - Or: [Download](https://aka.ms/installazurecliwindows)
@@ -1436,7 +1799,7 @@ ${pm === 'pnpm' ? '4' : '3'}. **Azure Cosmos DB Emulator**: Required for local d
1436
1799
  - Windows: \`winget install Microsoft.Azure.CosmosEmulator\`
1437
1800
  - Or: [Download](https://aka.ms/cosmosdb-emulator)
1438
1801
  - Docker: \`docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator\`
1439
- ${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
1440
1803
 
1441
1804
  ## 📁 Project Structure
1442
1805
 
@@ -1462,7 +1825,7 @@ ${functionsTree}
1462
1825
  Define your data model with Zod schema:
1463
1826
 
1464
1827
  \`\`\`bash
1465
- ${pmCmd.dlx} swallowkit create-model <model-name>
1828
+ ${pmCmd.exec} swallowkit create-model <model-name>
1466
1829
  \`\`\`
1467
1830
 
1468
1831
  This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to define your schema.
@@ -1472,7 +1835,7 @@ This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to defin
1472
1835
  Generate complete CRUD operations (Functions, API routes, UI):
1473
1836
 
1474
1837
  \`\`\`bash
1475
- ${pmCmd.dlx} swallowkit scaffold shared/models/<model-name>.ts
1838
+ ${pmCmd.exec} swallowkit scaffold shared/models/<model-name>.ts
1476
1839
  \`\`\`
1477
1840
 
1478
1841
  This generates:
@@ -1484,7 +1847,7 @@ ${backendScaffoldNote}
1484
1847
  ### 3. Start Development Servers
1485
1848
 
1486
1849
  \`\`\`bash
1487
- ${pmCmd.dlx} swallowkit dev
1850
+ ${pmCmd.exec} swallowkit dev
1488
1851
  \`\`\`
1489
1852
 
1490
1853
  This starts:
@@ -1502,7 +1865,7 @@ ${pythonLocalDevNote}
1502
1865
  Create all required Azure resources using Bicep:
1503
1866
 
1504
1867
  \`\`\`bash
1505
- ${pmCmd.dlx} swallowkit provision --resource-group <rg-name>
1868
+ ${pmCmd.exec} swallowkit provision --resource-group <rg-name>
1506
1869
  \`\`\`
1507
1870
 
1508
1871
  This creates:
@@ -1558,10 +1921,10 @@ func azure functionapp publish func-${projectName}
1558
1921
 
1559
1922
  ## 🔧 Available Commands
1560
1923
 
1561
- - \`${pmCmd.dlx} swallowkit create-model <name>\` - Create a new data model
1562
- - \`${pmCmd.dlx} swallowkit scaffold <model-file>\` - Generate CRUD code
1563
- - \`${pmCmd.dlx} swallowkit dev\` - Start development servers
1564
- - \`${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
1565
1928
  ${azureConfig.vnetOption !== 'none' ? `
1566
1929
  ## 🔒 Network Security (VNet Configuration)
1567
1930
 
@@ -1610,448 +1973,79 @@ This project was generated by SwallowKit. If you encounter any issues or have su
1610
1973
  console.log('✅ README.md created\n');
1611
1974
  }
1612
1975
 
1613
- function createAiAgentFiles(projectDir: string, projectName: string, backendLanguage: BackendLanguage) {
1976
+ function createAiAgentFiles(
1977
+ projectDir: string,
1978
+ projectName: string,
1979
+ backendLanguage: BackendLanguage,
1980
+ pm: PackageManager
1981
+ ) {
1614
1982
  console.log('🤖 Creating AI agent instruction files...\n');
1615
- const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
1616
- const functionsStructureLine = backendLanguage === 'typescript'
1617
- ? `│ └── src/ # HTTP trigger handlers with Cosmos DB bindings`
1618
- : backendLanguage === 'csharp'
1619
- ? `│ ├── Crud/ # C# HTTP trigger handlers\n│ └── generated/ # OpenAPI-derived C# schema assets`
1620
- : `│ ├── blueprints/ # Python HTTP trigger handlers\n│ └── generated/ # OpenAPI-derived Python schema assets`;
1621
- const backendSchemaNote = backendLanguage === 'typescript'
1622
- ? `- The shared package (\`@${projectName}/shared\`) is consumed by both Next.js and Azure Functions as a workspace dependency.`
1623
- : `- 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.`;
1624
- const backendRulesNote = backendLanguage === 'typescript'
1625
- ? `- 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.`
1626
- : `- 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\`.`;
1627
-
1628
- // ── 1. AGENTS.md (Codex / generic agents) ──────────────────────────
1629
-
1630
- const agentsMd = `# AGENTS.md
1631
-
1632
- This project was generated by **SwallowKit**.
1633
- All coding agents **must** follow the architecture and conventions described below.
1634
-
1635
- ## Architecture Overview
1636
-
1637
- This is a full-stack application deployed on Azure with a TypeScript frontend/BFF and an Azure Functions backend in ${backendLanguageLabel}.
1638
-
1639
- \`\`\`
1640
- Frontend (React / Next.js App Router)
1641
- ↓ fetch('/api/{model}', ...)
1642
- BFF Layer (Next.js API Routes)
1643
- ↓ HTTP → Azure Functions
1644
- Backend (Azure Functions)
1645
- ↓
1646
- Azure Cosmos DB (Document Database)
1647
- \`\`\`
1648
-
1649
- ### Project Structure
1650
-
1651
- \`\`\`
1652
- ${projectName}/
1653
- ├── app/ # Next.js App Router
1654
- │ ├── api/ # BFF API routes (proxy to Azure Functions)
1655
- │ └── {model}/ # UI pages per model (list, detail, create, edit)
1656
- ├── functions/ # Azure Functions (backend)
1657
- ${functionsStructureLine}
1658
- ├── shared/ # Shared workspace package
1659
- │ ├── models/ # Zod schema definitions (single source of truth)
1660
- │ └── index.ts # Re-exports all models
1661
- ├── lib/
1662
- │ └── api/ # API client utilities (backend.ts, call-function.ts)
1663
- ├── components/ # Shared React components
1664
- ├── infra/ # Bicep infrastructure-as-code files
1665
- │ ├── main.bicep
1666
- │ └── modules/
1667
- └── .github/workflows/ # CI/CD workflows (if configured)
1668
- \`\`\`
1669
-
1670
- ## Critical Design Principles
1671
-
1672
- ### 1. Next.js API Routes Are Strictly a BFF (Backend for Frontend)
1983
+ const runCmd = getCommands(pm).exec;
1673
1984
 
1674
- - \`app/api/\` routes exist **only** to proxy requests to Azure Functions.
1675
- - **Never** place business logic, database access, or direct Cosmos DB calls in Next.js API routes.
1676
- - The BFF layer may validate input/output with Zod schemas before forwarding to Functions.
1677
- - Use the \`callFunction\` helper (\`lib/api/call-function.ts\`) or the \`api\` client (\`lib/api/backend.ts\`) to call Azure Functions.
1678
-
1679
- Example BFF route pattern:
1680
-
1681
- \`\`\`typescript
1682
- // app/api/{model}/route.ts
1683
- import { callFunction } from '@/lib/api/call-function';
1684
- import { ModelSchema } from '@${projectName}/shared';
1685
- import { z } from 'zod/v4';
1686
-
1687
- export async function GET() {
1688
- return callFunction({
1689
- method: 'GET',
1690
- path: '/api/{model}',
1691
- responseSchema: z.array(ModelSchema),
1692
- });
1693
- }
1694
-
1695
- export async function POST(request: NextRequest) {
1696
- const body = await request.json();
1697
- return callFunction({
1698
- method: 'POST',
1699
- path: '/api/{model}',
1700
- body,
1701
- inputSchema: ModelSchema.omit({ id: true, createdAt: true, updatedAt: true }),
1702
- responseSchema: ModelSchema,
1703
- successStatus: 201,
1985
+ writeAgentInstructionFiles(projectDir, {
1986
+ projectName,
1987
+ backendLanguage,
1988
+ packageManager: pm,
1704
1989
  });
1705
- }
1706
- \`\`\`
1707
-
1708
- ### 2. Zod Schemas Are the Single Source of Truth
1709
-
1710
- - All data models are defined **once** as Zod schemas in \`shared/models/\`.
1711
- - TypeScript types are derived with \`z.infer<typeof Schema>\` — never define types separately.
1712
- - ${backendSchemaNote}
1713
-
1714
- Model definition pattern:
1715
-
1716
- \`\`\`typescript
1717
- // shared/models/{model}.ts
1718
- import { z } from 'zod/v4';
1719
-
1720
- export const Todo = z.object({
1721
- id: z.string(),
1722
- name: z.string().min(1),
1723
- // ... your fields
1724
- createdAt: z.string().optional(),
1725
- updatedAt: z.string().optional(),
1726
- });
1727
-
1728
- export type Todo = z.infer<typeof Todo>;
1729
- export const displayName = 'Todo';
1730
- \`\`\`
1731
-
1732
- Key rules:
1733
- - Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
1734
- - \`id\`, \`createdAt\`, and \`updatedAt\` are auto-managed by the backend. Mark them as \`optional()\` in the schema.
1735
- - Always re-export models from \`shared/index.ts\`.
1736
-
1737
- ### 3. Azure Functions Own All Business Logic and Data Access
1738
-
1739
- - ${backendRulesNote}
1740
-
1741
- ${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.`}
1742
-
1743
- ${backendLanguage === 'typescript' ? `\`\`\`typescript
1744
- // functions/src/{model}.ts
1745
- import { app } from '@azure/functions';
1746
- import { ModelSchema } from '@${projectName}/shared';
1747
-
1748
- const containerName = 'Models'; // PascalCase + 's'
1749
-
1750
- app.http('{model}-get-all', {
1751
- methods: ['GET'],
1752
- route: '{model}',
1753
- authLevel: 'anonymous',
1754
- extraInputs: [{ type: 'cosmosDB', name: 'cosmosInput', containerName, ... }],
1755
- handler: async (request, context) => {
1756
- const documents = context.extraInputs.get('cosmosInput');
1757
- const validated = z.array(ModelSchema).parse(documents);
1758
- return { status: 200, jsonBody: validated };
1759
- },
1760
- });
1761
- \`\`\`` : ''}
1762
-
1763
- ## Naming Conventions
1764
-
1765
- | Item | Convention | Example |
1766
- |------|-----------|---------|
1767
- | Model schema file | \`shared/models/{kebab-case}.ts\` | \`shared/models/todo.ts\` |
1768
- | Schema/type name | PascalCase (same name for both) | \`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\` |
1769
- | Functions handler file | backend-language specific under \`functions/\` | \`${backendLanguage === 'typescript' ? 'functions/src/todo.ts' : backendLanguage === 'csharp' ? 'functions/Crud/TodoFunctions.cs' : 'functions/blueprints/todo.py'}\` |
1770
- | Functions handler name | \`{camelCase}-{operation}\` | \`todo-get-all\`, \`todo-create\` |
1771
- | API route path | \`/api/{camelCase}\` | \`/api/todo\`, \`/api/todo/{id}\` |
1772
- | BFF route file | \`app/api/{kebab-case}/route.ts\` | \`app/api/todo/route.ts\` |
1773
- | BFF detail route | \`app/api/{kebab-case}/[id]/route.ts\` | \`app/api/todo/[id]/route.ts\` |
1774
- | UI page directory | \`app/{kebab-case}/\` | \`app/todo/page.tsx\` |
1775
- | React component | PascalCase | \`TodoForm.tsx\` |
1776
- | Cosmos DB container | PascalCase + 's' | \`Todos\` |
1777
- | Cosmos DB partition key | \`/id\` | Always \`/id\` |
1778
- | Bicep container file | \`infra/containers/{kebab-case}-container.bicep\` | \`infra/containers/todo-container.bicep\` |
1779
-
1780
- ## Adding New Models (SwallowKit CLI Skills)
1781
-
1782
- Use the SwallowKit CLI — do **not** manually create model files or CRUD boilerplate.
1783
-
1784
- ### Skill: Create a new data model
1785
-
1786
- \`\`\`bash
1787
- npx swallowkit create-model <name>
1788
- # Multiple models at once:
1789
- npx swallowkit create-model user post comment
1790
- \`\`\`
1791
-
1792
- Creates \`shared/models/<name>.ts\` with a Zod schema template including \`id\`, \`createdAt\`, \`updatedAt\`.
1793
- Edit the generated file to add your domain-specific fields, then run scaffold.
1794
-
1795
- ### Skill: Generate full CRUD from a model
1796
-
1797
- \`\`\`bash
1798
- npx swallowkit scaffold shared/models/<name>.ts
1799
- \`\`\`
1800
-
1801
- Generates:
1802
- - Azure Functions handlers (${backendLanguage === 'typescript' ? '\`functions/src/<name>.ts\`' : '\`functions/\` language-specific CRUD files + \`functions/generated/\` schema assets'})
1803
- - BFF API routes (\`app/api/<name>/route.ts\`, \`app/api/<name>/[id]/route.ts\`)
1804
- - UI pages (\`app/<name>/page.tsx\`, detail, create, edit pages)
1805
- - Cosmos DB Bicep container config (\`infra/containers/<name>-container.bicep\`)
1806
-
1807
- ### Skill: Start development servers
1808
-
1809
- \`\`\`bash
1810
- npx swallowkit dev
1811
- \`\`\`
1812
-
1813
- Runs Next.js (http://localhost:3000) and Azure Functions (http://localhost:7071) concurrently.
1814
- Checks for Cosmos DB Emulator availability.
1815
-
1816
- ### Skill: Provision Azure resources
1817
-
1818
- \`\`\`bash
1819
- npx swallowkit provision --resource-group <name> --location <region>
1820
- \`\`\`
1821
-
1822
- Deploys Bicep infrastructure: Static Web Apps, Functions, Cosmos DB, Storage, Managed Identity.
1823
-
1824
- ### Typical workflow for "add a new feature/model"
1825
-
1826
- 1. \`npx swallowkit create-model <name>\`
1827
- 2. Edit \`shared/models/<name>.ts\` — add fields
1828
- 3. \`npx swallowkit scaffold shared/models/<name>.ts\`
1829
- 4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
1830
-
1831
- ## Do NOT
1832
-
1833
- - **Do not** put business logic or database calls in \`app/api/\` routes. They are BFF only.
1834
- - **Do not** define TypeScript interfaces/types separately from Zod schemas. Always derive types with \`z.infer<>\`.
1835
- - **Do not** manually duplicate model definitions across layers. Use the shared package.
1836
- - **Do not** manually create CRUD boilerplate. Use \`swallowkit scaffold\`.
1837
- - **Do not** hardcode Cosmos DB connection strings. Use Managed Identity (\`CosmosDBConnection__accountEndpoint\`) in production and emulator settings locally.
1838
- - **Do not** change the partition key strategy. All containers use \`/id\` as the partition key.
1839
-
1840
- ## Technology Stack
1841
-
1842
- - **Frontend**: Next.js (App Router), React, TypeScript, Tailwind CSS
1843
- - **BFF**: Next.js API Routes (proxy only)
1844
- - **Backend**: Azure Functions (${backendLanguageLabel})
1845
- - **Database**: Azure Cosmos DB (NoSQL)
1846
- - **Schema**: Zod (shared across all layers via workspace package)
1847
- - **Infrastructure**: Bicep (IaC)
1848
- - **Hosting**: Azure Static Web Apps (frontend) + Azure Functions Flex Consumption (backend)
1849
- - **Auth**: Azure Managed Identity (no connection strings in production)
1850
- - **Monitoring**: Application Insights
1851
- `;
1852
-
1853
- fs.writeFileSync(path.join(projectDir, 'AGENTS.md'), agentsMd);
1854
- console.log(' ✅ AGENTS.md (Codex / generic agents)');
1855
-
1856
- // ── 2. CLAUDE.md (Claude Code) ─────────────────────────────────────
1857
-
1858
- const claudeMd = `# CLAUDE.md
1859
-
1860
- This file is for Claude Code. Read AGENTS.md in the project root for the full architecture, conventions, and rules.
1861
-
1862
- ## Quick Reference
1863
-
1864
- - **Architecture**: Next.js (frontend) → BFF (API routes, proxy only) → Azure Functions (backend) → Cosmos DB
1865
- - **Schema**: Zod schemas in \`shared/models/\` are the single source of truth. Never define types separately.
1866
- - **BFF rule**: \`app/api/\` routes must ONLY proxy to Azure Functions via \`callFunction()\`. No business logic.
1867
- - **Backend language**: ${backendLanguageLabel}
1868
- - **Backend rule**: Regenerate backend contracts with \`swallowkit scaffold\` after schema changes and keep \`functions/generated/\` in sync.
1869
-
1870
- ## SwallowKit CLI Commands
1871
-
1872
- | Task | Command |
1873
- |------|---------|
1874
- | Create model | \`npx swallowkit create-model <name>\` |
1875
- | Generate CRUD | \`npx swallowkit scaffold shared/models/<name>.ts\` |
1876
- | Dev servers | \`npx swallowkit dev\` |
1877
- | Provision Azure | \`npx swallowkit provision --resource-group <rg> --location <region>\` |
1878
-
1879
- ## Workflow: Add a new model
1880
-
1881
- 1. \`npx swallowkit create-model <name>\`
1882
- 2. Edit \`shared/models/<name>.ts\` — add your fields
1883
- 3. \`npx swallowkit scaffold shared/models/<name>.ts\`
1884
- 4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
1885
- `;
1886
-
1887
- fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), claudeMd);
1888
- console.log(' ✅ CLAUDE.md (Claude Code)');
1889
-
1890
- // ── 3. .github/copilot-instructions.md (GitHub Copilot) ────────────
1891
-
1892
- const ghDir = path.join(projectDir, '.github');
1893
- fs.mkdirSync(ghDir, { recursive: true });
1894
-
1895
- const copilotInstructions = `# Copilot Instructions
1896
-
1897
- This project was generated by **SwallowKit**. See \`AGENTS.md\` in the project root for the full specification.
1898
-
1899
- ## Architecture (3-layer)
1900
-
1901
- \`\`\`
1902
- Frontend (Next.js App Router) → BFF (Next.js API Routes) → Backend (Azure Functions) → Cosmos DB
1903
- \`\`\`
1904
-
1905
- ## Key Rules
1906
-
1907
- 1. **BFF is proxy only** — \`app/api/\` routes call Azure Functions via \`callFunction()\`. No business logic, no direct DB access.
1908
- 2. **Zod = single source of truth** — Models live in \`shared/models/\`. Types are derived with \`z.infer<>\`. Never define types separately.
1909
- 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/\`.
1910
- 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.
1911
-
1912
- ## Naming
1913
-
1914
- - Schema/type: PascalCase, same name for both (\`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\`)
1915
- - Files: kebab-case (\`shared/models/todo.ts\`, backend handlers under \`functions/\`)
1916
- - Cosmos DB containers: PascalCase + 's' (\`Todos\`), partition key always \`/id\`
1917
-
1918
- ## Managed Fields
1919
-
1920
- \`id\`, \`createdAt\`, \`updatedAt\` are auto-managed by the backend. Define them as \`optional()\` in schemas. Never trust client-sent values.
1921
- `;
1922
-
1923
- fs.writeFileSync(path.join(ghDir, 'copilot-instructions.md'), copilotInstructions);
1924
- console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot)');
1925
-
1926
- // ── 4. .github/instructions/*.instructions.md (Copilot layer-specific) ──
1927
-
1928
- const instructionsDir = path.join(ghDir, 'instructions');
1929
- fs.mkdirSync(instructionsDir, { recursive: true });
1930
-
1931
- // 4a. shared/models — Zod schema layer
1932
- const sharedModelsInstructions = `---
1933
- applyTo: "shared/models/**"
1934
- ---
1935
-
1936
- # Shared Models — Zod Schema Rules
1937
-
1938
- Files in this directory are the **single source of truth** for data models across the entire application.
1939
-
1940
- ## Rules
1941
-
1942
- - Define Zod schemas using \`zod/v4\` (\`import { z } from 'zod/v4'\`).
1943
- - Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
1944
- \`\`\`typescript
1945
- export const Todo = z.object({ ... });
1946
- export type Todo = z.infer<typeof Todo>;
1947
- \`\`\`
1948
- - Always include \`id: z.string()\`, \`createdAt: z.string().optional()\`, \`updatedAt: z.string().optional()\`. These are managed by the backend.
1949
- - Export a \`displayName\` string constant for UI display.
1950
- - Re-export every model from \`shared/index.ts\`.
1951
- - For relationships, use **nested schemas** (import and embed the related schema), not ID references.
1952
- - After editing a model, run \`npx swallowkit scaffold shared/models/<name>.ts\` to regenerate CRUD code.
1953
- `;
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)');
1954
1994
 
1955
1995
  fs.writeFileSync(
1956
- path.join(instructionsDir, 'shared-models.instructions.md'),
1957
- sharedModelsInstructions
1996
+ path.join(projectDir, '.mcp.json'),
1997
+ buildSwallowKitMcpProjectConfigSource(pm)
1958
1998
  );
1999
+ console.log(' ✅ .mcp.json (project-scoped MCP bootstrap)');
1959
2000
 
1960
- // 4b. app/api — BFF layer
1961
- const bffInstructions = `---
1962
- applyTo: "app/api/**"
1963
- ---
1964
-
1965
- # BFF API Routes — Rules
1966
-
1967
- Files in \`app/api/\` are the **BFF (Backend for Frontend)** layer. They exist solely to proxy requests to Azure Functions.
1968
-
1969
- ## Rules
1970
-
1971
- - **Never** put business logic, database access, or direct Cosmos DB calls here.
1972
- - Use \`callFunction()\` from \`@/lib/api/call-function\` to forward requests to Azure Functions.
1973
- - You may validate input/output with Zod schemas before forwarding.
1974
- - Import schemas from \`@${projectName}/shared\`.
2001
+ writeWorkflowDocs(projectDir, runCmd);
2002
+ console.log(' ✅ .swallowkit/workflows/ (agent-agnostic autonomous-loop runbooks)');
1975
2003
 
1976
- ## Pattern
1977
-
1978
- \`\`\`typescript
1979
- import { callFunction } from '@/lib/api/call-function';
1980
- import { ModelSchema } from '@${projectName}/shared';
1981
- import { z } from 'zod/v4';
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
+ }
1982
2009
 
1983
- export async function GET() {
1984
- return callFunction({
1985
- method: 'GET',
1986
- path: '/api/{model}',
1987
- responseSchema: z.array(ModelSchema),
1988
- });
2010
+ console.log('\n✅ AI agent files created\n');
2011
+ }
2012
+ export function buildFunctionsHostKeyBicepExpression(): string {
2013
+ return "listKeys('${resourceId('Microsoft.Web/sites', functionsAppName)}/host/default', '2023-12-01').functionKeys.default";
1989
2014
  }
1990
- \`\`\`
1991
- `;
1992
-
1993
- fs.writeFileSync(
1994
- path.join(instructionsDir, 'bff-routes.instructions.md'),
1995
- bffInstructions
1996
- );
1997
-
1998
- // 4c. functions — Azure Functions backend layer
1999
- const functionsInstructions = `---
2000
- applyTo: "functions/**"
2001
- ---
2002
2015
 
2003
- # Azure Functions — Backend Rules
2016
+ export function buildStaticWebAppConfigBicepSource(): string {
2017
+ return `@description('Static Web App name')
2018
+ param staticWebAppName string
2004
2019
 
2005
- Files in \`functions/\` contain all business logic and data access for this application.
2020
+ @description('Functions App default hostname for backend API calls')
2021
+ param functionsDefaultHostname string
2006
2022
 
2007
- ## Rules
2023
+ @description('Functions App name used to resolve its host key after deployment')
2024
+ param functionsAppName string
2008
2025
 
2009
- - Keep backend contracts aligned with \`shared/models/\` by rerunning \`swallowkit scaffold\` after schema changes.
2010
- - For TypeScript backends, use Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.
2011
- - For C#/Python backends, consume the generated OpenAPI-derived assets in \`functions/generated/\`.
2012
- - Auto-generate \`id\` (UUID), \`createdAt\`, and \`updatedAt\` on the backend. Never trust client-sent values.
2013
- - Container names are PascalCase + 's' (e.g., \`Todos\`). Partition key is always \`/id\`.
2026
+ @description('Application Insights connection string for SWA')
2027
+ param appInsightsConnectionString string
2014
2028
 
2015
- ## Handler Pattern
2029
+ resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
2030
+ name: staticWebAppName
2031
+ }
2016
2032
 
2017
- \`\`\`typescript
2018
- import { app } from '@azure/functions';
2019
- 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
+ }
2020
2043
 
2021
- app.http('{model}-get-all', {
2022
- methods: ['GET'],
2023
- route: '{model}',
2024
- authLevel: 'anonymous',
2025
- extraInputs: [cosmosInput],
2026
- handler: async (request, context) => {
2027
- const documents = context.extraInputs.get(cosmosInput);
2028
- const validated = z.array(ModelSchema).parse(documents);
2029
- return { status: 200, jsonBody: validated };
2030
- },
2031
- });
2032
- \`\`\`
2044
+ output configName string = staticWebAppConfig.name
2033
2045
  `;
2034
-
2035
- fs.writeFileSync(
2036
- path.join(instructionsDir, 'azure-functions.instructions.md'),
2037
- functionsInstructions
2038
- );
2039
-
2040
- console.log(' ✅ .github/instructions/ (Copilot layer-specific instructions)');
2041
- console.log(' - shared-models.instructions.md');
2042
- console.log(' - bff-routes.instructions.md');
2043
- console.log(' - azure-functions.instructions.md');
2044
-
2045
- console.log('\n✅ AI agent files created\n');
2046
- console.log(' Supported agents:');
2047
- console.log(' - OpenAI Codex → AGENTS.md');
2048
- console.log(' - Claude Code → CLAUDE.md (+ AGENTS.md)');
2049
- console.log(' - GitHub Copilot → .github/copilot-instructions.md');
2050
- console.log(' - GitHub Copilot (edit) → .github/instructions/*.instructions.md');
2051
- console.log('');
2052
2046
  }
2053
2047
 
2054
- async function createInfrastructure(
2048
+ export async function createInfrastructure(
2055
2049
  projectDir: string,
2056
2050
  projectName: string,
2057
2051
  azureConfig: AzureConfig,
@@ -2065,6 +2059,7 @@ async function createInfrastructure(
2065
2059
 
2066
2060
  const enableVNet = azureConfig.vnetOption !== 'none';
2067
2061
  const functionsRuntime = getFunctionsRuntimeConfig(backendLanguage);
2062
+ const staticWebAppSku = getStaticWebAppSku(azureConfig.swaPlan);
2068
2063
 
2069
2064
  // main.bicep
2070
2065
  const mainBicep = `targetScope = 'resourceGroup'
@@ -2078,6 +2073,13 @@ param location string = resourceGroup().location
2078
2073
  @description('Location for Static Web App (must be explicitly provided)')
2079
2074
  param swaLocation string
2080
2075
 
2076
+ @description('Static Web Apps hosting plan')
2077
+ @allowed([
2078
+ 'Free'
2079
+ 'Standard'
2080
+ ])
2081
+ param staticWebAppSku string = '${staticWebAppSku}'
2082
+
2081
2083
  @description('Cosmos DB mode')
2082
2084
  @allowed(['freetier', 'serverless'])
2083
2085
  param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
@@ -2120,7 +2122,7 @@ module staticWebApp 'modules/staticwebapp.bicep' = {
2120
2122
  params: {
2121
2123
  name: 'swa-\${projectName}'
2122
2124
  location: swaLocation
2123
- sku: 'Standard'
2125
+ sku: staticWebAppSku
2124
2126
  appInsightsConnectionString: appInsightsSwa.outputs.connectionString
2125
2127
  }
2126
2128
  }
@@ -2161,16 +2163,11 @@ module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enabl
2161
2163
  params: {
2162
2164
  name: 'pe-cosmos-\${projectName}'
2163
2165
  location: location
2164
- cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.id : cosmosDbServerless.outputs.id
2165
- cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
2166
- subnetId: vnet.outputs.privateEndpointSubnetId
2167
- 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
2168
2170
  }
2169
- dependsOn: [
2170
- cosmosDbFreeTier
2171
- cosmosDbServerless
2172
- vnet
2173
- ]
2174
2171
  }
2175
2172
 
2176
2173
  // Azure Functions (Flex Consumption) - Deploy AFTER Cosmos DB
@@ -2182,16 +2179,14 @@ module functionsFlex 'modules/functions-flex.bicep' = {
2182
2179
  storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
2183
2180
  appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
2184
2181
  swaDefaultHostname: staticWebApp.outputs.defaultHostname
2185
- cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
2186
- 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
2187
2184
  functionsRuntimeName: '${functionsRuntime.name}'
2188
2185
  functionsRuntimeVersion: '${functionsRuntime.version}'
2189
2186
  enableVNet: enableVNet
2190
- vnetSubnetId: enableVNet ? vnet.outputs.functionsSubnetId : ''
2187
+ vnetSubnetId: enableVNet ? vnet!.outputs.functionsSubnetId : ''
2191
2188
  }
2192
2189
  dependsOn: [
2193
- cosmosDbFreeTier
2194
- cosmosDbServerless
2195
2190
  cosmosPrivateEndpoint
2196
2191
  ]
2197
2192
  }
@@ -2200,23 +2195,17 @@ module functionsFlex 'modules/functions-flex.bicep' = {
2200
2195
  module cosmosDbRoleAssignmentFreeTier 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'freetier') {
2201
2196
  name: 'cosmosDbRoleAssignment'
2202
2197
  params: {
2203
- cosmosAccountName: cosmosDbFreeTier.outputs.accountName
2198
+ cosmosAccountName: cosmosDbFreeTier!.outputs.accountName
2204
2199
  functionsPrincipalId: functionsFlex.outputs.principalId
2205
2200
  }
2206
- dependsOn: [
2207
- functionsFlex
2208
- ]
2209
2201
  }
2210
2202
 
2211
2203
  module cosmosDbRoleAssignmentServerless 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'serverless') {
2212
2204
  name: 'cosmosDbRoleAssignment'
2213
2205
  params: {
2214
- cosmosAccountName: cosmosDbServerless.outputs.accountName
2206
+ cosmosAccountName: cosmosDbServerless!.outputs.accountName
2215
2207
  functionsPrincipalId: functionsFlex.outputs.principalId
2216
2208
  }
2217
- dependsOn: [
2218
- functionsFlex
2219
- ]
2220
2209
  }
2221
2210
 
2222
2211
  // Update SWA config with Functions hostname (after Functions deployment)
@@ -2225,20 +2214,18 @@ module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
2225
2214
  params: {
2226
2215
  staticWebAppName: staticWebApp.outputs.name
2227
2216
  functionsDefaultHostname: functionsFlex.outputs.defaultHostname
2217
+ functionsAppName: functionsFlex.outputs.name
2228
2218
  appInsightsConnectionString: appInsightsSwa.outputs.connectionString
2229
2219
  }
2230
- dependsOn: [
2231
- functionsFlex
2232
- ]
2233
2220
  }
2234
2221
 
2235
2222
  output staticWebAppName string = staticWebApp.outputs.name
2236
2223
  output staticWebAppUrl string = staticWebApp.outputs.defaultHostname
2237
2224
  output functionsAppName string = functionsFlex.outputs.name
2238
2225
  output functionsAppUrl string = functionsFlex.outputs.defaultHostname
2239
- output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
2240
- output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
2241
- 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
2242
2229
  output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
2243
2230
  output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
2244
2231
  output appInsightsSwaName string = appInsightsSwa.outputs.name
@@ -2246,7 +2233,7 @@ output appInsightsSwaConnectionString string = appInsightsSwa.outputs.connection
2246
2233
  output appInsightsFunctionsName string = appInsightsFunctions.outputs.name
2247
2234
  output appInsightsFunctionsConnectionString string = appInsightsFunctions.outputs.connectionString
2248
2235
  output vnetEnabled bool = enableVNet
2249
- output vnetName string = enableVNet ? vnet.outputs.name : ''
2236
+ output vnetName string = enableVNet ? vnet!.outputs.name : ''
2250
2237
  `;
2251
2238
 
2252
2239
  fs.writeFileSync(path.join(infraDir, 'main.bicep'), mainBicep);
@@ -2262,6 +2249,9 @@ output vnetName string = enableVNet ? vnet.outputs.name : ''
2262
2249
  "cosmosDbMode": {
2263
2250
  "value": "${azureConfig.cosmosDbMode}"
2264
2251
  },
2252
+ "staticWebAppSku": {
2253
+ "value": "${staticWebAppSku}"
2254
+ },
2265
2255
  "enableVNet": {
2266
2256
  "value": ${enableVNet}
2267
2257
  }
@@ -2311,38 +2301,14 @@ resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
2311
2301
  }
2312
2302
  }
2313
2303
 
2314
- output id string = staticWebApp.id
2315
- output name string = staticWebApp.name
2316
- output defaultHostname string = staticWebApp.properties.defaultHostname
2317
- `;
2318
- fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
2319
-
2320
- // modules/staticwebapp-config.bicep (for updating config after Functions deployment)
2321
- const staticWebAppConfigBicep = `@description('Static Web App name')
2322
- param staticWebAppName string
2323
-
2324
- @description('Functions App default hostname for backend API calls')
2325
- param functionsDefaultHostname string
2326
-
2327
- @description('Application Insights connection string for SWA')
2328
- param appInsightsConnectionString string
2329
-
2330
- resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
2331
- name: staticWebAppName
2332
- }
2333
-
2334
- resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
2335
- parent: staticWebApp
2336
- name: 'appsettings'
2337
- properties: {
2338
- APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
2339
- ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
2340
- BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
2341
- }
2342
- }
2343
-
2344
- output configName string = staticWebAppConfig.name
2304
+ output id string = staticWebApp.id
2305
+ output name string = staticWebApp.name
2306
+ output defaultHostname string = staticWebApp.properties.defaultHostname
2345
2307
  `;
2308
+ fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
2309
+
2310
+ // modules/staticwebapp-config.bicep (for updating config after Functions deployment)
2311
+ const staticWebAppConfigBicep = buildStaticWebAppConfigBicepSource();
2346
2312
  fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
2347
2313
 
2348
2314
  // modules/loganalytics.bicep (Shared Log Analytics Workspace)
@@ -2565,63 +2531,7 @@ output principalId string = functionApp.identity.principalId
2565
2531
  fs.writeFileSync(path.join(modulesDir, 'functions-flex.bicep'), functionsFlexBicep);
2566
2532
 
2567
2533
  // modules/cosmosdb-freetier.bicep (Free Tier)
2568
- const cosmosDbFreeTierBicep = `@description('Cosmos DB account name')
2569
- param accountName string
2570
-
2571
- @description('Database name')
2572
- param databaseName string
2573
-
2574
- @description('Location for Cosmos DB')
2575
- param location string
2576
-
2577
- @description('Public network access')
2578
- @allowed(['Enabled', 'Disabled'])
2579
- param publicNetworkAccess string = 'Enabled'
2580
-
2581
- // Cosmos DB Account (Free Tier)
2582
- resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
2583
- name: accountName
2584
- location: location
2585
- kind: 'GlobalDocumentDB'
2586
- properties: {
2587
- databaseAccountOfferType: 'Standard'
2588
- enableAutomaticFailover: false
2589
- enableFreeTier: true
2590
- publicNetworkAccess: publicNetworkAccess
2591
- disableLocalAuth: true
2592
- consistencyPolicy: {
2593
- defaultConsistencyLevel: 'Session'
2594
- }
2595
- locations: [
2596
- {
2597
- locationName: location
2598
- failoverPriority: 0
2599
- isZoneRedundant: false
2600
- }
2601
- ]
2602
- disableKeyBasedMetadataWriteAccess: true
2603
- }
2604
- }
2605
-
2606
- // Cosmos DB Database
2607
- resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
2608
- parent: cosmosAccount
2609
- name: databaseName
2610
- properties: {
2611
- resource: {
2612
- id: databaseName
2613
- }
2614
- options: {
2615
- throughput: 1000
2616
- }
2617
- }
2618
- }
2619
-
2620
- output id string = cosmosAccount.id
2621
- output accountName string = cosmosAccount.name
2622
- output endpoint string = cosmosAccount.properties.documentEndpoint
2623
- output databaseName string = database.name
2624
- `;
2534
+ const cosmosDbFreeTierBicep = buildCosmosDbFreeTierBicepSource();
2625
2535
  fs.writeFileSync(path.join(modulesDir, 'cosmosdb-freetier.bicep'), cosmosDbFreeTierBicep);
2626
2536
 
2627
2537
  // modules/cosmosdb-serverless.bicep (Serverless)
@@ -2714,9 +2624,10 @@ output roleAssignmentId string = roleAssignment.id
2714
2624
  `;
2715
2625
  fs.writeFileSync(path.join(modulesDir, 'cosmosdb-role-assignment.bicep'), cosmosDbRoleAssignmentBicep);
2716
2626
 
2717
- // VNet modules (only generate if VNet is enabled)
2718
- if (enableVNet) {
2719
- // 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
2720
2631
  const vnetBicep = `@description('VNet name')
2721
2632
  param name string
2722
2633
 
@@ -2846,26 +2757,30 @@ output privateDnsZoneId string = privateDnsZone.id
2846
2757
  `;
2847
2758
  fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
2848
2759
 
2849
- console.log('✅ VNet modules created\n');
2850
- }
2760
+ console.log('✅ VNet module definitions created\n');
2851
2761
 
2852
2762
  console.log('✅ Infrastructure files created\n');
2853
2763
  }
2854
2764
 
2855
- function getGitHubFunctionsWorkflow(pm: PackageManager, backendLanguage: BackendLanguage): string {
2765
+ export function getGitHubFunctionsWorkflow(pm: PackageManager, backendLanguage: BackendLanguage, projectPm: PackageManager = pm): string {
2856
2766
  const pmCmd = getCommands(pm);
2857
2767
  const pnpmSetupStep = getCiSetupStep(pm);
2768
+ const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
2858
2769
 
2859
2770
  const commonSetup = ` - uses: actions/checkout@v4
2860
2771
 
2772
+ ${pnpmSetupStep ? `${pnpmSetupStep}\n` : ''}
2861
2773
  - name: Setup Node.js
2862
2774
  uses: actions/setup-node@v4
2863
2775
  with:
2864
2776
  node-version: '22'
2865
- ${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}` : ''}
2866
2781
  - name: Install dependencies
2867
2782
  run: |
2868
- ${pmCmd.ci}
2783
+ ${getCiInstallCommand(pm, projectPm)}
2869
2784
 
2870
2785
  - name: Build shared package
2871
2786
  run: |
@@ -2951,7 +2866,7 @@ jobs:
2951
2866
  ${commonSetup} - name: Setup .NET
2952
2867
  uses: actions/setup-dotnet@v4
2953
2868
  with:
2954
- dotnet-version: '8.0.x'
2869
+ dotnet-version: '10.0.x'
2955
2870
 
2956
2871
  - name: Publish Functions
2957
2872
  run: |
@@ -3012,16 +2927,18 @@ ${commonSetup} - name: Setup Python
3012
2927
  `;
3013
2928
  }
3014
2929
 
3015
- function getAzureFunctionsPipeline(pm: PackageManager, backendLanguage: BackendLanguage): string {
2930
+ export function getAzureFunctionsPipeline(pm: PackageManager, backendLanguage: BackendLanguage, projectPm: PackageManager = pm): string {
3016
2931
  const pmCmd = getCommands(pm);
3017
2932
  const azPipelinesSetup = getAzurePipelinesSetup(pm);
2933
+ const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
3018
2934
  const commonSetup = ` - task: NodeTool@0
3019
2935
  inputs:
3020
2936
  versionSpec: '22.x'
3021
2937
  displayName: 'Install Node.js'
3022
2938
  ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
2939
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
3023
2940
  - script: |
3024
- ${pmCmd.ci}
2941
+ ${getCiInstallCommand(pm, projectPm)}
3025
2942
  displayName: 'Install workspace dependencies'
3026
2943
 
3027
2944
  - script: |
@@ -3127,7 +3044,7 @@ variables:
3127
3044
  steps:
3128
3045
  ${commonSetup} - task: UseDotNet@2
3129
3046
  inputs:
3130
- version: '8.0.x'
3047
+ version: '10.0.x'
3131
3048
  displayName: 'Install .NET SDK'
3132
3049
 
3133
3050
  - script: |
@@ -3209,6 +3126,195 @@ ${commonSetup} - task: UsePythonVersion@0
3209
3126
  `;
3210
3127
  }
3211
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
+
3212
3318
  async function createGitHubActionsWorkflows(
3213
3319
  projectDir: string,
3214
3320
  azureConfig: AzureConfig,
@@ -3216,13 +3322,27 @@ async function createGitHubActionsWorkflows(
3216
3322
  backendLanguage: BackendLanguage
3217
3323
  ) {
3218
3324
  console.log('📦 Creating GitHub Actions workflows...\n');
3219
-
3220
- const pmCmd = getCommands(pm);
3221
3325
  const workflowsDir = path.join(projectDir, '.github', 'workflows');
3222
3326
  fs.mkdirSync(workflowsDir, { recursive: true });
3223
3327
 
3224
- // deploy-swa.yml
3225
- 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
3226
3346
 
3227
3347
  on:
3228
3348
  push:
@@ -3235,6 +3355,7 @@ on:
3235
3355
  - 'shared/**'
3236
3356
  - 'public/**'
3237
3357
  - 'package.json'
3358
+ - '${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
3238
3359
  - 'next.config.js'
3239
3360
  - 'next.config.ts'
3240
3361
  workflow_dispatch:
@@ -3248,6 +3369,7 @@ on:
3248
3369
  - 'shared/**'
3249
3370
  - 'public/**'
3250
3371
  - 'package.json'
3372
+ - '${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
3251
3373
  - 'next.config.js'
3252
3374
  - 'next.config.ts'
3253
3375
 
@@ -3260,6 +3382,22 @@ jobs:
3260
3382
  - uses: actions/checkout@v4
3261
3383
  with:
3262
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)}
3263
3401
 
3264
3402
  - name: Deploy to Azure Static Web Apps
3265
3403
  if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
@@ -3271,93 +3409,27 @@ jobs:
3271
3409
  app_location: '/'
3272
3410
  api_location: ''
3273
3411
  output_location: ''
3412
+ app_build_command: 'npm run build'
3274
3413
  env:
3275
3414
  NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
3276
3415
  `;
3277
- fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
3278
-
3279
- // deploy-functions.yml
3280
- const functionsWorkflow = getGitHubFunctionsWorkflow(pm, backendLanguage);
3281
- fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
3282
-
3283
- console.log('✅ GitHub Actions workflows created\n');
3284
3416
  }
3285
3417
 
3286
3418
  async function createAzurePipelines(projectDir: string, pm: PackageManager, backendLanguage: BackendLanguage) {
3287
3419
  console.log('📦 Creating Azure Pipelines...\n');
3288
-
3289
- const pmCmd = getCommands(pm);
3290
- const azPipelinesSetup = getAzurePipelinesSetup(pm);
3420
+
3421
+ const pipelinesPm: PackageManager = pm;
3291
3422
  const pipelinesDir = path.join(projectDir, 'pipelines');
3292
3423
  fs.mkdirSync(pipelinesDir, { recursive: true });
3293
3424
 
3294
3425
  // swa.yml
3295
- const swaPipeline = `trigger:
3296
- branches:
3297
- include:
3298
- - main
3299
- paths:
3300
- include:
3301
- - app/**
3302
- - components/**
3303
- - lib/**
3304
- - shared/**
3305
- - public/**
3306
- - package.json
3307
- - next.config.js
3308
-
3309
- pr:
3310
- branches:
3311
- include:
3312
- - main
3313
- paths:
3314
- include:
3315
- - app/**
3316
- - components/**
3317
- - lib/**
3318
- - shared/**
3319
- - public/**
3320
- - package.json
3321
- - next.config.js
3322
-
3323
- pool:
3324
- vmImage: 'ubuntu-latest'
3325
-
3326
- variables:
3327
- - group: azure-deployment
3328
-
3329
- steps:
3330
- - task: NodeTool@0
3331
- inputs:
3332
- versionSpec: '22.x'
3333
- displayName: 'Install Node.js'
3334
- ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
3335
- - script: |
3336
- ${pmCmd.ci}
3337
- displayName: 'Install dependencies'
3338
-
3339
- - script: |
3340
- ${pmCmd.run} build
3341
- env:
3342
- NODE_ENV: production
3343
- displayName: 'Build Next.js app'
3344
-
3345
- - task: AzureStaticWebApp@0
3346
- condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
3347
- inputs:
3348
- app_location: '.'
3349
- output_location: '.next/standalone'
3350
- skip_app_build: true
3351
- azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
3352
- displayName: 'Deploy to Azure Static Web Apps'
3353
- `;
3426
+ const swaPipeline = buildAzureSwaPipeline(pipelinesPm, pm);
3354
3427
  fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
3355
3428
 
3356
3429
  // functions.yml
3357
- const functionsPipeline = getAzureFunctionsPipeline(pm, backendLanguage);
3430
+ const functionsPipeline = getAzureFunctionsPipeline(pipelinesPm, backendLanguage, pm);
3358
3431
  fs.writeFileSync(path.join(pipelinesDir, 'functions.yml'), functionsPipeline);
3359
3432
 
3360
3433
  console.log('✅ Azure Pipelines created\n');
3361
3434
  }
3362
3435
 
3363
-