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
@@ -36,15 +36,41 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.getStaticWebAppSku = getStaticWebAppSku;
40
+ exports.buildCosmosDbFreeTierBicepSource = buildCosmosDbFreeTierBicepSource;
41
+ exports.buildSharedTsConfig = buildSharedTsConfig;
39
42
  exports.initCommand = initCommand;
43
+ exports.withNpmLockfileSafeManifests = withNpmLockfileSafeManifests;
44
+ exports.parseIgnoredBuilds = parseIgnoredBuilds;
40
45
  exports.injectSwallowKitNextConfig = injectSwallowKitNextConfig;
41
46
  exports.buildCSharpFunctionsProgramSource = buildCSharpFunctionsProgramSource;
42
47
  exports.buildCSharpFunctionsProjectSource = buildCSharpFunctionsProjectSource;
48
+ exports.buildSwallowKitConfigSource = buildSwallowKitConfigSource;
49
+ exports.buildGeneratedProjectDependencies = buildGeneratedProjectDependencies;
50
+ exports.buildGeneratedProjectDevDependencies = buildGeneratedProjectDevDependencies;
51
+ exports.buildSwallowKitMcpProjectConfigSource = buildSwallowKitMcpProjectConfigSource;
52
+ exports.buildTypeScriptFunctionsPackageJson = buildTypeScriptFunctionsPackageJson;
53
+ exports.buildFunctionsHostKeyBicepExpression = buildFunctionsHostKeyBicepExpression;
54
+ exports.buildStaticWebAppConfigBicepSource = buildStaticWebAppConfigBicepSource;
55
+ exports.createInfrastructure = createInfrastructure;
56
+ exports.getGitHubFunctionsWorkflow = getGitHubFunctionsWorkflow;
57
+ exports.getAzureFunctionsPipeline = getAzureFunctionsPipeline;
58
+ exports.getSwaNodeVersionCommand = getSwaNodeVersionCommand;
59
+ exports.buildAzureSwaPipeline = buildAzureSwaPipeline;
60
+ exports.buildGitHubSwaWorkflow = buildGitHubSwaWorkflow;
43
61
  const fs = __importStar(require("fs"));
44
62
  const path = __importStar(require("path"));
45
63
  const child_process_1 = require("child_process");
46
64
  const prompts_1 = __importDefault(require("prompts"));
47
65
  const package_manager_1 = require("../../utils/package-manager");
66
+ const manifest_1 = require("../../core/project/manifest");
67
+ const workflows_1 = require("../../core/project/workflows");
68
+ const agent_instructions_1 = require("../../core/project/agent-instructions");
69
+ const native_schema_generator_1 = require("../../core/scaffold/native-schema-generator");
70
+ const python_uv_1 = require("../../utils/python-uv");
71
+ function getStaticWebAppSku(plan) {
72
+ return plan === 'free' ? 'Free' : 'Standard';
73
+ }
48
74
  const BACKEND_LANGUAGE_CHOICES = [
49
75
  { title: "TypeScript", value: "typescript" },
50
76
  { title: "C#", value: "csharp" },
@@ -56,6 +82,65 @@ function usesNodeFunctionsProject(backendLanguage) {
56
82
  function getBackendLanguageLabel(backendLanguage) {
57
83
  return BACKEND_LANGUAGE_CHOICES.find((choice) => choice.value === backendLanguage)?.title || backendLanguage;
58
84
  }
85
+ function buildCosmosDbFreeTierBicepSource() {
86
+ return `@description('Cosmos DB account name')
87
+ param accountName string
88
+
89
+ @description('Database name')
90
+ param databaseName string
91
+
92
+ @description('Location for Cosmos DB')
93
+ param location string
94
+
95
+ @description('Public network access')
96
+ @allowed(['Enabled', 'Disabled'])
97
+ param publicNetworkAccess string = 'Enabled'
98
+
99
+ // Cosmos DB Account (Free Tier)
100
+ resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
101
+ name: accountName
102
+ location: location
103
+ kind: 'GlobalDocumentDB'
104
+ properties: {
105
+ databaseAccountOfferType: 'Standard'
106
+ enableAutomaticFailover: false
107
+ enableFreeTier: true
108
+ publicNetworkAccess: publicNetworkAccess
109
+ disableLocalAuth: true
110
+ consistencyPolicy: {
111
+ defaultConsistencyLevel: 'Session'
112
+ }
113
+ locations: [
114
+ {
115
+ locationName: location
116
+ failoverPriority: 0
117
+ isZoneRedundant: false
118
+ }
119
+ ]
120
+ disableKeyBasedMetadataWriteAccess: true
121
+ }
122
+ }
123
+
124
+ // Cosmos DB Database
125
+ resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
126
+ parent: cosmosAccount
127
+ name: databaseName
128
+ properties: {
129
+ resource: {
130
+ id: databaseName
131
+ }
132
+ options: {
133
+ throughput: 1000
134
+ }
135
+ }
136
+ }
137
+
138
+ output id string = cosmosAccount.id
139
+ output accountName string = cosmosAccount.name
140
+ output endpoint string = cosmosAccount.properties.documentEndpoint
141
+ output databaseName string = database.name
142
+ `;
143
+ }
59
144
  function getFunctionsWorkerRuntime(backendLanguage) {
60
145
  if (backendLanguage === "csharp") {
61
146
  return "dotnet-isolated";
@@ -67,13 +152,34 @@ function getFunctionsWorkerRuntime(backendLanguage) {
67
152
  }
68
153
  function getFunctionsRuntimeConfig(backendLanguage) {
69
154
  if (backendLanguage === "csharp") {
70
- return { name: "dotnet-isolated", version: "8.0" };
155
+ return { name: "dotnet-isolated", version: "10.0" };
71
156
  }
72
157
  if (backendLanguage === "python") {
73
158
  return { name: "python", version: "3.11" };
74
159
  }
75
160
  return { name: "node", version: "22" };
76
161
  }
162
+ function buildSharedTsConfig() {
163
+ return {
164
+ compilerOptions: {
165
+ target: "ES2020",
166
+ module: "Node16",
167
+ moduleResolution: "node16",
168
+ lib: ["ES2020"],
169
+ outDir: "dist",
170
+ rootDir: ".",
171
+ declaration: true,
172
+ declarationMap: true,
173
+ sourceMap: true,
174
+ strict: true,
175
+ esModuleInterop: true,
176
+ skipLibCheck: true,
177
+ forceConsistentCasingInFileNames: true,
178
+ },
179
+ include: ["index.ts", "models/**/*"],
180
+ exclude: ["node_modules", "dist"],
181
+ };
182
+ }
77
183
  async function promptBackendLanguage() {
78
184
  const response = await (0, prompts_1.default)({
79
185
  type: "select",
@@ -98,37 +204,64 @@ async function promptCiCd() {
98
204
  });
99
205
  return response.cicd || 'skip';
100
206
  }
101
- async function promptAzureConfig() {
102
- const cosmosResponse = await (0, prompts_1.default)({
103
- type: 'select',
104
- name: 'mode',
105
- message: 'Cosmos DB mode (affects cost):',
106
- choices: [
107
- { title: 'Free Tier (1000 RU/s free, best for first project)', value: 'freetier' },
108
- { title: 'Serverless (pay-per-use, flexible)', value: 'serverless' }
109
- ],
110
- initial: 0
111
- });
112
- const vnetResponse = await (0, prompts_1.default)({
113
- type: 'select',
114
- name: 'vnet',
115
- message: 'Network security:',
116
- choices: [
117
- { title: 'VNet Integration (recommended) - Cosmos DB via Private Endpoint', value: 'outbound' },
118
- { title: 'None - Public endpoints, simplest but least secure', value: 'none' }
119
- ],
120
- initial: 0
121
- });
207
+ async function promptAzureConfig(options) {
208
+ let swaPlan = options.swaPlan;
209
+ if (!swaPlan) {
210
+ const response = await (0, prompts_1.default)({
211
+ type: 'select',
212
+ name: 'plan',
213
+ message: 'Azure Static Web Apps plan:',
214
+ choices: [
215
+ { title: 'Standard (production, SLA and advanced auth/networking features)', value: 'standard' },
216
+ { title: 'Free (personal projects, no SLA and lower quotas)', value: 'free' }
217
+ ],
218
+ initial: 0
219
+ });
220
+ swaPlan = response.plan || 'standard';
221
+ }
222
+ let cosmosDbMode = options.cosmosDbMode;
223
+ if (!cosmosDbMode) {
224
+ const response = await (0, prompts_1.default)({
225
+ type: 'select',
226
+ name: 'mode',
227
+ message: 'Cosmos DB mode (affects cost):',
228
+ choices: [
229
+ { title: 'Free Tier (1000 RU/s free, best for first project)', value: 'freetier' },
230
+ { title: 'Serverless (pay-per-use, flexible)', value: 'serverless' }
231
+ ],
232
+ initial: 0
233
+ });
234
+ cosmosDbMode = response.mode || 'freetier';
235
+ }
236
+ let vnetOption = options.vnet;
237
+ if (!vnetOption) {
238
+ const response = await (0, prompts_1.default)({
239
+ type: 'select',
240
+ name: 'vnet',
241
+ message: 'Network security:',
242
+ choices: [
243
+ { title: 'VNet Integration (recommended) - Cosmos DB via Private Endpoint', value: 'outbound' },
244
+ { title: 'None - Public endpoints, simplest but least secure', value: 'none' }
245
+ ],
246
+ initial: 0
247
+ });
248
+ vnetOption = response.vnet || 'outbound';
249
+ }
122
250
  return {
123
- cosmosDbMode: cosmosResponse.mode || 'freetier',
124
- vnetOption: vnetResponse.vnet || 'outbound'
251
+ cosmosDbMode: cosmosDbMode || 'freetier',
252
+ vnetOption: vnetOption || 'outbound',
253
+ swaPlan: swaPlan || 'standard'
125
254
  };
126
255
  }
127
256
  const VALID_CICD = ['github', 'azure', 'skip'];
128
257
  const VALID_BACKEND_LANGUAGE = ['typescript', 'csharp', 'python'];
129
258
  const VALID_COSMOS_DB_MODE = ['freetier', 'serverless'];
130
259
  const VALID_VNET = ['none', 'outbound'];
260
+ const VALID_SWA_PLAN = ['free', 'standard'];
131
261
  function validateInitFlags(options) {
262
+ if (options.packageManager && !['npm', 'pnpm'].includes(options.packageManager)) {
263
+ throw new Error('Invalid --package-manager: expected npm or pnpm');
264
+ }
132
265
  if (options.cicd && !VALID_CICD.includes(options.cicd)) {
133
266
  console.error(`❌ Invalid --cicd value: "${options.cicd}". Must be: ${VALID_CICD.join(', ')}`);
134
267
  process.exit(1);
@@ -145,6 +278,10 @@ function validateInitFlags(options) {
145
278
  console.error(`❌ Invalid --vnet value: "${options.vnet}". Must be: ${VALID_VNET.join(', ')}`);
146
279
  process.exit(1);
147
280
  }
281
+ if (options.swaPlan && !VALID_SWA_PLAN.includes(options.swaPlan)) {
282
+ console.error(`❌ Invalid --swa-plan value: "${options.swaPlan}". Must be: ${VALID_SWA_PLAN.join(', ')}`);
283
+ process.exit(1);
284
+ }
148
285
  }
149
286
  async function initCommand(options) {
150
287
  // Validate flag values before doing anything
@@ -152,7 +289,7 @@ async function initCommand(options) {
152
289
  console.log(`🚀 Initializing SwallowKit project: ${options.name}`);
153
290
  console.log(`📋 Template: ${options.template}`);
154
291
  // Detect package manager from invocation context (npx → npm, pnpm dlx → pnpm)
155
- const pm = (0, package_manager_1.detectFromUserAgent)();
292
+ const pm = options.packageManager || (0, package_manager_1.detectFromUserAgent)();
156
293
  const pmCmd = (0, package_manager_1.getCommands)(pm);
157
294
  console.log(`📦 Package manager: ${pm}`);
158
295
  const projectDir = path.join(process.cwd(), options.name);
@@ -162,12 +299,30 @@ async function initCommand(options) {
162
299
  console.error(`❌ Directory "${options.name}" already exists.`);
163
300
  process.exit(1);
164
301
  }
302
+ // 非対話環境でプロンプトに入るとハングするため、不足フラグを明示エラーで案内する
303
+ const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true && !process.env.CI;
304
+ if (!interactive) {
305
+ const missing = [];
306
+ if (!options.cicd)
307
+ missing.push('--cicd <github|azure|skip>');
308
+ if (!options.backendLanguage)
309
+ missing.push('--backend-language <typescript|csharp|python>');
310
+ if (!options.cosmosDbMode)
311
+ missing.push('--cosmos-db-mode <freetier|serverless>');
312
+ if (!options.vnet)
313
+ missing.push('--vnet <outbound|none>');
314
+ if (!options.swaPlan)
315
+ missing.push('--swa-plan <free|standard>');
316
+ if (missing.length > 0) {
317
+ console.error(`❌ Non-interactive environment detected. Pass all configuration flags to run init without prompts:\n` +
318
+ missing.map((flag) => ` ${flag}`).join('\n'));
319
+ process.exit(1);
320
+ }
321
+ }
165
322
  // Use flag values if provided, otherwise prompt interactively
166
323
  const cicdProvider = options.cicd || await promptCiCd();
167
324
  const backendLanguage = options.backendLanguage || await promptBackendLanguage();
168
- const azureConfig = (options.cosmosDbMode && options.vnet)
169
- ? { cosmosDbMode: options.cosmosDbMode, vnetOption: options.vnet }
170
- : await promptAzureConfig();
325
+ const azureConfig = await promptAzureConfig(options);
171
326
  // Create Next.js project with create-next-app
172
327
  await createNextJsProject(options.name, pm);
173
328
  // Upgrade Next.js to specified version (or latest) to avoid cached old versions
@@ -183,6 +338,7 @@ async function initCommand(options) {
183
338
  else if (cicdProvider === 'azure') {
184
339
  await createAzurePipelines(projectDir, pm, backendLanguage);
185
340
  }
341
+ await (0, manifest_1.syncProjectManifest)(projectDir);
186
342
  // Initialize Git repository and create initial commit
187
343
  try {
188
344
  // Try git init with -b main (Git 2.28+), fallback to git init
@@ -226,11 +382,11 @@ async function initCommand(options) {
226
382
  console.log(`\n✅ Project "${options.name}" created successfully!`);
227
383
  console.log("\n📝 Next steps:");
228
384
  console.log(` cd ${options.name}`);
229
- console.log(` ${pmCmd.dlx} swallowkit create-model <name> # Create your first model`);
230
- console.log(` ${pmCmd.dlx} swallowkit scaffold shared/models/<name>.ts # Generate CRUD code`);
231
- console.log(` ${pmCmd.dlx} swallowkit dev # Start development servers`);
385
+ console.log(` ${pmCmd.exec} swallowkit create-model <name> # Create your first model`);
386
+ console.log(` ${pmCmd.exec} swallowkit scaffold shared/models/<name>.ts # Generate CRUD code`);
387
+ console.log(` ${pmCmd.exec} swallowkit dev # Start development servers`);
232
388
  console.log("\n🚀 Deploy to Azure:");
233
- console.log(` ${pmCmd.dlx} swallowkit provision --resource-group <name>`);
389
+ console.log(` ${pmCmd.exec} swallowkit provision --resource-group <name>`);
234
390
  if (cicdProvider !== 'skip') {
235
391
  console.log(" Configure CI/CD secrets and push to repository");
236
392
  }
@@ -252,7 +408,7 @@ async function createNextJsProject(projectName, pm) {
252
408
  // for npm use "npx create-next-app@latest ..."
253
409
  const baseArgs = pm === 'pnpm'
254
410
  ? ['dlx', 'create-next-app@latest']
255
- : ['create-next-app@latest'];
411
+ : ['--yes', '--package', 'create-next-app@latest', '--', 'create-next-app'];
256
412
  const args = [
257
413
  ...baseArgs,
258
414
  projectName,
@@ -261,6 +417,7 @@ async function createNextJsProject(projectName, pm) {
261
417
  '--app',
262
418
  '--no-src',
263
419
  '--disable-git',
420
+ '--skip-install',
264
421
  '--import-alias',
265
422
  '@/*',
266
423
  ...(pmCmd.createNextAppFlag ? [pmCmd.createNextAppFlag] : []),
@@ -286,65 +443,164 @@ async function createNextJsProject(projectName, pm) {
286
443
  });
287
444
  }
288
445
  async function upgradeNextJs(projectDir, version, pm) {
289
- return new Promise((resolve, reject) => {
290
- console.log(`\n📦 Installing Next.js ${version} (to ensure latest security patches)...\n`);
291
- // pnpm: pnpm add next@... ; npm: npm install next@...
292
- const args = pm === 'pnpm'
293
- ? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact']
294
- : ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact'];
295
- const child = (0, child_process_1.spawn)(pm, args, {
296
- cwd: projectDir,
297
- stdio: 'inherit',
298
- shell: true,
299
- });
300
- child.on('close', (code) => {
301
- if (code !== 0) {
302
- reject(new Error(`${pm} add next@${version} exited with code ${code}`));
446
+ console.log(`\n📦 Installing Next.js ${version} (to ensure latest security patches)...\n`);
447
+ // pnpm: pnpm add next@... ; npm: npm install next@...
448
+ const args = pm === 'pnpm'
449
+ ? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact', '--lockfile-only']
450
+ : ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact', '--package-lock-only'];
451
+ await runPackageManagerCommand(pm, args, projectDir, `${pm} add next@${version}`);
452
+ console.log(`\n✅ Next.js ${version} installed\n`);
453
+ }
454
+ async function installDependencies(projectDir, pm = 'pnpm') {
455
+ console.log('\n📦 Installing dependencies...\n');
456
+ await runPackageManagerCommand(pm, pm === 'pnpm' ? ['install', '--no-frozen-lockfile'] : ['install'], projectDir, `${pm} install`);
457
+ console.log('\n✅ Dependencies installed\n');
458
+ }
459
+ async function withNpmLockfileSafeManifests(projectDir, action) {
460
+ const packageJsonPath = path.join(projectDir, 'package.json');
461
+ const functionsPackageJsonPath = path.join(projectDir, 'functions', 'package.json');
462
+ const sharedPackageJsonPath = path.join(projectDir, 'shared', 'package.json');
463
+ const originalFiles = new Map();
464
+ const hiddenNodeModules = new Map();
465
+ function rememberOriginal(filePath) {
466
+ if (!originalFiles.has(filePath)) {
467
+ originalFiles.set(filePath, fs.readFileSync(filePath, 'utf-8'));
468
+ }
469
+ }
470
+ function readJson(filePath) {
471
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
472
+ }
473
+ function writeJson(filePath, value) {
474
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
475
+ }
476
+ const sharedPackageName = fs.existsSync(sharedPackageJsonPath)
477
+ ? readJson(sharedPackageJsonPath).name
478
+ : undefined;
479
+ function replaceSharedWorkspaceDependency(packageJson, replacement) {
480
+ let changed = false;
481
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
482
+ const dependencies = packageJson[section];
483
+ if (!dependencies)
484
+ continue;
485
+ for (const [name, version] of Object.entries(dependencies)) {
486
+ if (version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
487
+ dependencies[name] = replacement;
488
+ changed = true;
489
+ }
303
490
  }
304
- else {
305
- console.log(`\n✅ Next.js ${version} installed\n`);
306
- resolve();
491
+ }
492
+ return changed;
493
+ }
494
+ function normalizePackageJson(filePath, replacement) {
495
+ if (!fs.existsSync(filePath))
496
+ return;
497
+ const packageJson = readJson(filePath);
498
+ if (!replaceSharedWorkspaceDependency(packageJson, replacement))
499
+ return;
500
+ rememberOriginal(filePath);
501
+ writeJson(filePath, packageJson);
502
+ }
503
+ function hideNodeModules(directory) {
504
+ const nodeModulesPath = path.join(directory, 'node_modules');
505
+ if (!fs.existsSync(nodeModulesPath))
506
+ return;
507
+ const hiddenPath = path.join(directory, `.node_modules.swallowkit-npm-lockfile-${process.pid}`);
508
+ fs.renameSync(nodeModulesPath, hiddenPath);
509
+ hiddenNodeModules.set(nodeModulesPath, hiddenPath);
510
+ }
511
+ try {
512
+ normalizePackageJson(packageJsonPath, 'file:shared');
513
+ normalizePackageJson(functionsPackageJsonPath, 'file:../shared');
514
+ hideNodeModules(projectDir);
515
+ hideNodeModules(path.join(projectDir, 'shared'));
516
+ hideNodeModules(path.join(projectDir, 'functions'));
517
+ await action();
518
+ }
519
+ finally {
520
+ for (const [nodeModulesPath, hiddenPath] of hiddenNodeModules) {
521
+ if (fs.existsSync(nodeModulesPath)) {
522
+ fs.rmSync(nodeModulesPath, { recursive: true, force: true });
307
523
  }
308
- });
309
- child.on('error', (error) => {
310
- reject(error);
311
- });
312
- });
524
+ fs.renameSync(hiddenPath, nodeModulesPath);
525
+ }
526
+ for (const [filePath, content] of originalFiles) {
527
+ fs.writeFileSync(filePath, content);
528
+ }
529
+ }
313
530
  }
314
- async function installDependencies(projectDir, pm = 'pnpm') {
531
+ /** Run installs without changing the project security policy. */
532
+ async function runPackageManagerCommand(pm, args, projectDir, label) {
533
+ const { code, output } = await spawnAndCapture(pm, args, projectDir, pm === 'pnpm');
534
+ if (code === 0)
535
+ return;
536
+ if (pm === 'pnpm') {
537
+ if (parseIgnoredBuilds(output).length > 0) {
538
+ throw new Error(`${label} exited with code ${code}: pnpm still reports unapproved build scripts. ` +
539
+ 'Review the named dependencies and project security policy before allowing any scripts. SwallowKit does not auto-approve them.');
540
+ }
541
+ }
542
+ throw new Error(`${label} exited with code ${code}`);
543
+ }
544
+ /**
545
+ * Spawn a child process inheriting stdin (so interactive prompts still work)
546
+ * while either inheriting or capturing+teeing stdout/stderr.
547
+ */
548
+ function spawnAndCapture(pm, args, projectDir, capture) {
315
549
  return new Promise((resolve, reject) => {
316
- console.log('\n📦 Installing dependencies...\n');
317
- const child = (0, child_process_1.spawn)(pm, ['install'], {
550
+ const stdio = capture
551
+ ? ['inherit', 'pipe', 'pipe']
552
+ : ['inherit', 'inherit', 'inherit'];
553
+ const child = (0, child_process_1.spawn)(pm, args, {
318
554
  cwd: projectDir,
319
- stdio: 'inherit',
555
+ stdio,
320
556
  shell: true,
321
557
  });
322
- child.on('close', (code) => {
323
- if (code !== 0) {
324
- reject(new Error(`${pm} install exited with code ${code}`));
325
- }
326
- else {
327
- console.log('\n✅ Dependencies installed\n');
328
- resolve();
329
- }
330
- });
331
- child.on('error', (error) => {
332
- reject(error);
333
- });
558
+ let combined = '';
559
+ if (capture) {
560
+ child.stdout?.on('data', (chunk) => {
561
+ process.stdout.write(chunk);
562
+ combined += chunk.toString('utf8');
563
+ });
564
+ child.stderr?.on('data', (chunk) => {
565
+ process.stderr.write(chunk);
566
+ combined += chunk.toString('utf8');
567
+ });
568
+ }
569
+ child.on('close', (code) => resolve({ code: code ?? 0, output: combined }));
570
+ child.on('error', reject);
334
571
  });
335
572
  }
573
+ /**
574
+ * Parse pnpm's `Ignored build scripts: foo@1.0.0, bar@2.0.0` warning/error and
575
+ * return the bare package names (without versions), de-duplicated.
576
+ */
577
+ function parseIgnoredBuilds(output) {
578
+ const match = output.match(/Ignored build scripts:\s*([^\n\r]+)/i);
579
+ if (!match)
580
+ return [];
581
+ const list = match[1]
582
+ .split(',')
583
+ .map((entry) => entry.trim())
584
+ // strip version suffix: `sharp@0.34.5` -> `sharp`, `@scope/pkg@1.0.0` -> `@scope/pkg`
585
+ .map((entry) => entry.replace(/@[^@]+$/, ''))
586
+ .filter((name) => name.length > 0);
587
+ return Array.from(new Set(list));
588
+ }
336
589
  function injectSwallowKitNextConfig(nextConfigContent, projectName) {
337
- return nextConfigContent.replace(/(const\s+nextConfig[:\s]*(?::\s*NextConfig\s*)?=\s*\{)(\s*\/\*[^*]*\*\/)?/, `$1\n output: 'standalone',\n transpilePackages: ['@${projectName}/shared'],\n serverExternalPackages: ['applicationinsights', 'diagnostic-channel-publishers'],$2`);
590
+ return nextConfigContent.replace(/(const\s+nextConfig[:\s]*(?::\s*NextConfig\s*)?=\s*\{)(\s*\/\*[^*]*\*\/)?/, `$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`);
338
591
  }
339
592
  function buildCSharpFunctionsProgramSource() {
340
- return `using Microsoft.Extensions.DependencyInjection;
593
+ return `using Microsoft.Azure.Functions.Worker;
594
+ using Microsoft.Extensions.DependencyInjection;
341
595
  using Microsoft.Extensions.Hosting;
596
+ using Microsoft.Azure.Functions.Worker.ApplicationInsights;
342
597
 
343
598
  var host = new HostBuilder()
344
599
  .ConfigureFunctionsWorkerDefaults()
345
600
  .ConfigureServices(services =>
346
601
  {
347
602
  services.AddApplicationInsightsTelemetryWorkerService();
603
+ services.ConfigureFunctionsApplicationInsights();
348
604
  })
349
605
  .Build();
350
606
 
@@ -354,7 +610,7 @@ host.Run();
354
610
  function buildCSharpFunctionsProjectSource() {
355
611
  return `<Project Sdk="Microsoft.NET.Sdk">
356
612
  <PropertyGroup>
357
- <TargetFramework>net8.0</TargetFramework>
613
+ <TargetFramework>net10.0</TargetFramework>
358
614
  <AzureFunctionsVersion>v4</AzureFunctionsVersion>
359
615
  <OutputType>Exe</OutputType>
360
616
  <ImplicitUsings>enable</ImplicitUsings>
@@ -368,62 +624,117 @@ function buildCSharpFunctionsProjectSource() {
368
624
  <ItemGroup>
369
625
  <PackageReference Include="Microsoft.Azure.Cosmos" Version="3.47.0" />
370
626
  <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
371
- <PackageReference Include="Azure.Identity" Version="1.13.2" />
372
- <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.23.0" />
373
- <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.2.0" />
374
- <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.18.0" OutputItemType="Analyzer" />
627
+ <PackageReference Include="Azure.Identity" Version="1.17.0" />
628
+ <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" />
629
+ <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
630
+ <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" OutputItemType="Analyzer" />
375
631
  <PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" />
376
- <PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="1.2.0" />
632
+ <PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
377
633
  </ItemGroup>
378
634
  </Project>
379
635
  `;
380
636
  }
637
+ function buildSwallowKitConfigSource(backendLanguage) {
638
+ return `module.exports = {
639
+ backend: {
640
+ language: '${backendLanguage}',
641
+ },
642
+ functions: {
643
+ baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
644
+ },
645
+ deployment: {
646
+ resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
647
+ swaName: process.env.AZURE_SWA_NAME || '',
648
+ },
649
+ }
650
+ `;
651
+ }
652
+ function buildGeneratedProjectDependencies(projectName, pm = 'pnpm') {
653
+ return {
654
+ '@azure/cosmos': '^4.0.0',
655
+ 'applicationinsights': '^3.3.0',
656
+ 'zod': '^4.0.0',
657
+ [`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:shared',
658
+ };
659
+ }
660
+ function getSwallowKitPackageMetadata() {
661
+ const packageJsonPath = path.resolve(__dirname, "..", "..", "..", "package.json");
662
+ try {
663
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
664
+ return {
665
+ name: packageJson.name || "swallowkit",
666
+ version: packageJson.version || "latest",
667
+ };
668
+ }
669
+ catch {
670
+ // Fall back to the published package name when package metadata is unavailable.
671
+ }
672
+ return {
673
+ name: "swallowkit",
674
+ version: "latest",
675
+ };
676
+ }
677
+ function buildGeneratedProjectDevDependencies() {
678
+ const { name, version } = getSwallowKitPackageMetadata();
679
+ return {
680
+ [name]: version,
681
+ };
682
+ }
683
+ function buildSwallowKitMcpProjectConfigSource(pm = 'pnpm') {
684
+ const { name } = getSwallowKitPackageMetadata();
685
+ return JSON.stringify({
686
+ mcpServers: {
687
+ swallowkit: {
688
+ command: pm === 'pnpm' ? 'pnpm' : 'npx',
689
+ args: pm === 'pnpm'
690
+ ? ["dlx", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"]
691
+ : ["--yes", "--package", `${name}@\${SWALLOWKIT_MCP_VERSION}`, "swallowkit-mcp"],
692
+ cwd: ".",
693
+ env: {
694
+ SWALLOWKIT_MCP_VERSION: "latest",
695
+ },
696
+ },
697
+ },
698
+ }, null, 2);
699
+ }
381
700
  async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig, pm, backendLanguage) {
382
701
  console.log('📦 Adding SwallowKit files...\n');
383
702
  const projectName = options.name;
384
- // 1. Update package.json to add swallowkit and @azure/cosmos dependencies
703
+ // 1. Update package.json to add runtime dependencies for generated projects
385
704
  const packageJsonPath = path.join(projectDir, 'package.json');
386
705
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
387
- // Add SwallowKit dependencies (Next.js version already upgraded by upgradeNextJs)
388
- // zod is in the shared workspace package, not here
706
+ // Root BFF/auth code imports Zod directly; declare it as well as the shared package.
389
707
  packageJson.dependencies = {
390
708
  ...packageJson.dependencies,
391
- 'swallowkit': 'latest',
392
- '@azure/cosmos': '^4.0.0',
393
- 'applicationinsights': '^3.3.0',
394
- [`@${projectName}/shared`]: '*',
709
+ ...buildGeneratedProjectDependencies(projectName, pm),
710
+ };
711
+ packageJson.devDependencies = {
712
+ ...packageJson.devDependencies,
713
+ ...buildGeneratedProjectDevDependencies(),
395
714
  };
396
- if (backendLanguage !== "typescript") {
397
- packageJson.devDependencies = {
398
- ...packageJson.devDependencies,
399
- '@openapitools/openapi-generator-cli': '^2.21.0',
400
- };
401
- }
402
715
  packageJson.scripts = {
403
716
  ...packageJson.scripts,
404
717
  'build': (0, package_manager_1.getBuildScript)(pm),
405
718
  'start': 'next start',
406
719
  'functions:start': (0, package_manager_1.getFunctionsStartScript)(pm, backendLanguage),
407
720
  };
408
- if (pm === 'pnpm') {
409
- packageJson.packageManager = 'pnpm@latest';
410
- }
411
721
  packageJson.engines = {
412
- node: '20.x',
722
+ node: '>=22.14.0',
723
+ ...(pm === 'pnpm' ? { pnpm: '>=11 <13' } : {}),
413
724
  };
414
- // Workspace configuration depends on package manager
725
+ packageJson.devEngines = {
726
+ packageManager: { name: pm, version: pm === 'pnpm' ? '>=11 <13' : '>=10', onFail: 'error' },
727
+ };
728
+ delete packageJson.packageManager;
729
+ // Keep package.json workspaces for npm-based CI/CD even when local development uses pnpm.
415
730
  const workspacePackages = usesNodeFunctionsProject(backendLanguage) ? ['shared', 'functions'] : ['shared'];
416
731
  const wsConfig = (0, package_manager_1.getWorkspaceConfig)(pm, workspacePackages);
417
- if (wsConfig.type === 'file') {
418
- // pnpm: workspaces are defined in pnpm-workspace.yaml
419
- delete packageJson.workspaces;
420
- }
421
- else {
422
- // npm: workspaces are defined in package.json
732
+ packageJson.workspaces = workspacePackages;
733
+ if (wsConfig.type !== 'file') {
423
734
  packageJson.workspaces = wsConfig.value;
424
735
  }
425
736
  fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
426
- // Create workspace config file if needed (pnpm-workspace.yaml)
737
+ // Create pnpm-workspace.yaml for local pnpm development when needed.
427
738
  if (wsConfig.type === 'file') {
428
739
  fs.writeFileSync(path.join(projectDir, wsConfig.filename), wsConfig.content);
429
740
  }
@@ -458,20 +769,7 @@ async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig,
458
769
  fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
459
770
  }
460
771
  // 3. Create SwallowKit config
461
- const swallowkitConfig = `/** @type {import('swallowkit').SwallowKitConfig} */
462
- module.exports = {
463
- backend: {
464
- language: '${backendLanguage}',
465
- },
466
- functions: {
467
- baseUrl: process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071',
468
- },
469
- deployment: {
470
- resourceGroup: process.env.AZURE_RESOURCE_GROUP || '',
471
- swaName: process.env.AZURE_SWA_NAME || '',
472
- },
473
- }
474
- `;
772
+ const swallowkitConfig = buildSwallowKitConfigSource(backendLanguage);
475
773
  fs.writeFileSync(path.join(projectDir, 'swallowkit.config.js'), swallowkitConfig);
476
774
  // 4. Create shared workspace package for Zod models (Single Source of Truth)
477
775
  await createSharedPackage(projectDir, projectName);
@@ -486,6 +784,13 @@ function getFunctionsBaseUrl(): string {
486
784
  return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
487
785
  }
488
786
 
787
+ function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string> {
788
+ const key = process.env.BACKEND_FUNCTIONS_KEY;
789
+ if (key) return { 'x-functions-key': key };
790
+ if (functionsBaseUrl.startsWith('http://localhost:') || functionsBaseUrl.startsWith('http://127.0.0.1:')) return {};
791
+ throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
792
+ }
793
+
489
794
  /**
490
795
  * Simple HTTP client for calling backend APIs
491
796
  * Use this to make requests to BFF API routes (which forward to Azure Functions)
@@ -493,10 +798,11 @@ function getFunctionsBaseUrl(): string {
493
798
  async function request<T>(
494
799
  endpoint: string,
495
800
  method: 'GET' | 'POST' | 'PUT' | 'DELETE',
496
- body?: any,
801
+ body?: unknown,
497
802
  queryParams?: Record<string, string>
498
803
  ): Promise<T> {
499
804
  const functionsBaseUrl = getFunctionsBaseUrl();
805
+ const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
500
806
  let url = \`\${functionsBaseUrl}\${endpoint}\`;
501
807
  if (queryParams) {
502
808
  const params = new URLSearchParams(queryParams);
@@ -508,6 +814,7 @@ async function request<T>(
508
814
  method,
509
815
  headers: {
510
816
  'Content-Type': 'application/json',
817
+ ...keyHeaders,
511
818
  },
512
819
  body: body ? JSON.stringify(body) : undefined,
513
820
  });
@@ -563,14 +870,14 @@ export const api = {
563
870
  /**
564
871
  * Make a POST request
565
872
  */
566
- post: <T>(endpoint: string, body?: any): Promise<T> => {
873
+ post: <T>(endpoint: string, body?: unknown): Promise<T> => {
567
874
  return request<T>(endpoint, 'POST', body);
568
875
  },
569
876
 
570
877
  /**
571
878
  * Make a PUT request
572
879
  */
573
- put: <T>(endpoint: string, body?: any): Promise<T> => {
880
+ put: <T>(endpoint: string, body?: unknown): Promise<T> => {
574
881
  return request<T>(endpoint, 'PUT', body);
575
882
  },
576
883
 
@@ -589,6 +896,7 @@ export const api = {
589
896
  // 6. Create .env.example
590
897
  const envExample = `# Azure Functions Backend URL
591
898
  BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071
899
+ BACKEND_FUNCTIONS_KEY=
592
900
 
593
901
  # Azure Configuration
594
902
  AZURE_RESOURCE_GROUP=your-resource-group
@@ -627,7 +935,7 @@ export async function register() {
627
935
  const originalConsoleError = console.error;
628
936
  const originalConsoleWarn = console.warn;
629
937
 
630
- console.log = function(...args: any[]) {
938
+ console.log = function(...args: unknown[]) {
631
939
  originalConsoleLog.apply(console, args);
632
940
  const message = args.map(arg =>
633
941
  typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
@@ -638,7 +946,7 @@ export async function register() {
638
946
  });
639
947
  };
640
948
 
641
- console.error = function(...args: any[]) {
949
+ console.error = function(...args: unknown[]) {
642
950
  originalConsoleError.apply(console, args);
643
951
  const message = args.map(arg =>
644
952
  typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
@@ -649,7 +957,7 @@ export async function register() {
649
957
  });
650
958
  };
651
959
 
652
- console.warn = function(...args: any[]) {
960
+ console.warn = function(...args: unknown[]) {
653
961
  originalConsoleWarn.apply(console, args);
654
962
  const message = args.map(arg =>
655
963
  typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
@@ -672,6 +980,7 @@ export async function register() {
672
980
  const envLocalContent = [
673
981
  '# Azure Functions Backend URL (Local)',
674
982
  'BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071',
983
+ 'BACKEND_FUNCTIONS_KEY=',
675
984
  ''
676
985
  ].join('\n');
677
986
  fs.writeFileSync(path.join(projectDir, '.env.local'), envLocalContent);
@@ -709,7 +1018,7 @@ export async function register() {
709
1018
  // 18. Create README.md
710
1019
  createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage);
711
1020
  // 19. Create AI agent instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
712
- createAiAgentFiles(projectDir, projectName, backendLanguage);
1021
+ createAiAgentFiles(projectDir, projectName, backendLanguage, pm);
713
1022
  }
714
1023
  async function createSharedPackage(projectDir, projectName) {
715
1024
  console.log('📦 Creating shared workspace package for Zod models...\n');
@@ -718,6 +1027,7 @@ async function createSharedPackage(projectDir, projectName) {
718
1027
  fs.mkdirSync(modelsDir, { recursive: true });
719
1028
  // shared/package.json
720
1029
  const sharedPackageJson = {
1030
+ private: true,
721
1031
  name: `@${projectName}/shared`,
722
1032
  version: '1.0.0',
723
1033
  description: 'Shared Zod models — Single Source of Truth for validation schemas',
@@ -736,26 +1046,7 @@ async function createSharedPackage(projectDir, projectName) {
736
1046
  };
737
1047
  fs.writeFileSync(path.join(sharedDir, 'package.json'), JSON.stringify(sharedPackageJson, null, 2));
738
1048
  // shared/tsconfig.json
739
- const sharedTsConfig = {
740
- compilerOptions: {
741
- target: 'ES2020',
742
- module: 'commonjs',
743
- moduleResolution: 'node',
744
- lib: ['ES2020'],
745
- outDir: 'dist',
746
- rootDir: '.',
747
- declaration: true,
748
- declarationMap: true,
749
- sourceMap: true,
750
- strict: true,
751
- esModuleInterop: true,
752
- skipLibCheck: true,
753
- forceConsistentCasingInFileNames: true,
754
- },
755
- include: ['index.ts', 'models/**/*'],
756
- exclude: ['node_modules', 'dist'],
757
- };
758
- fs.writeFileSync(path.join(sharedDir, 'tsconfig.json'), JSON.stringify(sharedTsConfig, null, 2));
1049
+ fs.writeFileSync(path.join(sharedDir, 'tsconfig.json'), JSON.stringify(buildSharedTsConfig(), null, 2));
759
1050
  // shared/index.ts (empty re-export file, scaffold will add entries)
760
1051
  fs.writeFileSync(path.join(sharedDir, 'index.ts'), `// Shared Zod models — auto-managed by SwallowKit scaffold command\n// Do not edit the export list below manually\n`);
761
1052
  // shared/.gitignore
@@ -828,8 +1119,9 @@ tsconfig.json
828
1119
  `);
829
1120
  }
830
1121
  else if (backendLanguage === 'python') {
831
- gitignoreLines.unshift('.venv', '__pycache__', '.python_packages');
1122
+ gitignoreLines.unshift('.venv', '.codegen-venv', '__pycache__', '.python_packages');
832
1123
  fs.writeFileSync(path.join(functionsDir, '.funcignore'), `.venv
1124
+ .codegen-venv
833
1125
  __pycache__
834
1126
  .pytest_cache
835
1127
  .mypy_cache
@@ -848,8 +1140,8 @@ tests
848
1140
  }
849
1141
  fs.writeFileSync(path.join(functionsDir, '.gitignore'), `${gitignoreLines.join('\n')}\n`);
850
1142
  }
851
- function createTypeScriptFunctionsProject(projectDir, functionsDir, pm) {
852
- const functionsPackageJson = {
1143
+ function buildTypeScriptFunctionsPackageJson(projectName, pm = 'pnpm') {
1144
+ return {
853
1145
  name: 'functions',
854
1146
  version: '1.0.0',
855
1147
  description: 'Azure Functions backend',
@@ -864,13 +1156,16 @@ function createTypeScriptFunctionsProject(projectDir, functionsDir, pm) {
864
1156
  '@azure/cosmos': '^4.0.0',
865
1157
  '@azure/identity': '^4.0.0',
866
1158
  zod: '>=3.25.0',
867
- [`@${path.basename(projectDir)}/shared`]: '*',
1159
+ [`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:../shared',
868
1160
  },
869
1161
  devDependencies: {
870
1162
  '@types/node': '^20.0.0',
871
1163
  typescript: '^5.0.0',
872
1164
  },
873
1165
  };
1166
+ }
1167
+ function createTypeScriptFunctionsProject(projectDir, functionsDir, pm) {
1168
+ const functionsPackageJson = buildTypeScriptFunctionsPackageJson(path.basename(projectDir), pm);
874
1169
  fs.writeFileSync(path.join(functionsDir, 'package.json'), JSON.stringify(functionsPackageJson, null, 2));
875
1170
  const sharedPkgName = `@${path.basename(projectDir)}/shared`;
876
1171
  const functionsTsConfig = {
@@ -941,7 +1236,7 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
941
1236
 
942
1237
  app.http('greet', {
943
1238
  methods: ['GET', 'POST'],
944
- authLevel: 'anonymous',
1239
+ authLevel: 'function',
945
1240
  handler: greet
946
1241
  });
947
1242
  `);
@@ -951,6 +1246,8 @@ function createCSharpFunctionsProject(projectDir, functionsDir) {
951
1246
  const projectPascal = projectBaseName.charAt(0).toUpperCase() + projectBaseName.slice(1);
952
1247
  const csprojName = `${projectPascal}.Functions.csproj`;
953
1248
  fs.writeFileSync(path.join(functionsDir, csprojName), buildCSharpFunctionsProjectSource());
1249
+ fs.mkdirSync(path.join(functionsDir, '.config'), { recursive: true });
1250
+ fs.writeFileSync(path.join(functionsDir, '.config', 'dotnet-tools.json'), (0, native_schema_generator_1.buildCSharpCodegenToolManifestSource)());
954
1251
  fs.writeFileSync(path.join(functionsDir, 'Program.cs'), buildCSharpFunctionsProgramSource());
955
1252
  const crudDir = path.join(functionsDir, 'Crud');
956
1253
  fs.mkdirSync(crudDir, { recursive: true });
@@ -964,7 +1261,7 @@ public sealed class GreetFunction
964
1261
  {
965
1262
  [Function("greet")]
966
1263
  public async Task<HttpResponseData> Run(
967
- [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "greet")] HttpRequestData request)
1264
+ [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "greet")] HttpRequestData request)
968
1265
  {
969
1266
  var query = request.Url.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
970
1267
  var name = "SwallowKit";
@@ -990,10 +1287,12 @@ public sealed class GreetFunction
990
1287
  }
991
1288
  function createPythonFunctionsProject(projectDir, functionsDir) {
992
1289
  fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
1290
+ ensureProjectGitignoreEntry(projectDir, '.uv');
993
1291
  fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
994
1292
  azure-cosmos>=4.9.0
995
1293
  azure-identity>=1.19.0
996
1294
  `);
1295
+ fs.writeFileSync(path.join(functionsDir, 'requirements.codegen.txt'), (0, native_schema_generator_1.buildPythonCodegenRequirementsSource)());
997
1296
  const blueprintsDir = path.join(functionsDir, 'blueprints');
998
1297
  fs.mkdirSync(blueprintsDir, { recursive: true });
999
1298
  fs.writeFileSync(path.join(blueprintsDir, '__init__.py'), '');
@@ -1022,12 +1321,25 @@ def greet(req: func.HttpRequest) -> func.HttpResponse:
1022
1321
 
1023
1322
  from blueprints.greet import bp as greet_bp
1024
1323
 
1025
- app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
1324
+ app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
1026
1325
 
1027
1326
  app.register_blueprint(greet_bp)
1028
1327
  # SwallowKit scaffold registrations
1029
1328
  `);
1030
1329
  }
1330
+ function ensureProjectGitignoreEntry(projectDir, entry) {
1331
+ const gitignorePath = path.join(projectDir, '.gitignore');
1332
+ if (!fs.existsSync(gitignorePath)) {
1333
+ return;
1334
+ }
1335
+ const current = fs.readFileSync(gitignorePath, 'utf8');
1336
+ const lines = current.split(/\r?\n/);
1337
+ if (lines.includes(entry)) {
1338
+ return;
1339
+ }
1340
+ const normalized = current.endsWith('\n') ? current : `${current}\n`;
1341
+ fs.writeFileSync(gitignorePath, `${normalized}${entry}\n`);
1342
+ }
1031
1343
  async function createBffApiRoute(projectDir) {
1032
1344
  console.log('📦 Creating BFF API route...\n');
1033
1345
  const apiDir = path.join(projectDir, 'app', 'api', 'greet');
@@ -1188,7 +1500,7 @@ export default function Home() {
1188
1500
  Create your first model with Zod and generate CRUD operations automatically.
1189
1501
  </p>
1190
1502
  <code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
1191
- ${pmCmd.dlx} swallowkit scaffold shared/models/your-model.ts
1503
+ ${pmCmd.exec} swallowkit scaffold shared/models/your-model.ts
1192
1504
  </code>
1193
1505
  </div>
1194
1506
  </section>
@@ -1234,17 +1546,18 @@ function createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, back
1234
1546
  const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
1235
1547
  const schemaBridgeDescription = backendLanguage === 'typescript'
1236
1548
  ? 'Zod (shared between frontend and backend)'
1237
- : `Zod + OpenAPI bridge (Zod in shared/, generated ${backendLanguageLabel} schemas in functions/generated/)`;
1549
+ : `Zod + OpenAPI export (Zod in shared/, native-generated ${backendLanguageLabel} schemas in functions/generated/)`;
1238
1550
  const functionsTree = backendLanguage === 'typescript'
1239
1551
  ? `│ └── src/\n│ └── greet.ts # Sample function`
1240
1552
  : backendLanguage === 'csharp'
1241
- ? `│ ├── Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ # OpenAPI-derived C# models`
1242
- : `│ ├── blueprints/\n│ │ └── greet.py\n│ └── generated/ # OpenAPI-derived Python models`;
1553
+ ? `│ ├── Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ # Native-generated C# schema assets`
1554
+ : `│ ├── blueprints/\n│ │ └── greet.py\n│ └── generated/ # Native-generated Python schema assets`;
1243
1555
  const backendScaffoldNote = backendLanguage === 'typescript'
1244
1556
  ? '- Azure Functions CRUD endpoints'
1245
- : `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI spec + generated ${backendLanguageLabel} schema assets`;
1557
+ : `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI export + native-generated ${backendLanguageLabel} schema assets`;
1558
+ const pythonUvPaths = (0, python_uv_1.getProjectLocalUvPaths)(projectDir);
1246
1559
  const pythonLocalDevNote = backendLanguage === 'python'
1247
- ? `\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`
1560
+ ? `\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`
1248
1561
  : '';
1249
1562
  const readme = `# ${projectName}
1250
1563
 
@@ -1273,7 +1586,7 @@ This project was initialized with the following settings:
1273
1586
 
1274
1587
  Before you begin, ensure you have the following installed:
1275
1588
 
1276
- 1. **Node.js 18+**: [Download](https://nodejs.org/)${pm === 'pnpm' ? `\n2. **pnpm**: \`corepack enable\` or \`npm install -g pnpm\`` : ''}
1589
+ 1. **Node.js 22.14+**: [Download](https://nodejs.org/)${pm === 'pnpm' ? `\n2. **pnpm**: \`corepack enable\` or \`npm install -g pnpm\`` : ''}
1277
1590
  ${pm === 'pnpm' ? '3' : '2'}. **Azure CLI**: Required for provisioning Azure resources
1278
1591
  - Install: \`winget install Microsoft.AzureCLI\` (Windows)
1279
1592
  - Or: [Download](https://aka.ms/installazurecliwindows)
@@ -1281,7 +1594,7 @@ ${pm === 'pnpm' ? '4' : '3'}. **Azure Cosmos DB Emulator**: Required for local d
1281
1594
  - Windows: \`winget install Microsoft.Azure.CosmosEmulator\`
1282
1595
  - Or: [Download](https://aka.ms/cosmosdb-emulator)
1283
1596
  - Docker: \`docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator\`
1284
- ${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools**: Automatically installed with project dependencies
1597
+ ${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools v4**: Install separately for local Azure Functions execution
1285
1598
 
1286
1599
  ## 📁 Project Structure
1287
1600
 
@@ -1307,7 +1620,7 @@ ${functionsTree}
1307
1620
  Define your data model with Zod schema:
1308
1621
 
1309
1622
  \`\`\`bash
1310
- ${pmCmd.dlx} swallowkit create-model <model-name>
1623
+ ${pmCmd.exec} swallowkit create-model <model-name>
1311
1624
  \`\`\`
1312
1625
 
1313
1626
  This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to define your schema.
@@ -1317,7 +1630,7 @@ This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to defin
1317
1630
  Generate complete CRUD operations (Functions, API routes, UI):
1318
1631
 
1319
1632
  \`\`\`bash
1320
- ${pmCmd.dlx} swallowkit scaffold shared/models/<model-name>.ts
1633
+ ${pmCmd.exec} swallowkit scaffold shared/models/<model-name>.ts
1321
1634
  \`\`\`
1322
1635
 
1323
1636
  This generates:
@@ -1329,7 +1642,7 @@ ${backendScaffoldNote}
1329
1642
  ### 3. Start Development Servers
1330
1643
 
1331
1644
  \`\`\`bash
1332
- ${pmCmd.dlx} swallowkit dev
1645
+ ${pmCmd.exec} swallowkit dev
1333
1646
  \`\`\`
1334
1647
 
1335
1648
  This starts:
@@ -1347,7 +1660,7 @@ ${pythonLocalDevNote}
1347
1660
  Create all required Azure resources using Bicep:
1348
1661
 
1349
1662
  \`\`\`bash
1350
- ${pmCmd.dlx} swallowkit provision --resource-group <rg-name>
1663
+ ${pmCmd.exec} swallowkit provision --resource-group <rg-name>
1351
1664
  \`\`\`
1352
1665
 
1353
1666
  This creates:
@@ -1403,10 +1716,10 @@ func azure functionapp publish func-${projectName}
1403
1716
 
1404
1717
  ## 🔧 Available Commands
1405
1718
 
1406
- - \`${pmCmd.dlx} swallowkit create-model <name>\` - Create a new data model
1407
- - \`${pmCmd.dlx} swallowkit scaffold <model-file>\` - Generate CRUD code
1408
- - \`${pmCmd.dlx} swallowkit dev\` - Start development servers
1409
- - \`${pmCmd.dlx} swallowkit provision -g <rg-name>\` - Provision Azure resources
1719
+ - \`${pmCmd.exec} swallowkit create-model <name>\` - Create a new data model
1720
+ - \`${pmCmd.exec} swallowkit scaffold <model-file>\` - Generate CRUD code
1721
+ - \`${pmCmd.exec} swallowkit dev\` - Start development servers
1722
+ - \`${pmCmd.exec} swallowkit provision -g <rg-name>\` - Provision Azure resources
1410
1723
  ${azureConfig.vnetOption !== 'none' ? `
1411
1724
  ## 🔒 Network Security (VNet Configuration)
1412
1725
 
@@ -1453,416 +1766,62 @@ This project was generated by SwallowKit. If you encounter any issues or have su
1453
1766
  fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
1454
1767
  console.log('✅ README.md created\n');
1455
1768
  }
1456
- function createAiAgentFiles(projectDir, projectName, backendLanguage) {
1769
+ function createAiAgentFiles(projectDir, projectName, backendLanguage, pm) {
1457
1770
  console.log('🤖 Creating AI agent instruction files...\n');
1458
- const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
1459
- const functionsStructureLine = backendLanguage === 'typescript'
1460
- ? `│ └── src/ # HTTP trigger handlers with Cosmos DB bindings`
1461
- : backendLanguage === 'csharp'
1462
- ? `│ ├── Crud/ # C# HTTP trigger handlers\n│ └── generated/ # OpenAPI-derived C# schema assets`
1463
- : `│ ├── blueprints/ # Python HTTP trigger handlers\n│ └── generated/ # OpenAPI-derived Python schema assets`;
1464
- const backendSchemaNote = backendLanguage === 'typescript'
1465
- ? `- The shared package (\`@${projectName}/shared\`) is consumed by both Next.js and Azure Functions as a workspace dependency.`
1466
- : `- 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.`;
1467
- const backendRulesNote = backendLanguage === 'typescript'
1468
- ? `- 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.`
1469
- : `- 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\`.`;
1470
- // ── 1. AGENTS.md (Codex / generic agents) ──────────────────────────
1471
- const agentsMd = `# AGENTS.md
1472
-
1473
- This project was generated by **SwallowKit**.
1474
- All coding agents **must** follow the architecture and conventions described below.
1475
-
1476
- ## Architecture Overview
1477
-
1478
- This is a full-stack application deployed on Azure with a TypeScript frontend/BFF and an Azure Functions backend in ${backendLanguageLabel}.
1479
-
1480
- \`\`\`
1481
- Frontend (React / Next.js App Router)
1482
- ↓ fetch('/api/{model}', ...)
1483
- BFF Layer (Next.js API Routes)
1484
- ↓ HTTP → Azure Functions
1485
- Backend (Azure Functions)
1486
- ↓
1487
- Azure Cosmos DB (Document Database)
1488
- \`\`\`
1489
-
1490
- ### Project Structure
1491
-
1492
- \`\`\`
1493
- ${projectName}/
1494
- ├── app/ # Next.js App Router
1495
- │ ├── api/ # BFF API routes (proxy to Azure Functions)
1496
- │ └── {model}/ # UI pages per model (list, detail, create, edit)
1497
- ├── functions/ # Azure Functions (backend)
1498
- ${functionsStructureLine}
1499
- ├── shared/ # Shared workspace package
1500
- │ ├── models/ # Zod schema definitions (single source of truth)
1501
- │ └── index.ts # Re-exports all models
1502
- ├── lib/
1503
- │ └── api/ # API client utilities (backend.ts, call-function.ts)
1504
- ├── components/ # Shared React components
1505
- ├── infra/ # Bicep infrastructure-as-code files
1506
- │ ├── main.bicep
1507
- │ └── modules/
1508
- └── .github/workflows/ # CI/CD workflows (if configured)
1509
- \`\`\`
1510
-
1511
- ## Critical Design Principles
1512
-
1513
- ### 1. Next.js API Routes Are Strictly a BFF (Backend for Frontend)
1771
+ const runCmd = (0, package_manager_1.getCommands)(pm).exec;
1772
+ (0, agent_instructions_1.writeAgentInstructionFiles)(projectDir, {
1773
+ projectName,
1774
+ backendLanguage,
1775
+ packageManager: pm,
1776
+ });
1777
+ console.log(' ✅ AGENTS.md (canonical project contract)');
1778
+ console.log(' ✅ CLAUDE.md (Claude Code adapter)');
1779
+ console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot adapter)');
1780
+ console.log(' ✅ .github/instructions/ (path-specific instructions)');
1781
+ fs.writeFileSync(path.join(projectDir, '.mcp.json'), buildSwallowKitMcpProjectConfigSource(pm));
1782
+ console.log(' ✅ .mcp.json (project-scoped MCP bootstrap)');
1783
+ (0, workflows_1.writeWorkflowDocs)(projectDir, runCmd);
1784
+ console.log(' ✅ .swallowkit/workflows/ (agent-agnostic autonomous-loop runbooks)');
1785
+ const skillPaths = (0, workflows_1.writeAgentSkills)(projectDir, runCmd);
1786
+ console.log(' ✅ .github/skills/ (Agent Skills — agentskills.io open standard)');
1787
+ for (const skillPath of skillPaths) {
1788
+ console.log(` - ${skillPath}`);
1789
+ }
1790
+ console.log('\n✅ AI agent files created\n');
1791
+ }
1792
+ function buildFunctionsHostKeyBicepExpression() {
1793
+ return "listKeys('${resourceId('Microsoft.Web/sites', functionsAppName)}/host/default', '2023-12-01').functionKeys.default";
1794
+ }
1795
+ function buildStaticWebAppConfigBicepSource() {
1796
+ return `@description('Static Web App name')
1797
+ param staticWebAppName string
1514
1798
 
1515
- - \`app/api/\` routes exist **only** to proxy requests to Azure Functions.
1516
- - **Never** place business logic, database access, or direct Cosmos DB calls in Next.js API routes.
1517
- - The BFF layer may validate input/output with Zod schemas before forwarding to Functions.
1518
- - Use the \`callFunction\` helper (\`lib/api/call-function.ts\`) or the \`api\` client (\`lib/api/backend.ts\`) to call Azure Functions.
1799
+ @description('Functions App default hostname for backend API calls')
1800
+ param functionsDefaultHostname string
1519
1801
 
1520
- Example BFF route pattern:
1802
+ @description('Functions App name used to resolve its host key after deployment')
1803
+ param functionsAppName string
1521
1804
 
1522
- \`\`\`typescript
1523
- // app/api/{model}/route.ts
1524
- import { callFunction } from '@/lib/api/call-function';
1525
- import { ModelSchema } from '@${projectName}/shared';
1526
- import { z } from 'zod/v4';
1805
+ @description('Application Insights connection string for SWA')
1806
+ param appInsightsConnectionString string
1527
1807
 
1528
- export async function GET() {
1529
- return callFunction({
1530
- method: 'GET',
1531
- path: '/api/{model}',
1532
- responseSchema: z.array(ModelSchema),
1533
- });
1808
+ resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
1809
+ name: staticWebAppName
1534
1810
  }
1535
1811
 
1536
- export async function POST(request: NextRequest) {
1537
- const body = await request.json();
1538
- return callFunction({
1539
- method: 'POST',
1540
- path: '/api/{model}',
1541
- body,
1542
- inputSchema: ModelSchema.omit({ id: true, createdAt: true, updatedAt: true }),
1543
- responseSchema: ModelSchema,
1544
- successStatus: 201,
1545
- });
1546
- }
1547
- \`\`\`
1548
-
1549
- ### 2. Zod Schemas Are the Single Source of Truth
1550
-
1551
- - All data models are defined **once** as Zod schemas in \`shared/models/\`.
1552
- - TypeScript types are derived with \`z.infer<typeof Schema>\` — never define types separately.
1553
- - ${backendSchemaNote}
1554
-
1555
- Model definition pattern:
1556
-
1557
- \`\`\`typescript
1558
- // shared/models/{model}.ts
1559
- import { z } from 'zod/v4';
1560
-
1561
- export const Todo = z.object({
1562
- id: z.string(),
1563
- name: z.string().min(1),
1564
- // ... your fields
1565
- createdAt: z.string().optional(),
1566
- updatedAt: z.string().optional(),
1567
- });
1568
-
1569
- export type Todo = z.infer<typeof Todo>;
1570
- export const displayName = 'Todo';
1571
- \`\`\`
1572
-
1573
- Key rules:
1574
- - Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
1575
- - \`id\`, \`createdAt\`, and \`updatedAt\` are auto-managed by the backend. Mark them as \`optional()\` in the schema.
1576
- - Always re-export models from \`shared/index.ts\`.
1577
-
1578
- ### 3. Azure Functions Own All Business Logic and Data Access
1579
-
1580
- - ${backendRulesNote}
1581
-
1582
- ${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.`}
1583
-
1584
- ${backendLanguage === 'typescript' ? `\`\`\`typescript
1585
- // functions/src/{model}.ts
1586
- import { app } from '@azure/functions';
1587
- import { ModelSchema } from '@${projectName}/shared';
1588
-
1589
- const containerName = 'Models'; // PascalCase + 's'
1590
-
1591
- app.http('{model}-get-all', {
1592
- methods: ['GET'],
1593
- route: '{model}',
1594
- authLevel: 'anonymous',
1595
- extraInputs: [{ type: 'cosmosDB', name: 'cosmosInput', containerName, ... }],
1596
- handler: async (request, context) => {
1597
- const documents = context.extraInputs.get('cosmosInput');
1598
- const validated = z.array(ModelSchema).parse(documents);
1599
- return { status: 200, jsonBody: validated };
1600
- },
1601
- });
1602
- \`\`\`` : ''}
1603
-
1604
- ## Naming Conventions
1605
-
1606
- | Item | Convention | Example |
1607
- |------|-----------|---------|
1608
- | Model schema file | \`shared/models/{kebab-case}.ts\` | \`shared/models/todo.ts\` |
1609
- | Schema/type name | PascalCase (same name for both) | \`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\` |
1610
- | Functions handler file | backend-language specific under \`functions/\` | \`${backendLanguage === 'typescript' ? 'functions/src/todo.ts' : backendLanguage === 'csharp' ? 'functions/Crud/TodoFunctions.cs' : 'functions/blueprints/todo.py'}\` |
1611
- | Functions handler name | \`{camelCase}-{operation}\` | \`todo-get-all\`, \`todo-create\` |
1612
- | API route path | \`/api/{camelCase}\` | \`/api/todo\`, \`/api/todo/{id}\` |
1613
- | BFF route file | \`app/api/{kebab-case}/route.ts\` | \`app/api/todo/route.ts\` |
1614
- | BFF detail route | \`app/api/{kebab-case}/[id]/route.ts\` | \`app/api/todo/[id]/route.ts\` |
1615
- | UI page directory | \`app/{kebab-case}/\` | \`app/todo/page.tsx\` |
1616
- | React component | PascalCase | \`TodoForm.tsx\` |
1617
- | Cosmos DB container | PascalCase + 's' | \`Todos\` |
1618
- | Cosmos DB partition key | \`/id\` | Always \`/id\` |
1619
- | Bicep container file | \`infra/containers/{kebab-case}-container.bicep\` | \`infra/containers/todo-container.bicep\` |
1620
-
1621
- ## Adding New Models (SwallowKit CLI Skills)
1622
-
1623
- Use the SwallowKit CLI — do **not** manually create model files or CRUD boilerplate.
1624
-
1625
- ### Skill: Create a new data model
1626
-
1627
- \`\`\`bash
1628
- npx swallowkit create-model <name>
1629
- # Multiple models at once:
1630
- npx swallowkit create-model user post comment
1631
- \`\`\`
1632
-
1633
- Creates \`shared/models/<name>.ts\` with a Zod schema template including \`id\`, \`createdAt\`, \`updatedAt\`.
1634
- Edit the generated file to add your domain-specific fields, then run scaffold.
1635
-
1636
- ### Skill: Generate full CRUD from a model
1637
-
1638
- \`\`\`bash
1639
- npx swallowkit scaffold shared/models/<name>.ts
1640
- \`\`\`
1641
-
1642
- Generates:
1643
- - Azure Functions handlers (${backendLanguage === 'typescript' ? '\`functions/src/<name>.ts\`' : '\`functions/\` language-specific CRUD files + \`functions/generated/\` schema assets'})
1644
- - BFF API routes (\`app/api/<name>/route.ts\`, \`app/api/<name>/[id]/route.ts\`)
1645
- - UI pages (\`app/<name>/page.tsx\`, detail, create, edit pages)
1646
- - Cosmos DB Bicep container config (\`infra/containers/<name>-container.bicep\`)
1647
-
1648
- ### Skill: Start development servers
1649
-
1650
- \`\`\`bash
1651
- npx swallowkit dev
1652
- \`\`\`
1653
-
1654
- Runs Next.js (http://localhost:3000) and Azure Functions (http://localhost:7071) concurrently.
1655
- Checks for Cosmos DB Emulator availability.
1656
-
1657
- ### Skill: Provision Azure resources
1658
-
1659
- \`\`\`bash
1660
- npx swallowkit provision --resource-group <name> --location <region>
1661
- \`\`\`
1662
-
1663
- Deploys Bicep infrastructure: Static Web Apps, Functions, Cosmos DB, Storage, Managed Identity.
1664
-
1665
- ### Typical workflow for "add a new feature/model"
1666
-
1667
- 1. \`npx swallowkit create-model <name>\`
1668
- 2. Edit \`shared/models/<name>.ts\` — add fields
1669
- 3. \`npx swallowkit scaffold shared/models/<name>.ts\`
1670
- 4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
1671
-
1672
- ## Do NOT
1673
-
1674
- - **Do not** put business logic or database calls in \`app/api/\` routes. They are BFF only.
1675
- - **Do not** define TypeScript interfaces/types separately from Zod schemas. Always derive types with \`z.infer<>\`.
1676
- - **Do not** manually duplicate model definitions across layers. Use the shared package.
1677
- - **Do not** manually create CRUD boilerplate. Use \`swallowkit scaffold\`.
1678
- - **Do not** hardcode Cosmos DB connection strings. Use Managed Identity (\`CosmosDBConnection__accountEndpoint\`) in production and emulator settings locally.
1679
- - **Do not** change the partition key strategy. All containers use \`/id\` as the partition key.
1680
-
1681
- ## Technology Stack
1682
-
1683
- - **Frontend**: Next.js (App Router), React, TypeScript, Tailwind CSS
1684
- - **BFF**: Next.js API Routes (proxy only)
1685
- - **Backend**: Azure Functions (${backendLanguageLabel})
1686
- - **Database**: Azure Cosmos DB (NoSQL)
1687
- - **Schema**: Zod (shared across all layers via workspace package)
1688
- - **Infrastructure**: Bicep (IaC)
1689
- - **Hosting**: Azure Static Web Apps (frontend) + Azure Functions Flex Consumption (backend)
1690
- - **Auth**: Azure Managed Identity (no connection strings in production)
1691
- - **Monitoring**: Application Insights
1692
- `;
1693
- fs.writeFileSync(path.join(projectDir, 'AGENTS.md'), agentsMd);
1694
- console.log(' ✅ AGENTS.md (Codex / generic agents)');
1695
- // ── 2. CLAUDE.md (Claude Code) ─────────────────────────────────────
1696
- const claudeMd = `# CLAUDE.md
1697
-
1698
- This file is for Claude Code. Read AGENTS.md in the project root for the full architecture, conventions, and rules.
1699
-
1700
- ## Quick Reference
1701
-
1702
- - **Architecture**: Next.js (frontend) → BFF (API routes, proxy only) → Azure Functions (backend) → Cosmos DB
1703
- - **Schema**: Zod schemas in \`shared/models/\` are the single source of truth. Never define types separately.
1704
- - **BFF rule**: \`app/api/\` routes must ONLY proxy to Azure Functions via \`callFunction()\`. No business logic.
1705
- - **Backend language**: ${backendLanguageLabel}
1706
- - **Backend rule**: Regenerate backend contracts with \`swallowkit scaffold\` after schema changes and keep \`functions/generated/\` in sync.
1707
-
1708
- ## SwallowKit CLI Commands
1709
-
1710
- | Task | Command |
1711
- |------|---------|
1712
- | Create model | \`npx swallowkit create-model <name>\` |
1713
- | Generate CRUD | \`npx swallowkit scaffold shared/models/<name>.ts\` |
1714
- | Dev servers | \`npx swallowkit dev\` |
1715
- | Provision Azure | \`npx swallowkit provision --resource-group <rg> --location <region>\` |
1716
-
1717
- ## Workflow: Add a new model
1718
-
1719
- 1. \`npx swallowkit create-model <name>\`
1720
- 2. Edit \`shared/models/<name>.ts\` — add your fields
1721
- 3. \`npx swallowkit scaffold shared/models/<name>.ts\`
1722
- 4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
1723
- `;
1724
- fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), claudeMd);
1725
- console.log(' ✅ CLAUDE.md (Claude Code)');
1726
- // ── 3. .github/copilot-instructions.md (GitHub Copilot) ────────────
1727
- const ghDir = path.join(projectDir, '.github');
1728
- fs.mkdirSync(ghDir, { recursive: true });
1729
- const copilotInstructions = `# Copilot Instructions
1730
-
1731
- This project was generated by **SwallowKit**. See \`AGENTS.md\` in the project root for the full specification.
1732
-
1733
- ## Architecture (3-layer)
1734
-
1735
- \`\`\`
1736
- Frontend (Next.js App Router) → BFF (Next.js API Routes) → Backend (Azure Functions) → Cosmos DB
1737
- \`\`\`
1738
-
1739
- ## Key Rules
1740
-
1741
- 1. **BFF is proxy only** — \`app/api/\` routes call Azure Functions via \`callFunction()\`. No business logic, no direct DB access.
1742
- 2. **Zod = single source of truth** — Models live in \`shared/models/\`. Types are derived with \`z.infer<>\`. Never define types separately.
1743
- 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/\`.
1744
- 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.
1745
-
1746
- ## Naming
1747
-
1748
- - Schema/type: PascalCase, same name for both (\`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\`)
1749
- - Files: kebab-case (\`shared/models/todo.ts\`, backend handlers under \`functions/\`)
1750
- - Cosmos DB containers: PascalCase + 's' (\`Todos\`), partition key always \`/id\`
1751
-
1752
- ## Managed Fields
1753
-
1754
- \`id\`, \`createdAt\`, \`updatedAt\` are auto-managed by the backend. Define them as \`optional()\` in schemas. Never trust client-sent values.
1755
- `;
1756
- fs.writeFileSync(path.join(ghDir, 'copilot-instructions.md'), copilotInstructions);
1757
- console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot)');
1758
- // ── 4. .github/instructions/*.instructions.md (Copilot layer-specific) ──
1759
- const instructionsDir = path.join(ghDir, 'instructions');
1760
- fs.mkdirSync(instructionsDir, { recursive: true });
1761
- // 4a. shared/models — Zod schema layer
1762
- const sharedModelsInstructions = `---
1763
- applyTo: "shared/models/**"
1764
- ---
1765
-
1766
- # Shared Models — Zod Schema Rules
1767
-
1768
- Files in this directory are the **single source of truth** for data models across the entire application.
1769
-
1770
- ## Rules
1771
-
1772
- - Define Zod schemas using \`zod/v4\` (\`import { z } from 'zod/v4'\`).
1773
- - Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
1774
- \`\`\`typescript
1775
- export const Todo = z.object({ ... });
1776
- export type Todo = z.infer<typeof Todo>;
1777
- \`\`\`
1778
- - Always include \`id: z.string()\`, \`createdAt: z.string().optional()\`, \`updatedAt: z.string().optional()\`. These are managed by the backend.
1779
- - Export a \`displayName\` string constant for UI display.
1780
- - Re-export every model from \`shared/index.ts\`.
1781
- - For relationships, use **nested schemas** (import and embed the related schema), not ID references.
1782
- - After editing a model, run \`npx swallowkit scaffold shared/models/<name>.ts\` to regenerate CRUD code.
1783
- `;
1784
- fs.writeFileSync(path.join(instructionsDir, 'shared-models.instructions.md'), sharedModelsInstructions);
1785
- // 4b. app/api — BFF layer
1786
- const bffInstructions = `---
1787
- applyTo: "app/api/**"
1788
- ---
1789
-
1790
- # BFF API Routes — Rules
1791
-
1792
- Files in \`app/api/\` are the **BFF (Backend for Frontend)** layer. They exist solely to proxy requests to Azure Functions.
1793
-
1794
- ## Rules
1795
-
1796
- - **Never** put business logic, database access, or direct Cosmos DB calls here.
1797
- - Use \`callFunction()\` from \`@/lib/api/call-function\` to forward requests to Azure Functions.
1798
- - You may validate input/output with Zod schemas before forwarding.
1799
- - Import schemas from \`@${projectName}/shared\`.
1800
-
1801
- ## Pattern
1802
-
1803
- \`\`\`typescript
1804
- import { callFunction } from '@/lib/api/call-function';
1805
- import { ModelSchema } from '@${projectName}/shared';
1806
- import { z } from 'zod/v4';
1807
-
1808
- export async function GET() {
1809
- return callFunction({
1810
- method: 'GET',
1811
- path: '/api/{model}',
1812
- responseSchema: z.array(ModelSchema),
1813
- });
1812
+ resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
1813
+ parent: staticWebApp
1814
+ name: 'appsettings'
1815
+ properties: {
1816
+ APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
1817
+ ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
1818
+ BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
1819
+ BACKEND_FUNCTIONS_KEY: ${buildFunctionsHostKeyBicepExpression()}
1820
+ }
1814
1821
  }
1815
- \`\`\`
1816
- `;
1817
- fs.writeFileSync(path.join(instructionsDir, 'bff-routes.instructions.md'), bffInstructions);
1818
- // 4c. functions — Azure Functions backend layer
1819
- const functionsInstructions = `---
1820
- applyTo: "functions/**"
1821
- ---
1822
-
1823
- # Azure Functions — Backend Rules
1824
-
1825
- Files in \`functions/\` contain all business logic and data access for this application.
1826
-
1827
- ## Rules
1828
-
1829
- - Keep backend contracts aligned with \`shared/models/\` by rerunning \`swallowkit scaffold\` after schema changes.
1830
- - For TypeScript backends, use Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.
1831
- - For C#/Python backends, consume the generated OpenAPI-derived assets in \`functions/generated/\`.
1832
- - Auto-generate \`id\` (UUID), \`createdAt\`, and \`updatedAt\` on the backend. Never trust client-sent values.
1833
- - Container names are PascalCase + 's' (e.g., \`Todos\`). Partition key is always \`/id\`.
1834
-
1835
- ## Handler Pattern
1836
-
1837
- \`\`\`typescript
1838
- import { app } from '@azure/functions';
1839
- import { ModelSchema } from '@${projectName}/shared';
1840
-
1841
- app.http('{model}-get-all', {
1842
- methods: ['GET'],
1843
- route: '{model}',
1844
- authLevel: 'anonymous',
1845
- extraInputs: [cosmosInput],
1846
- handler: async (request, context) => {
1847
- const documents = context.extraInputs.get(cosmosInput);
1848
- const validated = z.array(ModelSchema).parse(documents);
1849
- return { status: 200, jsonBody: validated };
1850
- },
1851
- });
1852
- \`\`\`
1822
+
1823
+ output configName string = staticWebAppConfig.name
1853
1824
  `;
1854
- fs.writeFileSync(path.join(instructionsDir, 'azure-functions.instructions.md'), functionsInstructions);
1855
- console.log(' ✅ .github/instructions/ (Copilot layer-specific instructions)');
1856
- console.log(' - shared-models.instructions.md');
1857
- console.log(' - bff-routes.instructions.md');
1858
- console.log(' - azure-functions.instructions.md');
1859
- console.log('\n✅ AI agent files created\n');
1860
- console.log(' Supported agents:');
1861
- console.log(' - OpenAI Codex → AGENTS.md');
1862
- console.log(' - Claude Code → CLAUDE.md (+ AGENTS.md)');
1863
- console.log(' - GitHub Copilot → .github/copilot-instructions.md');
1864
- console.log(' - GitHub Copilot (edit) → .github/instructions/*.instructions.md');
1865
- console.log('');
1866
1825
  }
1867
1826
  async function createInfrastructure(projectDir, projectName, azureConfig, backendLanguage) {
1868
1827
  console.log('📦 Creating infrastructure files (Bicep)...\n');
@@ -1871,6 +1830,7 @@ async function createInfrastructure(projectDir, projectName, azureConfig, backen
1871
1830
  fs.mkdirSync(modulesDir, { recursive: true });
1872
1831
  const enableVNet = azureConfig.vnetOption !== 'none';
1873
1832
  const functionsRuntime = getFunctionsRuntimeConfig(backendLanguage);
1833
+ const staticWebAppSku = getStaticWebAppSku(azureConfig.swaPlan);
1874
1834
  // main.bicep
1875
1835
  const mainBicep = `targetScope = 'resourceGroup'
1876
1836
 
@@ -1883,6 +1843,13 @@ param location string = resourceGroup().location
1883
1843
  @description('Location for Static Web App (must be explicitly provided)')
1884
1844
  param swaLocation string
1885
1845
 
1846
+ @description('Static Web Apps hosting plan')
1847
+ @allowed([
1848
+ 'Free'
1849
+ 'Standard'
1850
+ ])
1851
+ param staticWebAppSku string = '${staticWebAppSku}'
1852
+
1886
1853
  @description('Cosmos DB mode')
1887
1854
  @allowed(['freetier', 'serverless'])
1888
1855
  param cosmosDbMode string = '${azureConfig.cosmosDbMode}'
@@ -1925,7 +1892,7 @@ module staticWebApp 'modules/staticwebapp.bicep' = {
1925
1892
  params: {
1926
1893
  name: 'swa-\${projectName}'
1927
1894
  location: swaLocation
1928
- sku: 'Standard'
1895
+ sku: staticWebAppSku
1929
1896
  appInsightsConnectionString: appInsightsSwa.outputs.connectionString
1930
1897
  }
1931
1898
  }
@@ -1966,16 +1933,11 @@ module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enabl
1966
1933
  params: {
1967
1934
  name: 'pe-cosmos-\${projectName}'
1968
1935
  location: location
1969
- cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.id : cosmosDbServerless.outputs.id
1970
- cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
1971
- subnetId: vnet.outputs.privateEndpointSubnetId
1972
- vnetId: vnet.outputs.id
1936
+ cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.id : cosmosDbServerless!.outputs.id
1937
+ cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.accountName : cosmosDbServerless!.outputs.accountName
1938
+ subnetId: vnet!.outputs.privateEndpointSubnetId
1939
+ vnetId: vnet!.outputs.id
1973
1940
  }
1974
- dependsOn: [
1975
- cosmosDbFreeTier
1976
- cosmosDbServerless
1977
- vnet
1978
- ]
1979
1941
  }
1980
1942
 
1981
1943
  // Azure Functions (Flex Consumption) - Deploy AFTER Cosmos DB
@@ -1987,16 +1949,14 @@ module functionsFlex 'modules/functions-flex.bicep' = {
1987
1949
  storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
1988
1950
  appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
1989
1951
  swaDefaultHostname: staticWebApp.outputs.defaultHostname
1990
- cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
1991
- cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
1952
+ cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.endpoint : cosmosDbServerless!.outputs.endpoint
1953
+ cosmosDbDatabaseName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.databaseName : cosmosDbServerless!.outputs.databaseName
1992
1954
  functionsRuntimeName: '${functionsRuntime.name}'
1993
1955
  functionsRuntimeVersion: '${functionsRuntime.version}'
1994
1956
  enableVNet: enableVNet
1995
- vnetSubnetId: enableVNet ? vnet.outputs.functionsSubnetId : ''
1957
+ vnetSubnetId: enableVNet ? vnet!.outputs.functionsSubnetId : ''
1996
1958
  }
1997
1959
  dependsOn: [
1998
- cosmosDbFreeTier
1999
- cosmosDbServerless
2000
1960
  cosmosPrivateEndpoint
2001
1961
  ]
2002
1962
  }
@@ -2005,23 +1965,17 @@ module functionsFlex 'modules/functions-flex.bicep' = {
2005
1965
  module cosmosDbRoleAssignmentFreeTier 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'freetier') {
2006
1966
  name: 'cosmosDbRoleAssignment'
2007
1967
  params: {
2008
- cosmosAccountName: cosmosDbFreeTier.outputs.accountName
1968
+ cosmosAccountName: cosmosDbFreeTier!.outputs.accountName
2009
1969
  functionsPrincipalId: functionsFlex.outputs.principalId
2010
1970
  }
2011
- dependsOn: [
2012
- functionsFlex
2013
- ]
2014
1971
  }
2015
1972
 
2016
1973
  module cosmosDbRoleAssignmentServerless 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'serverless') {
2017
1974
  name: 'cosmosDbRoleAssignment'
2018
1975
  params: {
2019
- cosmosAccountName: cosmosDbServerless.outputs.accountName
1976
+ cosmosAccountName: cosmosDbServerless!.outputs.accountName
2020
1977
  functionsPrincipalId: functionsFlex.outputs.principalId
2021
1978
  }
2022
- dependsOn: [
2023
- functionsFlex
2024
- ]
2025
1979
  }
2026
1980
 
2027
1981
  // Update SWA config with Functions hostname (after Functions deployment)
@@ -2030,20 +1984,18 @@ module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
2030
1984
  params: {
2031
1985
  staticWebAppName: staticWebApp.outputs.name
2032
1986
  functionsDefaultHostname: functionsFlex.outputs.defaultHostname
1987
+ functionsAppName: functionsFlex.outputs.name
2033
1988
  appInsightsConnectionString: appInsightsSwa.outputs.connectionString
2034
1989
  }
2035
- dependsOn: [
2036
- functionsFlex
2037
- ]
2038
1990
  }
2039
1991
 
2040
1992
  output staticWebAppName string = staticWebApp.outputs.name
2041
1993
  output staticWebAppUrl string = staticWebApp.outputs.defaultHostname
2042
1994
  output functionsAppName string = functionsFlex.outputs.name
2043
1995
  output functionsAppUrl string = functionsFlex.outputs.defaultHostname
2044
- output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
2045
- output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
2046
- output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.databaseName : cosmosDbServerless.outputs.databaseName
1996
+ output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.accountName : cosmosDbServerless!.outputs.accountName
1997
+ output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.endpoint : cosmosDbServerless!.outputs.endpoint
1998
+ output cosmosDatabaseName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier!.outputs.databaseName : cosmosDbServerless!.outputs.databaseName
2047
1999
  output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
2048
2000
  output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
2049
2001
  output appInsightsSwaName string = appInsightsSwa.outputs.name
@@ -2051,7 +2003,7 @@ output appInsightsSwaConnectionString string = appInsightsSwa.outputs.connection
2051
2003
  output appInsightsFunctionsName string = appInsightsFunctions.outputs.name
2052
2004
  output appInsightsFunctionsConnectionString string = appInsightsFunctions.outputs.connectionString
2053
2005
  output vnetEnabled bool = enableVNet
2054
- output vnetName string = enableVNet ? vnet.outputs.name : ''
2006
+ output vnetName string = enableVNet ? vnet!.outputs.name : ''
2055
2007
  `;
2056
2008
  fs.writeFileSync(path.join(infraDir, 'main.bicep'), mainBicep);
2057
2009
  // main.parameters.json
@@ -2065,6 +2017,9 @@ output vnetName string = enableVNet ? vnet.outputs.name : ''
2065
2017
  "cosmosDbMode": {
2066
2018
  "value": "${azureConfig.cosmosDbMode}"
2067
2019
  },
2020
+ "staticWebAppSku": {
2021
+ "value": "${staticWebAppSku}"
2022
+ },
2068
2023
  "enableVNet": {
2069
2024
  "value": ${enableVNet}
2070
2025
  }
@@ -2119,31 +2074,7 @@ output defaultHostname string = staticWebApp.properties.defaultHostname
2119
2074
  `;
2120
2075
  fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
2121
2076
  // modules/staticwebapp-config.bicep (for updating config after Functions deployment)
2122
- const staticWebAppConfigBicep = `@description('Static Web App name')
2123
- param staticWebAppName string
2124
-
2125
- @description('Functions App default hostname for backend API calls')
2126
- param functionsDefaultHostname string
2127
-
2128
- @description('Application Insights connection string for SWA')
2129
- param appInsightsConnectionString string
2130
-
2131
- resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
2132
- name: staticWebAppName
2133
- }
2134
-
2135
- resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
2136
- parent: staticWebApp
2137
- name: 'appsettings'
2138
- properties: {
2139
- APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
2140
- ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
2141
- BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
2142
- }
2143
- }
2144
-
2145
- output configName string = staticWebAppConfig.name
2146
- `;
2077
+ const staticWebAppConfigBicep = buildStaticWebAppConfigBicepSource();
2147
2078
  fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
2148
2079
  // modules/loganalytics.bicep (Shared Log Analytics Workspace)
2149
2080
  const logAnalyticsBicep = `@description('Log Analytics workspace name')
@@ -2362,63 +2293,7 @@ output principalId string = functionApp.identity.principalId
2362
2293
  `;
2363
2294
  fs.writeFileSync(path.join(modulesDir, 'functions-flex.bicep'), functionsFlexBicep);
2364
2295
  // modules/cosmosdb-freetier.bicep (Free Tier)
2365
- const cosmosDbFreeTierBicep = `@description('Cosmos DB account name')
2366
- param accountName string
2367
-
2368
- @description('Database name')
2369
- param databaseName string
2370
-
2371
- @description('Location for Cosmos DB')
2372
- param location string
2373
-
2374
- @description('Public network access')
2375
- @allowed(['Enabled', 'Disabled'])
2376
- param publicNetworkAccess string = 'Enabled'
2377
-
2378
- // Cosmos DB Account (Free Tier)
2379
- resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
2380
- name: accountName
2381
- location: location
2382
- kind: 'GlobalDocumentDB'
2383
- properties: {
2384
- databaseAccountOfferType: 'Standard'
2385
- enableAutomaticFailover: false
2386
- enableFreeTier: true
2387
- publicNetworkAccess: publicNetworkAccess
2388
- disableLocalAuth: true
2389
- consistencyPolicy: {
2390
- defaultConsistencyLevel: 'Session'
2391
- }
2392
- locations: [
2393
- {
2394
- locationName: location
2395
- failoverPriority: 0
2396
- isZoneRedundant: false
2397
- }
2398
- ]
2399
- disableKeyBasedMetadataWriteAccess: true
2400
- }
2401
- }
2402
-
2403
- // Cosmos DB Database
2404
- resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
2405
- parent: cosmosAccount
2406
- name: databaseName
2407
- properties: {
2408
- resource: {
2409
- id: databaseName
2410
- }
2411
- options: {
2412
- throughput: 1000
2413
- }
2414
- }
2415
- }
2416
-
2417
- output id string = cosmosAccount.id
2418
- output accountName string = cosmosAccount.name
2419
- output endpoint string = cosmosAccount.properties.documentEndpoint
2420
- output databaseName string = database.name
2421
- `;
2296
+ const cosmosDbFreeTierBicep = buildCosmosDbFreeTierBicepSource();
2422
2297
  fs.writeFileSync(path.join(modulesDir, 'cosmosdb-freetier.bicep'), cosmosDbFreeTierBicep);
2423
2298
  // modules/cosmosdb-serverless.bicep (Serverless)
2424
2299
  const cosmosDbServerlessBicep = `@description('Cosmos DB account name')
@@ -2508,10 +2383,11 @@ resource roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignment
2508
2383
  output roleAssignmentId string = roleAssignment.id
2509
2384
  `;
2510
2385
  fs.writeFileSync(path.join(modulesDir, 'cosmosdb-role-assignment.bicep'), cosmosDbRoleAssignmentBicep);
2511
- // VNet modules (only generate if VNet is enabled)
2512
- if (enableVNet) {
2513
- // modules/vnet.bicep
2514
- const vnetBicep = `@description('VNet name')
2386
+ // Bicep resolves module files at compile time even when their deployment condition is false,
2387
+ // so these definitions must always be generated. main.bicep still controls deployment with
2388
+ // `if (enableVNet)`.
2389
+ // modules/vnet.bicep
2390
+ const vnetBicep = `@description('VNet name')
2515
2391
  param name string
2516
2392
 
2517
2393
  @description('Location for VNet')
@@ -2557,9 +2433,9 @@ output name string = vnet.name
2557
2433
  output functionsSubnetId string = vnet.properties.subnets[0].id
2558
2434
  output privateEndpointSubnetId string = vnet.properties.subnets[1].id
2559
2435
  `;
2560
- fs.writeFileSync(path.join(modulesDir, 'vnet.bicep'), vnetBicep);
2561
- // modules/private-endpoint-cosmos.bicep
2562
- const cosmosPrivateEndpointBicep = `@description('Private endpoint name')
2436
+ fs.writeFileSync(path.join(modulesDir, 'vnet.bicep'), vnetBicep);
2437
+ // modules/private-endpoint-cosmos.bicep
2438
+ const cosmosPrivateEndpointBicep = `@description('Private endpoint name')
2563
2439
  param name string
2564
2440
 
2565
2441
  @description('Location')
@@ -2637,24 +2513,28 @@ resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneG
2637
2513
  output privateEndpointId string = privateEndpoint.id
2638
2514
  output privateDnsZoneId string = privateDnsZone.id
2639
2515
  `;
2640
- fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
2641
- console.log('✅ VNet modules created\n');
2642
- }
2516
+ fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
2517
+ console.log('✅ VNet module definitions created\n');
2643
2518
  console.log('✅ Infrastructure files created\n');
2644
2519
  }
2645
- function getGitHubFunctionsWorkflow(pm, backendLanguage) {
2520
+ function getGitHubFunctionsWorkflow(pm, backendLanguage, projectPm = pm) {
2646
2521
  const pmCmd = (0, package_manager_1.getCommands)(pm);
2647
2522
  const pnpmSetupStep = (0, package_manager_1.getCiSetupStep)(pm);
2523
+ const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
2648
2524
  const commonSetup = ` - uses: actions/checkout@v4
2649
2525
 
2526
+ ${pnpmSetupStep ? `${pnpmSetupStep}\n` : ''}
2650
2527
  - name: Setup Node.js
2651
2528
  uses: actions/setup-node@v4
2652
2529
  with:
2653
2530
  node-version: '22'
2654
- ${pnpmSetupStep ? `\n${pnpmSetupStep}\n` : ''}
2531
+ ${npmWorkspaceNormalizationStep ? '' : ` cache: ${pm}
2532
+ cache-dependency-path: ${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}
2533
+ `}
2534
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
2655
2535
  - name: Install dependencies
2656
2536
  run: |
2657
- ${pmCmd.ci}
2537
+ ${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
2658
2538
 
2659
2539
  - name: Build shared package
2660
2540
  run: |
@@ -2738,7 +2618,7 @@ jobs:
2738
2618
  ${commonSetup} - name: Setup .NET
2739
2619
  uses: actions/setup-dotnet@v4
2740
2620
  with:
2741
- dotnet-version: '8.0.x'
2621
+ dotnet-version: '10.0.x'
2742
2622
 
2743
2623
  - name: Publish Functions
2744
2624
  run: |
@@ -2797,16 +2677,18 @@ ${commonSetup} - name: Setup Python
2797
2677
  sku: flexconsumption
2798
2678
  `;
2799
2679
  }
2800
- function getAzureFunctionsPipeline(pm, backendLanguage) {
2680
+ function getAzureFunctionsPipeline(pm, backendLanguage, projectPm = pm) {
2801
2681
  const pmCmd = (0, package_manager_1.getCommands)(pm);
2802
2682
  const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
2683
+ const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
2803
2684
  const commonSetup = ` - task: NodeTool@0
2804
2685
  inputs:
2805
2686
  versionSpec: '22.x'
2806
2687
  displayName: 'Install Node.js'
2807
2688
  ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
2689
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
2808
2690
  - script: |
2809
- ${pmCmd.ci}
2691
+ ${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
2810
2692
  displayName: 'Install workspace dependencies'
2811
2693
 
2812
2694
  - script: |
@@ -2910,7 +2792,7 @@ variables:
2910
2792
  steps:
2911
2793
  ${commonSetup} - task: UseDotNet@2
2912
2794
  inputs:
2913
- version: '8.0.x'
2795
+ version: '10.0.x'
2914
2796
  displayName: 'Install .NET SDK'
2915
2797
 
2916
2798
  - script: |
@@ -2990,13 +2872,205 @@ ${commonSetup} - task: UsePythonVersion@0
2990
2872
  displayName: 'Deploy to Azure Functions'
2991
2873
  `;
2992
2874
  }
2875
+ // setup-node selects the runner runtime only; Oryx reads the app manifest separately.
2876
+ function getSwaNodeVersionCommand() {
2877
+ 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));"`;
2878
+ }
2879
+ function buildAzureSwaPipeline(pm, projectPm = pm) {
2880
+ // SWA/Oryx also invokes npm internally; normalize only the disposable CI checkout.
2881
+ pm = 'npm';
2882
+ const pmCmd = (0, package_manager_1.getCommands)(pm);
2883
+ const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
2884
+ const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
2885
+ return `trigger:
2886
+ branches:
2887
+ include:
2888
+ - main
2889
+ paths:
2890
+ include:
2891
+ - app/**
2892
+ - components/**
2893
+ - lib/**
2894
+ - shared/**
2895
+ - public/**
2896
+ - package.json
2897
+ - next.config.js
2898
+
2899
+ pr:
2900
+ branches:
2901
+ include:
2902
+ - main
2903
+ paths:
2904
+ include:
2905
+ - app/**
2906
+ - components/**
2907
+ - lib/**
2908
+ - shared/**
2909
+ - public/**
2910
+ - package.json
2911
+ - next.config.js
2912
+
2913
+ pool:
2914
+ vmImage: 'ubuntu-latest'
2915
+
2916
+ variables:
2917
+ - group: azure-deployment
2918
+
2919
+ steps:
2920
+ - task: NodeTool@0
2921
+ inputs:
2922
+ versionSpec: '22.x'
2923
+ displayName: 'Install Node.js'
2924
+ ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
2925
+ - script: |
2926
+ ${getSwaNodeVersionCommand()}
2927
+ displayName: 'Select Node.js 22 for SWA'
2928
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
2929
+ - script: |
2930
+ ${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
2931
+ displayName: 'Install dependencies'
2932
+
2933
+ - script: |
2934
+ ${pmCmd.run} build
2935
+ env:
2936
+ NODE_ENV: production
2937
+ displayName: 'Build Next.js app'
2938
+
2939
+ - task: AzureStaticWebApp@0
2940
+ condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
2941
+ inputs:
2942
+ app_location: '.'
2943
+ output_location: '.next/standalone'
2944
+ skip_app_build: true
2945
+ azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
2946
+ displayName: 'Deploy to Azure Static Web Apps'
2947
+ `;
2948
+ }
2949
+ function getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm) {
2950
+ if (pm !== 'npm' || projectPm !== 'pnpm') {
2951
+ return '';
2952
+ }
2953
+ return ` - script: |
2954
+ node <<'NODE'
2955
+ const fs = require('fs');
2956
+
2957
+ function readJson(path) {
2958
+ return JSON.parse(fs.readFileSync(path, 'utf8'));
2959
+ }
2960
+
2961
+ function writeJson(path, value) {
2962
+ fs.writeFileSync(path, JSON.stringify(value, null, 2) + String.fromCharCode(10));
2963
+ }
2964
+
2965
+ const sharedPackagePath = 'shared/package.json';
2966
+ const sharedPackageName = fs.existsSync(sharedPackagePath) ? readJson(sharedPackagePath).name : undefined;
2967
+
2968
+ function replaceSharedWorkspaceDep(pkg, replacement) {
2969
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
2970
+ if (!pkg[section]) continue;
2971
+
2972
+ for (const [name, version] of Object.entries(pkg[section])) {
2973
+ if (typeof version === 'string' && version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
2974
+ pkg[section][name] = replacement;
2975
+ }
2976
+ }
2977
+ }
2978
+ }
2979
+
2980
+ const root = readJson('package.json');
2981
+ root.workspaces = ['shared', 'functions'].filter(dir => fs.existsSync(dir + '/package.json'));
2982
+ delete root.packageManager;
2983
+ root.devEngines = { ...root.devEngines, packageManager: { name: 'npm', version: '>=10', onFail: 'error' } };
2984
+ replaceSharedWorkspaceDep(root, 'file:shared');
2985
+ root.scripts = root.scripts || {};
2986
+ root.scripts.build = ${JSON.stringify((0, package_manager_1.getBuildScript)('npm'))};
2987
+ writeJson('package.json', root);
2988
+
2989
+ if (fs.existsSync('functions/package.json')) {
2990
+ const functionsPkg = readJson('functions/package.json');
2991
+ replaceSharedWorkspaceDep(functionsPkg, 'file:../shared');
2992
+ if (functionsPkg.scripts && functionsPkg.scripts.prestart) {
2993
+ functionsPkg.scripts.prestart = 'npm run build';
2994
+ }
2995
+ writeJson('functions/package.json', functionsPkg);
2996
+ }
2997
+ NODE
2998
+
2999
+ npm install --package-lock-only --ignore-scripts
3000
+ displayName: 'Normalize pnpm workspace for npm CI'`;
3001
+ }
3002
+ function getGitHubNpmWorkspaceNormalizationStep(pm, projectPm) {
3003
+ if (pm !== 'npm' || projectPm !== 'pnpm') {
3004
+ return '';
3005
+ }
3006
+ return ` - name: Normalize pnpm workspace for npm CI
3007
+ shell: bash
3008
+ run: |
3009
+ node <<'NODE'
3010
+ const fs = require('fs');
3011
+
3012
+ function readJson(path) {
3013
+ return JSON.parse(fs.readFileSync(path, 'utf8'));
3014
+ }
3015
+
3016
+ function writeJson(path, value) {
3017
+ fs.writeFileSync(path, JSON.stringify(value, null, 2) + String.fromCharCode(10));
3018
+ }
3019
+
3020
+ const sharedPackagePath = 'shared/package.json';
3021
+ const sharedPackageName = fs.existsSync(sharedPackagePath) ? readJson(sharedPackagePath).name : undefined;
3022
+
3023
+ function replaceSharedWorkspaceDep(pkg, replacement) {
3024
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
3025
+ if (!pkg[section]) continue;
3026
+
3027
+ for (const [name, version] of Object.entries(pkg[section])) {
3028
+ if (typeof version === 'string' && version.startsWith('workspace:') && (name === sharedPackageName || name.endsWith('/shared'))) {
3029
+ pkg[section][name] = replacement;
3030
+ }
3031
+ }
3032
+ }
3033
+ }
3034
+
3035
+ const root = readJson('package.json');
3036
+ root.workspaces = ['shared', 'functions'].filter(dir => fs.existsSync(dir + '/package.json'));
3037
+ delete root.packageManager;
3038
+ root.devEngines = { ...root.devEngines, packageManager: { name: 'npm', version: '>=10', onFail: 'error' } };
3039
+ replaceSharedWorkspaceDep(root, 'file:shared');
3040
+ root.scripts = root.scripts || {};
3041
+ root.scripts.build = ${JSON.stringify((0, package_manager_1.getBuildScript)('npm'))};
3042
+ writeJson('package.json', root);
3043
+
3044
+ if (fs.existsSync('functions/package.json')) {
3045
+ const functionsPkg = readJson('functions/package.json');
3046
+ replaceSharedWorkspaceDep(functionsPkg, 'file:../shared');
3047
+ if (functionsPkg.scripts && functionsPkg.scripts.prestart) {
3048
+ functionsPkg.scripts.prestart = 'npm run build';
3049
+ }
3050
+ writeJson('functions/package.json', functionsPkg);
3051
+ }
3052
+ NODE
3053
+
3054
+ npm install --package-lock-only --ignore-scripts
3055
+ `;
3056
+ }
2993
3057
  async function createGitHubActionsWorkflows(projectDir, azureConfig, pm, backendLanguage) {
2994
3058
  console.log('📦 Creating GitHub Actions workflows...\n');
2995
- const pmCmd = (0, package_manager_1.getCommands)(pm);
2996
3059
  const workflowsDir = path.join(projectDir, '.github', 'workflows');
2997
3060
  fs.mkdirSync(workflowsDir, { recursive: true });
2998
- // deploy-swa.yml
2999
- const swaWorkflow = `name: Deploy Static Web App
3061
+ const actionsPm = pm;
3062
+ const swaWorkflow = buildGitHubSwaWorkflow(actionsPm, pm);
3063
+ fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
3064
+ // deploy-functions.yml
3065
+ const functionsWorkflow = getGitHubFunctionsWorkflow(actionsPm, backendLanguage, pm);
3066
+ fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
3067
+ console.log('✅ GitHub Actions workflows created\n');
3068
+ }
3069
+ function buildGitHubSwaWorkflow(pm, projectPm = pm) {
3070
+ // SWA/Oryx also invokes npm internally; normalize only the disposable CI checkout.
3071
+ pm = 'npm';
3072
+ const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
3073
+ return `name: Deploy Static Web App
3000
3074
 
3001
3075
  on:
3002
3076
  push:
@@ -3009,6 +3083,7 @@ on:
3009
3083
  - 'shared/**'
3010
3084
  - 'public/**'
3011
3085
  - 'package.json'
3086
+ - '${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
3012
3087
  - 'next.config.js'
3013
3088
  - 'next.config.ts'
3014
3089
  workflow_dispatch:
@@ -3022,6 +3097,7 @@ on:
3022
3097
  - 'shared/**'
3023
3098
  - 'public/**'
3024
3099
  - 'package.json'
3100
+ - '${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
3025
3101
  - 'next.config.js'
3026
3102
  - 'next.config.ts'
3027
3103
 
@@ -3034,6 +3110,22 @@ jobs:
3034
3110
  - uses: actions/checkout@v4
3035
3111
  with:
3036
3112
  submodules: true
3113
+
3114
+ ${(0, package_manager_1.getCiSetupStep)(pm) ? `${(0, package_manager_1.getCiSetupStep)(pm)}\n` : ''}
3115
+ - name: Setup Node.js
3116
+ uses: actions/setup-node@v4
3117
+ with:
3118
+ node-version: '22'
3119
+ ${npmWorkspaceNormalizationStep ? '' : ` cache: ${pm}
3120
+ cache-dependency-path: ${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}
3121
+ `}
3122
+ - name: Select Node.js 22 for SWA
3123
+ run: |
3124
+ ${getSwaNodeVersionCommand()}
3125
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
3126
+ - name: Install and build app
3127
+ run: |
3128
+ ${(0, package_manager_1.getSwaAppBuildCommand)(pm, projectPm)}
3037
3129
 
3038
3130
  - name: Deploy to Azure Static Web Apps
3039
3131
  if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
@@ -3045,84 +3137,21 @@ jobs:
3045
3137
  app_location: '/'
3046
3138
  api_location: ''
3047
3139
  output_location: ''
3140
+ app_build_command: 'npm run build'
3048
3141
  env:
3049
3142
  NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
3050
3143
  `;
3051
- fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
3052
- // deploy-functions.yml
3053
- const functionsWorkflow = getGitHubFunctionsWorkflow(pm, backendLanguage);
3054
- fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
3055
- console.log('✅ GitHub Actions workflows created\n');
3056
3144
  }
3057
3145
  async function createAzurePipelines(projectDir, pm, backendLanguage) {
3058
3146
  console.log('📦 Creating Azure Pipelines...\n');
3059
- const pmCmd = (0, package_manager_1.getCommands)(pm);
3060
- const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
3147
+ const pipelinesPm = pm;
3061
3148
  const pipelinesDir = path.join(projectDir, 'pipelines');
3062
3149
  fs.mkdirSync(pipelinesDir, { recursive: true });
3063
3150
  // swa.yml
3064
- const swaPipeline = `trigger:
3065
- branches:
3066
- include:
3067
- - main
3068
- paths:
3069
- include:
3070
- - app/**
3071
- - components/**
3072
- - lib/**
3073
- - shared/**
3074
- - public/**
3075
- - package.json
3076
- - next.config.js
3077
-
3078
- pr:
3079
- branches:
3080
- include:
3081
- - main
3082
- paths:
3083
- include:
3084
- - app/**
3085
- - components/**
3086
- - lib/**
3087
- - shared/**
3088
- - public/**
3089
- - package.json
3090
- - next.config.js
3091
-
3092
- pool:
3093
- vmImage: 'ubuntu-latest'
3094
-
3095
- variables:
3096
- - group: azure-deployment
3097
-
3098
- steps:
3099
- - task: NodeTool@0
3100
- inputs:
3101
- versionSpec: '22.x'
3102
- displayName: 'Install Node.js'
3103
- ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
3104
- - script: |
3105
- ${pmCmd.ci}
3106
- displayName: 'Install dependencies'
3107
-
3108
- - script: |
3109
- ${pmCmd.run} build
3110
- env:
3111
- NODE_ENV: production
3112
- displayName: 'Build Next.js app'
3113
-
3114
- - task: AzureStaticWebApp@0
3115
- condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
3116
- inputs:
3117
- app_location: '.'
3118
- output_location: '.next/standalone'
3119
- skip_app_build: true
3120
- azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
3121
- displayName: 'Deploy to Azure Static Web Apps'
3122
- `;
3151
+ const swaPipeline = buildAzureSwaPipeline(pipelinesPm, pm);
3123
3152
  fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
3124
3153
  // functions.yml
3125
- const functionsPipeline = getAzureFunctionsPipeline(pm, backendLanguage);
3154
+ const functionsPipeline = getAzureFunctionsPipeline(pipelinesPm, backendLanguage, pm);
3126
3155
  fs.writeFileSync(path.join(pipelinesDir, 'functions.yml'), functionsPipeline);
3127
3156
  console.log('✅ Azure Pipelines created\n');
3128
3157
  }