swallowkit 1.0.0-beta.9 → 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 +69 -1
  32. package/dist/cli/commands/init.d.ts.map +1 -1
  33. package/dist/cli/commands/init.js +788 -766
  34. package/dist/cli/commands/init.js.map +1 -1
  35. package/dist/cli/commands/scaffold.d.ts +24 -3
  36. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  37. package/dist/cli/commands/scaffold.js +408 -189
  38. package/dist/cli/commands/scaffold.js.map +1 -1
  39. package/dist/cli/index.d.ts +5 -1
  40. package/dist/cli/index.d.ts.map +1 -1
  41. package/dist/cli/index.js +335 -58
  42. package/dist/cli/index.js.map +1 -1
  43. package/dist/core/config.d.ts +13 -3
  44. package/dist/core/config.d.ts.map +1 -1
  45. package/dist/core/config.js +151 -8
  46. package/dist/core/config.js.map +1 -1
  47. package/dist/core/mock/connector-mock-server.d.ts +101 -0
  48. package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
  49. package/dist/core/mock/connector-mock-server.js +480 -0
  50. package/dist/core/mock/connector-mock-server.js.map +1 -0
  51. package/dist/core/mock/zod-mock-generator.d.ts +14 -0
  52. package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
  53. package/dist/core/mock/zod-mock-generator.js +163 -0
  54. package/dist/core/mock/zod-mock-generator.js.map +1 -0
  55. package/dist/core/operations/auth-operations.d.ts +61 -0
  56. package/dist/core/operations/auth-operations.d.ts.map +1 -0
  57. package/dist/core/operations/auth-operations.js +272 -0
  58. package/dist/core/operations/auth-operations.js.map +1 -0
  59. package/dist/core/operations/create-model.d.ts +15 -0
  60. package/dist/core/operations/create-model.d.ts.map +1 -0
  61. package/dist/core/operations/create-model.js +185 -0
  62. package/dist/core/operations/create-model.js.map +1 -0
  63. package/dist/core/operations/file-session.d.ts +56 -0
  64. package/dist/core/operations/file-session.d.ts.map +1 -0
  65. package/dist/core/operations/file-session.js +203 -0
  66. package/dist/core/operations/file-session.js.map +1 -0
  67. package/dist/core/operations/provision-operations.d.ts +52 -0
  68. package/dist/core/operations/provision-operations.d.ts.map +1 -0
  69. package/dist/core/operations/provision-operations.js +211 -0
  70. package/dist/core/operations/provision-operations.js.map +1 -0
  71. package/dist/core/operations/runtime.d.ts +37 -0
  72. package/dist/core/operations/runtime.d.ts.map +1 -0
  73. package/dist/core/operations/runtime.js +248 -0
  74. package/dist/core/operations/runtime.js.map +1 -0
  75. package/dist/core/operations/scaffold-apply.d.ts +21 -0
  76. package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
  77. package/dist/core/operations/scaffold-apply.js +57 -0
  78. package/dist/core/operations/scaffold-apply.js.map +1 -0
  79. package/dist/core/operations/scaffold-machine.d.ts +16 -0
  80. package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
  81. package/dist/core/operations/scaffold-machine.js +51 -0
  82. package/dist/core/operations/scaffold-machine.js.map +1 -0
  83. package/dist/core/operations/scaffold-plan.d.ts +43 -0
  84. package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
  85. package/dist/core/operations/scaffold-plan.js +187 -0
  86. package/dist/core/operations/scaffold-plan.js.map +1 -0
  87. package/dist/core/project/agent-instructions.d.ts +29 -0
  88. package/dist/core/project/agent-instructions.d.ts.map +1 -0
  89. package/dist/core/project/agent-instructions.js +160 -0
  90. package/dist/core/project/agent-instructions.js.map +1 -0
  91. package/dist/core/project/artifacts.d.ts +58 -0
  92. package/dist/core/project/artifacts.d.ts.map +1 -0
  93. package/dist/core/project/artifacts.js +179 -0
  94. package/dist/core/project/artifacts.js.map +1 -0
  95. package/dist/core/project/boundaries.d.ts +50 -0
  96. package/dist/core/project/boundaries.d.ts.map +1 -0
  97. package/dist/core/project/boundaries.js +117 -0
  98. package/dist/core/project/boundaries.js.map +1 -0
  99. package/dist/core/project/capabilities.d.ts +58 -0
  100. package/dist/core/project/capabilities.d.ts.map +1 -0
  101. package/dist/core/project/capabilities.js +135 -0
  102. package/dist/core/project/capabilities.js.map +1 -0
  103. package/dist/core/project/drift.d.ts +29 -0
  104. package/dist/core/project/drift.d.ts.map +1 -0
  105. package/dist/core/project/drift.js +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 +366 -2
  208. package/src/__tests__/jest-globals.d.ts +8 -0
  209. package/src/__tests__/machine.test.ts +371 -0
  210. package/src/__tests__/mcp.test.ts +253 -0
  211. package/src/__tests__/model-parser.test.ts +142 -0
  212. package/src/__tests__/multi-auth.test.ts +160 -0
  213. package/src/__tests__/nextjs-generator.test.ts +8 -0
  214. package/src/__tests__/package-manager.test.ts +59 -4
  215. package/src/__tests__/plan-apply.test.ts +499 -0
  216. package/src/__tests__/python-uv.test.ts +48 -0
  217. package/src/__tests__/scaffold.test.ts +287 -24
  218. package/src/__tests__/tsconfig.json +15 -0
  219. package/src/__tests__/ui-generator.test.ts +104 -0
  220. package/src/__tests__/zod-mock-generator.test.ts +132 -0
  221. package/src/cli/commands/add-auth.ts +853 -0
  222. package/src/cli/commands/add-connector.ts +159 -0
  223. package/src/cli/commands/cosmos-worktree-isolation.ts +130 -0
  224. package/src/cli/commands/create-model.ts +21 -100
  225. package/src/cli/commands/dev-seeds.ts +278 -7
  226. package/src/cli/commands/dev.ts +607 -160
  227. package/src/cli/commands/init.ts +851 -786
  228. package/src/cli/commands/scaffold.ts +531 -236
  229. package/src/cli/index.ts +338 -60
  230. package/src/core/config.ts +145 -9
  231. package/src/core/mock/connector-mock-server.ts +555 -0
  232. package/src/core/mock/zod-mock-generator.ts +205 -0
  233. package/src/core/operations/auth-operations.ts +360 -0
  234. package/src/core/operations/create-model.ts +191 -0
  235. package/src/core/operations/file-session.ts +222 -0
  236. package/src/core/operations/provision-operations.ts +297 -0
  237. package/src/core/operations/runtime.ts +258 -0
  238. package/src/core/operations/scaffold-apply.ts +106 -0
  239. package/src/core/operations/scaffold-machine.ts +77 -0
  240. package/src/core/operations/scaffold-plan.ts +215 -0
  241. package/src/core/project/agent-instructions.ts +167 -0
  242. package/src/core/project/agent-templates/AGENTS.md +504 -0
  243. package/src/core/project/agent-templates/CLAUDE.md +31 -0
  244. package/src/core/project/agent-templates/azure-functions.instructions.md +22 -0
  245. package/src/core/project/agent-templates/bff-routes.instructions.md +18 -0
  246. package/src/core/project/agent-templates/copilot-instructions.md +27 -0
  247. package/src/core/project/agent-templates/shared-models.instructions.md +72 -0
  248. package/src/core/project/artifacts.ts +205 -0
  249. package/src/core/project/boundaries.ts +170 -0
  250. package/src/core/project/capabilities.ts +193 -0
  251. package/src/core/project/drift.ts +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,17 +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;
43
48
  exports.buildSwallowKitConfigSource = buildSwallowKitConfigSource;
44
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;
45
61
  const fs = __importStar(require("fs"));
46
62
  const path = __importStar(require("path"));
47
63
  const child_process_1 = require("child_process");
48
64
  const prompts_1 = __importDefault(require("prompts"));
49
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
+ }
50
74
  const BACKEND_LANGUAGE_CHOICES = [
51
75
  { title: "TypeScript", value: "typescript" },
52
76
  { title: "C#", value: "csharp" },
@@ -58,6 +82,65 @@ function usesNodeFunctionsProject(backendLanguage) {
58
82
  function getBackendLanguageLabel(backendLanguage) {
59
83
  return BACKEND_LANGUAGE_CHOICES.find((choice) => choice.value === backendLanguage)?.title || backendLanguage;
60
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
+ }
61
144
  function getFunctionsWorkerRuntime(backendLanguage) {
62
145
  if (backendLanguage === "csharp") {
63
146
  return "dotnet-isolated";
@@ -69,13 +152,34 @@ function getFunctionsWorkerRuntime(backendLanguage) {
69
152
  }
70
153
  function getFunctionsRuntimeConfig(backendLanguage) {
71
154
  if (backendLanguage === "csharp") {
72
- return { name: "dotnet-isolated", version: "8.0" };
155
+ return { name: "dotnet-isolated", version: "10.0" };
73
156
  }
74
157
  if (backendLanguage === "python") {
75
158
  return { name: "python", version: "3.11" };
76
159
  }
77
160
  return { name: "node", version: "22" };
78
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
+ }
79
183
  async function promptBackendLanguage() {
80
184
  const response = await (0, prompts_1.default)({
81
185
  type: "select",
@@ -100,37 +204,64 @@ async function promptCiCd() {
100
204
  });
101
205
  return response.cicd || 'skip';
102
206
  }
103
- async function promptAzureConfig() {
104
- const cosmosResponse = await (0, prompts_1.default)({
105
- type: 'select',
106
- name: 'mode',
107
- message: 'Cosmos DB mode (affects cost):',
108
- choices: [
109
- { title: 'Free Tier (1000 RU/s free, best for first project)', value: 'freetier' },
110
- { title: 'Serverless (pay-per-use, flexible)', value: 'serverless' }
111
- ],
112
- initial: 0
113
- });
114
- const vnetResponse = await (0, prompts_1.default)({
115
- type: 'select',
116
- name: 'vnet',
117
- message: 'Network security:',
118
- choices: [
119
- { title: 'VNet Integration (recommended) - Cosmos DB via Private Endpoint', value: 'outbound' },
120
- { title: 'None - Public endpoints, simplest but least secure', value: 'none' }
121
- ],
122
- initial: 0
123
- });
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
+ }
124
250
  return {
125
- cosmosDbMode: cosmosResponse.mode || 'freetier',
126
- vnetOption: vnetResponse.vnet || 'outbound'
251
+ cosmosDbMode: cosmosDbMode || 'freetier',
252
+ vnetOption: vnetOption || 'outbound',
253
+ swaPlan: swaPlan || 'standard'
127
254
  };
128
255
  }
129
256
  const VALID_CICD = ['github', 'azure', 'skip'];
130
257
  const VALID_BACKEND_LANGUAGE = ['typescript', 'csharp', 'python'];
131
258
  const VALID_COSMOS_DB_MODE = ['freetier', 'serverless'];
132
259
  const VALID_VNET = ['none', 'outbound'];
260
+ const VALID_SWA_PLAN = ['free', 'standard'];
133
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
+ }
134
265
  if (options.cicd && !VALID_CICD.includes(options.cicd)) {
135
266
  console.error(`❌ Invalid --cicd value: "${options.cicd}". Must be: ${VALID_CICD.join(', ')}`);
136
267
  process.exit(1);
@@ -147,6 +278,10 @@ function validateInitFlags(options) {
147
278
  console.error(`❌ Invalid --vnet value: "${options.vnet}". Must be: ${VALID_VNET.join(', ')}`);
148
279
  process.exit(1);
149
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
+ }
150
285
  }
151
286
  async function initCommand(options) {
152
287
  // Validate flag values before doing anything
@@ -154,7 +289,7 @@ async function initCommand(options) {
154
289
  console.log(`🚀 Initializing SwallowKit project: ${options.name}`);
155
290
  console.log(`📋 Template: ${options.template}`);
156
291
  // Detect package manager from invocation context (npx → npm, pnpm dlx → pnpm)
157
- const pm = (0, package_manager_1.detectFromUserAgent)();
292
+ const pm = options.packageManager || (0, package_manager_1.detectFromUserAgent)();
158
293
  const pmCmd = (0, package_manager_1.getCommands)(pm);
159
294
  console.log(`📦 Package manager: ${pm}`);
160
295
  const projectDir = path.join(process.cwd(), options.name);
@@ -164,12 +299,30 @@ async function initCommand(options) {
164
299
  console.error(`❌ Directory "${options.name}" already exists.`);
165
300
  process.exit(1);
166
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
+ }
167
322
  // Use flag values if provided, otherwise prompt interactively
168
323
  const cicdProvider = options.cicd || await promptCiCd();
169
324
  const backendLanguage = options.backendLanguage || await promptBackendLanguage();
170
- const azureConfig = (options.cosmosDbMode && options.vnet)
171
- ? { cosmosDbMode: options.cosmosDbMode, vnetOption: options.vnet }
172
- : await promptAzureConfig();
325
+ const azureConfig = await promptAzureConfig(options);
173
326
  // Create Next.js project with create-next-app
174
327
  await createNextJsProject(options.name, pm);
175
328
  // Upgrade Next.js to specified version (or latest) to avoid cached old versions
@@ -185,6 +338,7 @@ async function initCommand(options) {
185
338
  else if (cicdProvider === 'azure') {
186
339
  await createAzurePipelines(projectDir, pm, backendLanguage);
187
340
  }
341
+ await (0, manifest_1.syncProjectManifest)(projectDir);
188
342
  // Initialize Git repository and create initial commit
189
343
  try {
190
344
  // Try git init with -b main (Git 2.28+), fallback to git init
@@ -228,11 +382,11 @@ async function initCommand(options) {
228
382
  console.log(`\n✅ Project "${options.name}" created successfully!`);
229
383
  console.log("\n📝 Next steps:");
230
384
  console.log(` cd ${options.name}`);
231
- console.log(` ${pmCmd.dlx} swallowkit create-model <name> # Create your first model`);
232
- console.log(` ${pmCmd.dlx} swallowkit scaffold shared/models/<name>.ts # Generate CRUD code`);
233
- 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`);
234
388
  console.log("\n🚀 Deploy to Azure:");
235
- console.log(` ${pmCmd.dlx} swallowkit provision --resource-group <name>`);
389
+ console.log(` ${pmCmd.exec} swallowkit provision --resource-group <name>`);
236
390
  if (cicdProvider !== 'skip') {
237
391
  console.log(" Configure CI/CD secrets and push to repository");
238
392
  }
@@ -254,7 +408,7 @@ async function createNextJsProject(projectName, pm) {
254
408
  // for npm use "npx create-next-app@latest ..."
255
409
  const baseArgs = pm === 'pnpm'
256
410
  ? ['dlx', 'create-next-app@latest']
257
- : ['create-next-app@latest'];
411
+ : ['--yes', '--package', 'create-next-app@latest', '--', 'create-next-app'];
258
412
  const args = [
259
413
  ...baseArgs,
260
414
  projectName,
@@ -263,6 +417,7 @@ async function createNextJsProject(projectName, pm) {
263
417
  '--app',
264
418
  '--no-src',
265
419
  '--disable-git',
420
+ '--skip-install',
266
421
  '--import-alias',
267
422
  '@/*',
268
423
  ...(pmCmd.createNextAppFlag ? [pmCmd.createNextAppFlag] : []),
@@ -288,65 +443,164 @@ async function createNextJsProject(projectName, pm) {
288
443
  });
289
444
  }
290
445
  async function upgradeNextJs(projectDir, version, pm) {
291
- return new Promise((resolve, reject) => {
292
- console.log(`\n📦 Installing Next.js ${version} (to ensure latest security patches)...\n`);
293
- // pnpm: pnpm add next@... ; npm: npm install next@...
294
- const args = pm === 'pnpm'
295
- ? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact']
296
- : ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact'];
297
- const child = (0, child_process_1.spawn)(pm, args, {
298
- cwd: projectDir,
299
- stdio: 'inherit',
300
- shell: true,
301
- });
302
- child.on('close', (code) => {
303
- if (code !== 0) {
304
- 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
+ }
305
490
  }
306
- else {
307
- console.log(`\n✅ Next.js ${version} installed\n`);
308
- 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 });
309
523
  }
310
- });
311
- child.on('error', (error) => {
312
- reject(error);
313
- });
314
- });
524
+ fs.renameSync(hiddenPath, nodeModulesPath);
525
+ }
526
+ for (const [filePath, content] of originalFiles) {
527
+ fs.writeFileSync(filePath, content);
528
+ }
529
+ }
315
530
  }
316
- 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) {
317
549
  return new Promise((resolve, reject) => {
318
- console.log('\n📦 Installing dependencies...\n');
319
- 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, {
320
554
  cwd: projectDir,
321
- stdio: 'inherit',
555
+ stdio,
322
556
  shell: true,
323
557
  });
324
- child.on('close', (code) => {
325
- if (code !== 0) {
326
- reject(new Error(`${pm} install exited with code ${code}`));
327
- }
328
- else {
329
- console.log('\n✅ Dependencies installed\n');
330
- resolve();
331
- }
332
- });
333
- child.on('error', (error) => {
334
- reject(error);
335
- });
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);
336
571
  });
337
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
+ }
338
589
  function injectSwallowKitNextConfig(nextConfigContent, projectName) {
339
- 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`);
340
591
  }
341
592
  function buildCSharpFunctionsProgramSource() {
342
- return `using Microsoft.Extensions.DependencyInjection;
593
+ return `using Microsoft.Azure.Functions.Worker;
594
+ using Microsoft.Extensions.DependencyInjection;
343
595
  using Microsoft.Extensions.Hosting;
596
+ using Microsoft.Azure.Functions.Worker.ApplicationInsights;
344
597
 
345
598
  var host = new HostBuilder()
346
599
  .ConfigureFunctionsWorkerDefaults()
347
600
  .ConfigureServices(services =>
348
601
  {
349
602
  services.AddApplicationInsightsTelemetryWorkerService();
603
+ services.ConfigureFunctionsApplicationInsights();
350
604
  })
351
605
  .Build();
352
606
 
@@ -356,7 +610,7 @@ host.Run();
356
610
  function buildCSharpFunctionsProjectSource() {
357
611
  return `<Project Sdk="Microsoft.NET.Sdk">
358
612
  <PropertyGroup>
359
- <TargetFramework>net8.0</TargetFramework>
613
+ <TargetFramework>net10.0</TargetFramework>
360
614
  <AzureFunctionsVersion>v4</AzureFunctionsVersion>
361
615
  <OutputType>Exe</OutputType>
362
616
  <ImplicitUsings>enable</ImplicitUsings>
@@ -370,12 +624,12 @@ function buildCSharpFunctionsProjectSource() {
370
624
  <ItemGroup>
371
625
  <PackageReference Include="Microsoft.Azure.Cosmos" Version="3.47.0" />
372
626
  <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
373
- <PackageReference Include="Azure.Identity" Version="1.13.2" />
374
- <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.23.0" />
375
- <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.2.0" />
376
- <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" />
377
631
  <PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" />
378
- <PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="1.2.0" />
632
+ <PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
379
633
  </ItemGroup>
380
634
  </Project>
381
635
  `;
@@ -395,55 +649,92 @@ function buildSwallowKitConfigSource(backendLanguage) {
395
649
  }
396
650
  `;
397
651
  }
398
- function buildGeneratedProjectDependencies(projectName) {
652
+ function buildGeneratedProjectDependencies(projectName, pm = 'pnpm') {
399
653
  return {
400
654
  '@azure/cosmos': '^4.0.0',
401
655
  'applicationinsights': '^3.3.0',
402
- [`@${projectName}/shared`]: '*',
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,
403
681
  };
404
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
+ }
405
700
  async function addSwallowKitFiles(projectDir, options, cicdChoice, azureConfig, pm, backendLanguage) {
406
701
  console.log('📦 Adding SwallowKit files...\n');
407
702
  const projectName = options.name;
408
703
  // 1. Update package.json to add runtime dependencies for generated projects
409
704
  const packageJsonPath = path.join(projectDir, 'package.json');
410
705
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
411
- // 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.
412
707
  packageJson.dependencies = {
413
708
  ...packageJson.dependencies,
414
- ...buildGeneratedProjectDependencies(projectName),
709
+ ...buildGeneratedProjectDependencies(projectName, pm),
710
+ };
711
+ packageJson.devDependencies = {
712
+ ...packageJson.devDependencies,
713
+ ...buildGeneratedProjectDevDependencies(),
415
714
  };
416
- if (backendLanguage !== "typescript") {
417
- packageJson.devDependencies = {
418
- ...packageJson.devDependencies,
419
- '@openapitools/openapi-generator-cli': '^2.21.0',
420
- };
421
- }
422
715
  packageJson.scripts = {
423
716
  ...packageJson.scripts,
424
717
  'build': (0, package_manager_1.getBuildScript)(pm),
425
718
  'start': 'next start',
426
719
  'functions:start': (0, package_manager_1.getFunctionsStartScript)(pm, backendLanguage),
427
720
  };
428
- if (pm === 'pnpm') {
429
- packageJson.packageManager = 'pnpm@latest';
430
- }
431
721
  packageJson.engines = {
432
- node: '20.x',
722
+ node: '>=22.14.0',
723
+ ...(pm === 'pnpm' ? { pnpm: '>=11 <13' } : {}),
433
724
  };
434
- // 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.
435
730
  const workspacePackages = usesNodeFunctionsProject(backendLanguage) ? ['shared', 'functions'] : ['shared'];
436
731
  const wsConfig = (0, package_manager_1.getWorkspaceConfig)(pm, workspacePackages);
437
- if (wsConfig.type === 'file') {
438
- // pnpm: workspaces are defined in pnpm-workspace.yaml
439
- delete packageJson.workspaces;
440
- }
441
- else {
442
- // npm: workspaces are defined in package.json
732
+ packageJson.workspaces = workspacePackages;
733
+ if (wsConfig.type !== 'file') {
443
734
  packageJson.workspaces = wsConfig.value;
444
735
  }
445
736
  fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
446
- // Create workspace config file if needed (pnpm-workspace.yaml)
737
+ // Create pnpm-workspace.yaml for local pnpm development when needed.
447
738
  if (wsConfig.type === 'file') {
448
739
  fs.writeFileSync(path.join(projectDir, wsConfig.filename), wsConfig.content);
449
740
  }
@@ -493,6 +784,13 @@ function getFunctionsBaseUrl(): string {
493
784
  return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
494
785
  }
495
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
+
496
794
  /**
497
795
  * Simple HTTP client for calling backend APIs
498
796
  * Use this to make requests to BFF API routes (which forward to Azure Functions)
@@ -500,10 +798,11 @@ function getFunctionsBaseUrl(): string {
500
798
  async function request<T>(
501
799
  endpoint: string,
502
800
  method: 'GET' | 'POST' | 'PUT' | 'DELETE',
503
- body?: any,
801
+ body?: unknown,
504
802
  queryParams?: Record<string, string>
505
803
  ): Promise<T> {
506
804
  const functionsBaseUrl = getFunctionsBaseUrl();
805
+ const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
507
806
  let url = \`\${functionsBaseUrl}\${endpoint}\`;
508
807
  if (queryParams) {
509
808
  const params = new URLSearchParams(queryParams);
@@ -515,6 +814,7 @@ async function request<T>(
515
814
  method,
516
815
  headers: {
517
816
  'Content-Type': 'application/json',
817
+ ...keyHeaders,
518
818
  },
519
819
  body: body ? JSON.stringify(body) : undefined,
520
820
  });
@@ -570,14 +870,14 @@ export const api = {
570
870
  /**
571
871
  * Make a POST request
572
872
  */
573
- post: <T>(endpoint: string, body?: any): Promise<T> => {
873
+ post: <T>(endpoint: string, body?: unknown): Promise<T> => {
574
874
  return request<T>(endpoint, 'POST', body);
575
875
  },
576
876
 
577
877
  /**
578
878
  * Make a PUT request
579
879
  */
580
- put: <T>(endpoint: string, body?: any): Promise<T> => {
880
+ put: <T>(endpoint: string, body?: unknown): Promise<T> => {
581
881
  return request<T>(endpoint, 'PUT', body);
582
882
  },
583
883
 
@@ -596,6 +896,7 @@ export const api = {
596
896
  // 6. Create .env.example
597
897
  const envExample = `# Azure Functions Backend URL
598
898
  BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071
899
+ BACKEND_FUNCTIONS_KEY=
599
900
 
600
901
  # Azure Configuration
601
902
  AZURE_RESOURCE_GROUP=your-resource-group
@@ -634,7 +935,7 @@ export async function register() {
634
935
  const originalConsoleError = console.error;
635
936
  const originalConsoleWarn = console.warn;
636
937
 
637
- console.log = function(...args: any[]) {
938
+ console.log = function(...args: unknown[]) {
638
939
  originalConsoleLog.apply(console, args);
639
940
  const message = args.map(arg =>
640
941
  typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
@@ -645,7 +946,7 @@ export async function register() {
645
946
  });
646
947
  };
647
948
 
648
- console.error = function(...args: any[]) {
949
+ console.error = function(...args: unknown[]) {
649
950
  originalConsoleError.apply(console, args);
650
951
  const message = args.map(arg =>
651
952
  typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
@@ -656,7 +957,7 @@ export async function register() {
656
957
  });
657
958
  };
658
959
 
659
- console.warn = function(...args: any[]) {
960
+ console.warn = function(...args: unknown[]) {
660
961
  originalConsoleWarn.apply(console, args);
661
962
  const message = args.map(arg =>
662
963
  typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
@@ -679,6 +980,7 @@ export async function register() {
679
980
  const envLocalContent = [
680
981
  '# Azure Functions Backend URL (Local)',
681
982
  'BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071',
983
+ 'BACKEND_FUNCTIONS_KEY=',
682
984
  ''
683
985
  ].join('\n');
684
986
  fs.writeFileSync(path.join(projectDir, '.env.local'), envLocalContent);
@@ -716,7 +1018,7 @@ export async function register() {
716
1018
  // 18. Create README.md
717
1019
  createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, backendLanguage);
718
1020
  // 19. Create AI agent instruction files (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
719
- createAiAgentFiles(projectDir, projectName, backendLanguage);
1021
+ createAiAgentFiles(projectDir, projectName, backendLanguage, pm);
720
1022
  }
721
1023
  async function createSharedPackage(projectDir, projectName) {
722
1024
  console.log('📦 Creating shared workspace package for Zod models...\n');
@@ -725,6 +1027,7 @@ async function createSharedPackage(projectDir, projectName) {
725
1027
  fs.mkdirSync(modelsDir, { recursive: true });
726
1028
  // shared/package.json
727
1029
  const sharedPackageJson = {
1030
+ private: true,
728
1031
  name: `@${projectName}/shared`,
729
1032
  version: '1.0.0',
730
1033
  description: 'Shared Zod models — Single Source of Truth for validation schemas',
@@ -743,26 +1046,7 @@ async function createSharedPackage(projectDir, projectName) {
743
1046
  };
744
1047
  fs.writeFileSync(path.join(sharedDir, 'package.json'), JSON.stringify(sharedPackageJson, null, 2));
745
1048
  // shared/tsconfig.json
746
- const sharedTsConfig = {
747
- compilerOptions: {
748
- target: 'ES2020',
749
- module: 'commonjs',
750
- moduleResolution: 'node',
751
- lib: ['ES2020'],
752
- outDir: 'dist',
753
- rootDir: '.',
754
- declaration: true,
755
- declarationMap: true,
756
- sourceMap: true,
757
- strict: true,
758
- esModuleInterop: true,
759
- skipLibCheck: true,
760
- forceConsistentCasingInFileNames: true,
761
- },
762
- include: ['index.ts', 'models/**/*'],
763
- exclude: ['node_modules', 'dist'],
764
- };
765
- 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));
766
1050
  // shared/index.ts (empty re-export file, scaffold will add entries)
767
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`);
768
1052
  // shared/.gitignore
@@ -835,8 +1119,9 @@ tsconfig.json
835
1119
  `);
836
1120
  }
837
1121
  else if (backendLanguage === 'python') {
838
- gitignoreLines.unshift('.venv', '__pycache__', '.python_packages');
1122
+ gitignoreLines.unshift('.venv', '.codegen-venv', '__pycache__', '.python_packages');
839
1123
  fs.writeFileSync(path.join(functionsDir, '.funcignore'), `.venv
1124
+ .codegen-venv
840
1125
  __pycache__
841
1126
  .pytest_cache
842
1127
  .mypy_cache
@@ -855,8 +1140,8 @@ tests
855
1140
  }
856
1141
  fs.writeFileSync(path.join(functionsDir, '.gitignore'), `${gitignoreLines.join('\n')}\n`);
857
1142
  }
858
- function createTypeScriptFunctionsProject(projectDir, functionsDir, pm) {
859
- const functionsPackageJson = {
1143
+ function buildTypeScriptFunctionsPackageJson(projectName, pm = 'pnpm') {
1144
+ return {
860
1145
  name: 'functions',
861
1146
  version: '1.0.0',
862
1147
  description: 'Azure Functions backend',
@@ -871,13 +1156,16 @@ function createTypeScriptFunctionsProject(projectDir, functionsDir, pm) {
871
1156
  '@azure/cosmos': '^4.0.0',
872
1157
  '@azure/identity': '^4.0.0',
873
1158
  zod: '>=3.25.0',
874
- [`@${path.basename(projectDir)}/shared`]: '*',
1159
+ [`@${projectName}/shared`]: pm === 'pnpm' ? 'workspace:*' : 'file:../shared',
875
1160
  },
876
1161
  devDependencies: {
877
1162
  '@types/node': '^20.0.0',
878
1163
  typescript: '^5.0.0',
879
1164
  },
880
1165
  };
1166
+ }
1167
+ function createTypeScriptFunctionsProject(projectDir, functionsDir, pm) {
1168
+ const functionsPackageJson = buildTypeScriptFunctionsPackageJson(path.basename(projectDir), pm);
881
1169
  fs.writeFileSync(path.join(functionsDir, 'package.json'), JSON.stringify(functionsPackageJson, null, 2));
882
1170
  const sharedPkgName = `@${path.basename(projectDir)}/shared`;
883
1171
  const functionsTsConfig = {
@@ -948,7 +1236,7 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
948
1236
 
949
1237
  app.http('greet', {
950
1238
  methods: ['GET', 'POST'],
951
- authLevel: 'anonymous',
1239
+ authLevel: 'function',
952
1240
  handler: greet
953
1241
  });
954
1242
  `);
@@ -958,6 +1246,8 @@ function createCSharpFunctionsProject(projectDir, functionsDir) {
958
1246
  const projectPascal = projectBaseName.charAt(0).toUpperCase() + projectBaseName.slice(1);
959
1247
  const csprojName = `${projectPascal}.Functions.csproj`;
960
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)());
961
1251
  fs.writeFileSync(path.join(functionsDir, 'Program.cs'), buildCSharpFunctionsProgramSource());
962
1252
  const crudDir = path.join(functionsDir, 'Crud');
963
1253
  fs.mkdirSync(crudDir, { recursive: true });
@@ -971,7 +1261,7 @@ public sealed class GreetFunction
971
1261
  {
972
1262
  [Function("greet")]
973
1263
  public async Task<HttpResponseData> Run(
974
- [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "greet")] HttpRequestData request)
1264
+ [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "greet")] HttpRequestData request)
975
1265
  {
976
1266
  var query = request.Url.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
977
1267
  var name = "SwallowKit";
@@ -997,10 +1287,12 @@ public sealed class GreetFunction
997
1287
  }
998
1288
  function createPythonFunctionsProject(projectDir, functionsDir) {
999
1289
  fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
1290
+ ensureProjectGitignoreEntry(projectDir, '.uv');
1000
1291
  fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
1001
1292
  azure-cosmos>=4.9.0
1002
1293
  azure-identity>=1.19.0
1003
1294
  `);
1295
+ fs.writeFileSync(path.join(functionsDir, 'requirements.codegen.txt'), (0, native_schema_generator_1.buildPythonCodegenRequirementsSource)());
1004
1296
  const blueprintsDir = path.join(functionsDir, 'blueprints');
1005
1297
  fs.mkdirSync(blueprintsDir, { recursive: true });
1006
1298
  fs.writeFileSync(path.join(blueprintsDir, '__init__.py'), '');
@@ -1029,12 +1321,25 @@ def greet(req: func.HttpRequest) -> func.HttpResponse:
1029
1321
 
1030
1322
  from blueprints.greet import bp as greet_bp
1031
1323
 
1032
- app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
1324
+ app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
1033
1325
 
1034
1326
  app.register_blueprint(greet_bp)
1035
1327
  # SwallowKit scaffold registrations
1036
1328
  `);
1037
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
+ }
1038
1343
  async function createBffApiRoute(projectDir) {
1039
1344
  console.log('📦 Creating BFF API route...\n');
1040
1345
  const apiDir = path.join(projectDir, 'app', 'api', 'greet');
@@ -1195,7 +1500,7 @@ export default function Home() {
1195
1500
  Create your first model with Zod and generate CRUD operations automatically.
1196
1501
  </p>
1197
1502
  <code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
1198
- ${pmCmd.dlx} swallowkit scaffold shared/models/your-model.ts
1503
+ ${pmCmd.exec} swallowkit scaffold shared/models/your-model.ts
1199
1504
  </code>
1200
1505
  </div>
1201
1506
  </section>
@@ -1241,17 +1546,18 @@ function createReadme(projectDir, projectName, cicdChoice, azureConfig, pm, back
1241
1546
  const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
1242
1547
  const schemaBridgeDescription = backendLanguage === 'typescript'
1243
1548
  ? 'Zod (shared between frontend and backend)'
1244
- : `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/)`;
1245
1550
  const functionsTree = backendLanguage === 'typescript'
1246
1551
  ? `│ └── src/\n│ └── greet.ts # Sample function`
1247
1552
  : backendLanguage === 'csharp'
1248
- ? `│ ├── Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ # OpenAPI-derived C# models`
1249
- : `│ ├── 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`;
1250
1555
  const backendScaffoldNote = backendLanguage === 'typescript'
1251
1556
  ? '- Azure Functions CRUD endpoints'
1252
- : `- 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);
1253
1559
  const pythonLocalDevNote = backendLanguage === 'python'
1254
- ? `\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`
1255
1561
  : '';
1256
1562
  const readme = `# ${projectName}
1257
1563
 
@@ -1280,7 +1586,7 @@ This project was initialized with the following settings:
1280
1586
 
1281
1587
  Before you begin, ensure you have the following installed:
1282
1588
 
1283
- 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\`` : ''}
1284
1590
  ${pm === 'pnpm' ? '3' : '2'}. **Azure CLI**: Required for provisioning Azure resources
1285
1591
  - Install: \`winget install Microsoft.AzureCLI\` (Windows)
1286
1592
  - Or: [Download](https://aka.ms/installazurecliwindows)
@@ -1288,7 +1594,7 @@ ${pm === 'pnpm' ? '4' : '3'}. **Azure Cosmos DB Emulator**: Required for local d
1288
1594
  - Windows: \`winget install Microsoft.Azure.CosmosEmulator\`
1289
1595
  - Or: [Download](https://aka.ms/cosmosdb-emulator)
1290
1596
  - Docker: \`docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator\`
1291
- ${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
1292
1598
 
1293
1599
  ## 📁 Project Structure
1294
1600
 
@@ -1314,7 +1620,7 @@ ${functionsTree}
1314
1620
  Define your data model with Zod schema:
1315
1621
 
1316
1622
  \`\`\`bash
1317
- ${pmCmd.dlx} swallowkit create-model <model-name>
1623
+ ${pmCmd.exec} swallowkit create-model <model-name>
1318
1624
  \`\`\`
1319
1625
 
1320
1626
  This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to define your schema.
@@ -1324,7 +1630,7 @@ This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to defin
1324
1630
  Generate complete CRUD operations (Functions, API routes, UI):
1325
1631
 
1326
1632
  \`\`\`bash
1327
- ${pmCmd.dlx} swallowkit scaffold shared/models/<model-name>.ts
1633
+ ${pmCmd.exec} swallowkit scaffold shared/models/<model-name>.ts
1328
1634
  \`\`\`
1329
1635
 
1330
1636
  This generates:
@@ -1336,7 +1642,7 @@ ${backendScaffoldNote}
1336
1642
  ### 3. Start Development Servers
1337
1643
 
1338
1644
  \`\`\`bash
1339
- ${pmCmd.dlx} swallowkit dev
1645
+ ${pmCmd.exec} swallowkit dev
1340
1646
  \`\`\`
1341
1647
 
1342
1648
  This starts:
@@ -1354,7 +1660,7 @@ ${pythonLocalDevNote}
1354
1660
  Create all required Azure resources using Bicep:
1355
1661
 
1356
1662
  \`\`\`bash
1357
- ${pmCmd.dlx} swallowkit provision --resource-group <rg-name>
1663
+ ${pmCmd.exec} swallowkit provision --resource-group <rg-name>
1358
1664
  \`\`\`
1359
1665
 
1360
1666
  This creates:
@@ -1410,10 +1716,10 @@ func azure functionapp publish func-${projectName}
1410
1716
 
1411
1717
  ## 🔧 Available Commands
1412
1718
 
1413
- - \`${pmCmd.dlx} swallowkit create-model <name>\` - Create a new data model
1414
- - \`${pmCmd.dlx} swallowkit scaffold <model-file>\` - Generate CRUD code
1415
- - \`${pmCmd.dlx} swallowkit dev\` - Start development servers
1416
- - \`${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
1417
1723
  ${azureConfig.vnetOption !== 'none' ? `
1418
1724
  ## 🔒 Network Security (VNet Configuration)
1419
1725
 
@@ -1460,435 +1766,89 @@ This project was generated by SwallowKit. If you encounter any issues or have su
1460
1766
  fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
1461
1767
  console.log('✅ README.md created\n');
1462
1768
  }
1463
- function createAiAgentFiles(projectDir, projectName, backendLanguage) {
1769
+ function createAiAgentFiles(projectDir, projectName, backendLanguage, pm) {
1464
1770
  console.log('🤖 Creating AI agent instruction files...\n');
1465
- const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
1466
- const functionsStructureLine = backendLanguage === 'typescript'
1467
- ? `│ └── src/ # HTTP trigger handlers with Cosmos DB bindings`
1468
- : backendLanguage === 'csharp'
1469
- ? `│ ├── Crud/ # C# HTTP trigger handlers\n│ └── generated/ # OpenAPI-derived C# schema assets`
1470
- : `│ ├── blueprints/ # Python HTTP trigger handlers\n│ └── generated/ # OpenAPI-derived Python schema assets`;
1471
- const backendSchemaNote = backendLanguage === 'typescript'
1472
- ? `- The shared package (\`@${projectName}/shared\`) is consumed by both Next.js and Azure Functions as a workspace dependency.`
1473
- : `- 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.`;
1474
- const backendRulesNote = backendLanguage === 'typescript'
1475
- ? `- 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.`
1476
- : `- 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\`.`;
1477
- // ── 1. AGENTS.md (Codex / generic agents) ──────────────────────────
1478
- const agentsMd = `# AGENTS.md
1479
-
1480
- This project was generated by **SwallowKit**.
1481
- All coding agents **must** follow the architecture and conventions described below.
1482
-
1483
- ## Architecture Overview
1484
-
1485
- This is a full-stack application deployed on Azure with a TypeScript frontend/BFF and an Azure Functions backend in ${backendLanguageLabel}.
1486
-
1487
- \`\`\`
1488
- Frontend (React / Next.js App Router)
1489
- ↓ fetch('/api/{model}', ...)
1490
- BFF Layer (Next.js API Routes)
1491
- ↓ HTTP → Azure Functions
1492
- Backend (Azure Functions)
1493
- ↓
1494
- Azure Cosmos DB (Document Database)
1495
- \`\`\`
1496
-
1497
- ### Project Structure
1498
-
1499
- \`\`\`
1500
- ${projectName}/
1501
- ├── app/ # Next.js App Router
1502
- │ ├── api/ # BFF API routes (proxy to Azure Functions)
1503
- │ └── {model}/ # UI pages per model (list, detail, create, edit)
1504
- ├── functions/ # Azure Functions (backend)
1505
- ${functionsStructureLine}
1506
- ├── shared/ # Shared workspace package
1507
- │ ├── models/ # Zod schema definitions (single source of truth)
1508
- │ └── index.ts # Re-exports all models
1509
- ├── lib/
1510
- │ └── api/ # API client utilities (backend.ts, call-function.ts)
1511
- ├── components/ # Shared React components
1512
- ├── infra/ # Bicep infrastructure-as-code files
1513
- │ ├── main.bicep
1514
- │ └── modules/
1515
- └── .github/workflows/ # CI/CD workflows (if configured)
1516
- \`\`\`
1517
-
1518
- ## Critical Design Principles
1519
-
1520
- ### 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
1521
1798
 
1522
- - \`app/api/\` routes exist **only** to proxy requests to Azure Functions.
1523
- - **Never** place business logic, database access, or direct Cosmos DB calls in Next.js API routes.
1524
- - The BFF layer may validate input/output with Zod schemas before forwarding to Functions.
1525
- - 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
1526
1801
 
1527
- Example BFF route pattern:
1802
+ @description('Functions App name used to resolve its host key after deployment')
1803
+ param functionsAppName string
1528
1804
 
1529
- \`\`\`typescript
1530
- // app/api/{model}/route.ts
1531
- import { callFunction } from '@/lib/api/call-function';
1532
- import { ModelSchema } from '@${projectName}/shared';
1533
- import { z } from 'zod/v4';
1805
+ @description('Application Insights connection string for SWA')
1806
+ param appInsightsConnectionString string
1534
1807
 
1535
- export async function GET() {
1536
- return callFunction({
1537
- method: 'GET',
1538
- path: '/api/{model}',
1539
- responseSchema: z.array(ModelSchema),
1540
- });
1808
+ resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
1809
+ name: staticWebAppName
1541
1810
  }
1542
1811
 
1543
- export async function POST(request: NextRequest) {
1544
- const body = await request.json();
1545
- return callFunction({
1546
- method: 'POST',
1547
- path: '/api/{model}',
1548
- body,
1549
- inputSchema: ModelSchema.omit({ id: true, createdAt: true, updatedAt: true }),
1550
- responseSchema: ModelSchema,
1551
- successStatus: 201,
1552
- });
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
+ }
1553
1821
  }
1554
- \`\`\`
1555
1822
 
1556
- ### 2. Zod Schemas Are the Single Source of Truth
1823
+ output configName string = staticWebAppConfig.name
1824
+ `;
1825
+ }
1826
+ async function createInfrastructure(projectDir, projectName, azureConfig, backendLanguage) {
1827
+ console.log('📦 Creating infrastructure files (Bicep)...\n');
1828
+ const infraDir = path.join(projectDir, 'infra');
1829
+ const modulesDir = path.join(infraDir, 'modules');
1830
+ fs.mkdirSync(modulesDir, { recursive: true });
1831
+ const enableVNet = azureConfig.vnetOption !== 'none';
1832
+ const functionsRuntime = getFunctionsRuntimeConfig(backendLanguage);
1833
+ const staticWebAppSku = getStaticWebAppSku(azureConfig.swaPlan);
1834
+ // main.bicep
1835
+ const mainBicep = `targetScope = 'resourceGroup'
1557
1836
 
1558
- - All data models are defined **once** as Zod schemas in \`shared/models/\`.
1559
- - TypeScript types are derived with \`z.infer<typeof Schema>\` — never define types separately.
1560
- - ${backendSchemaNote}
1837
+ @description('Project name')
1838
+ param projectName string
1561
1839
 
1562
- Model definition pattern:
1840
+ @description('Location for Functions and Cosmos DB')
1841
+ param location string = resourceGroup().location
1563
1842
 
1564
- \`\`\`typescript
1565
- // shared/models/{model}.ts
1566
- import { z } from 'zod/v4';
1843
+ @description('Location for Static Web App (must be explicitly provided)')
1844
+ param swaLocation string
1567
1845
 
1568
- export const Todo = z.object({
1569
- id: z.string(),
1570
- name: z.string().min(1),
1571
- // ... your fields
1572
- createdAt: z.string().optional(),
1573
- updatedAt: z.string().optional(),
1574
- });
1575
-
1576
- export type Todo = z.infer<typeof Todo>;
1577
- export const displayName = 'Todo';
1578
- \`\`\`
1579
-
1580
- Key rules:
1581
- - Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
1582
- - \`id\`, \`createdAt\`, and \`updatedAt\` are auto-managed by the backend. Mark them as \`optional()\` in the schema.
1583
- - Always re-export models from \`shared/index.ts\`.
1584
-
1585
- ### 3. Azure Functions Own All Business Logic and Data Access
1586
-
1587
- - ${backendRulesNote}
1588
-
1589
- ${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.`}
1590
-
1591
- ${backendLanguage === 'typescript' ? `\`\`\`typescript
1592
- // functions/src/{model}.ts
1593
- import { app } from '@azure/functions';
1594
- import { ModelSchema } from '@${projectName}/shared';
1595
-
1596
- const containerName = 'Models'; // PascalCase + 's'
1597
-
1598
- app.http('{model}-get-all', {
1599
- methods: ['GET'],
1600
- route: '{model}',
1601
- authLevel: 'anonymous',
1602
- extraInputs: [{ type: 'cosmosDB', name: 'cosmosInput', containerName, ... }],
1603
- handler: async (request, context) => {
1604
- const documents = context.extraInputs.get('cosmosInput');
1605
- const validated = z.array(ModelSchema).parse(documents);
1606
- return { status: 200, jsonBody: validated };
1607
- },
1608
- });
1609
- \`\`\`` : ''}
1610
-
1611
- ## Naming Conventions
1612
-
1613
- | Item | Convention | Example |
1614
- |------|-----------|---------|
1615
- | Model schema file | \`shared/models/{kebab-case}.ts\` | \`shared/models/todo.ts\` |
1616
- | Schema/type name | PascalCase (same name for both) | \`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\` |
1617
- | Functions handler file | backend-language specific under \`functions/\` | \`${backendLanguage === 'typescript' ? 'functions/src/todo.ts' : backendLanguage === 'csharp' ? 'functions/Crud/TodoFunctions.cs' : 'functions/blueprints/todo.py'}\` |
1618
- | Functions handler name | \`{camelCase}-{operation}\` | \`todo-get-all\`, \`todo-create\` |
1619
- | API route path | \`/api/{camelCase}\` | \`/api/todo\`, \`/api/todo/{id}\` |
1620
- | BFF route file | \`app/api/{kebab-case}/route.ts\` | \`app/api/todo/route.ts\` |
1621
- | BFF detail route | \`app/api/{kebab-case}/[id]/route.ts\` | \`app/api/todo/[id]/route.ts\` |
1622
- | UI page directory | \`app/{kebab-case}/\` | \`app/todo/page.tsx\` |
1623
- | React component | PascalCase | \`TodoForm.tsx\` |
1624
- | Cosmos DB container | PascalCase + 's' | \`Todos\` |
1625
- | Cosmos DB partition key | \`/id\` | Always \`/id\` |
1626
- | Bicep container file | \`infra/containers/{kebab-case}-container.bicep\` | \`infra/containers/todo-container.bicep\` |
1627
-
1628
- ## Adding New Models (SwallowKit CLI Skills)
1629
-
1630
- Use the SwallowKit CLI — do **not** manually create model files or CRUD boilerplate.
1631
-
1632
- ### Skill: Create a new data model
1633
-
1634
- \`\`\`bash
1635
- npx swallowkit create-model <name>
1636
- # Multiple models at once:
1637
- npx swallowkit create-model user post comment
1638
- \`\`\`
1639
-
1640
- Creates \`shared/models/<name>.ts\` with a Zod schema template including \`id\`, \`createdAt\`, \`updatedAt\`.
1641
- Edit the generated file to add your domain-specific fields, then run scaffold.
1642
-
1643
- ### Skill: Generate full CRUD from a model
1644
-
1645
- \`\`\`bash
1646
- npx swallowkit scaffold shared/models/<name>.ts
1647
- \`\`\`
1648
-
1649
- Generates:
1650
- - Azure Functions handlers (${backendLanguage === 'typescript' ? '\`functions/src/<name>.ts\`' : '\`functions/\` language-specific CRUD files + \`functions/generated/\` schema assets'})
1651
- - BFF API routes (\`app/api/<name>/route.ts\`, \`app/api/<name>/[id]/route.ts\`)
1652
- - UI pages (\`app/<name>/page.tsx\`, detail, create, edit pages)
1653
- - Cosmos DB Bicep container config (\`infra/containers/<name>-container.bicep\`)
1654
-
1655
- ### Skill: Start development servers
1656
-
1657
- \`\`\`bash
1658
- npx swallowkit dev
1659
- \`\`\`
1660
-
1661
- Runs Next.js (http://localhost:3000) and Azure Functions (http://localhost:7071) concurrently.
1662
- Checks for Cosmos DB Emulator availability.
1663
-
1664
- ### Skill: Provision Azure resources
1665
-
1666
- \`\`\`bash
1667
- npx swallowkit provision --resource-group <name> --location <region>
1668
- \`\`\`
1669
-
1670
- Deploys Bicep infrastructure: Static Web Apps, Functions, Cosmos DB, Storage, Managed Identity.
1671
-
1672
- ### Typical workflow for "add a new feature/model"
1673
-
1674
- 1. \`npx swallowkit create-model <name>\`
1675
- 2. Edit \`shared/models/<name>.ts\` — add fields
1676
- 3. \`npx swallowkit scaffold shared/models/<name>.ts\`
1677
- 4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
1678
-
1679
- ## Do NOT
1680
-
1681
- - **Do not** put business logic or database calls in \`app/api/\` routes. They are BFF only.
1682
- - **Do not** define TypeScript interfaces/types separately from Zod schemas. Always derive types with \`z.infer<>\`.
1683
- - **Do not** manually duplicate model definitions across layers. Use the shared package.
1684
- - **Do not** manually create CRUD boilerplate. Use \`swallowkit scaffold\`.
1685
- - **Do not** hardcode Cosmos DB connection strings. Use Managed Identity (\`CosmosDBConnection__accountEndpoint\`) in production and emulator settings locally.
1686
- - **Do not** change the partition key strategy. All containers use \`/id\` as the partition key.
1687
-
1688
- ## Technology Stack
1689
-
1690
- - **Frontend**: Next.js (App Router), React, TypeScript, Tailwind CSS
1691
- - **BFF**: Next.js API Routes (proxy only)
1692
- - **Backend**: Azure Functions (${backendLanguageLabel})
1693
- - **Database**: Azure Cosmos DB (NoSQL)
1694
- - **Schema**: Zod (shared across all layers via workspace package)
1695
- - **Infrastructure**: Bicep (IaC)
1696
- - **Hosting**: Azure Static Web Apps (frontend) + Azure Functions Flex Consumption (backend)
1697
- - **Auth**: Azure Managed Identity (no connection strings in production)
1698
- - **Monitoring**: Application Insights
1699
- `;
1700
- fs.writeFileSync(path.join(projectDir, 'AGENTS.md'), agentsMd);
1701
- console.log(' ✅ AGENTS.md (Codex / generic agents)');
1702
- // ── 2. CLAUDE.md (Claude Code) ─────────────────────────────────────
1703
- const claudeMd = `# CLAUDE.md
1704
-
1705
- This file is for Claude Code. Read AGENTS.md in the project root for the full architecture, conventions, and rules.
1706
-
1707
- ## Quick Reference
1708
-
1709
- - **Architecture**: Next.js (frontend) → BFF (API routes, proxy only) → Azure Functions (backend) → Cosmos DB
1710
- - **Schema**: Zod schemas in \`shared/models/\` are the single source of truth. Never define types separately.
1711
- - **BFF rule**: \`app/api/\` routes must ONLY proxy to Azure Functions via \`callFunction()\`. No business logic.
1712
- - **Backend language**: ${backendLanguageLabel}
1713
- - **Backend rule**: Regenerate backend contracts with \`swallowkit scaffold\` after schema changes and keep \`functions/generated/\` in sync.
1714
-
1715
- ## SwallowKit CLI Commands
1716
-
1717
- | Task | Command |
1718
- |------|---------|
1719
- | Create model | \`npx swallowkit create-model <name>\` |
1720
- | Generate CRUD | \`npx swallowkit scaffold shared/models/<name>.ts\` |
1721
- | Dev servers | \`npx swallowkit dev\` |
1722
- | Provision Azure | \`npx swallowkit provision --resource-group <rg> --location <region>\` |
1723
-
1724
- ## Workflow: Add a new model
1725
-
1726
- 1. \`npx swallowkit create-model <name>\`
1727
- 2. Edit \`shared/models/<name>.ts\` — add your fields
1728
- 3. \`npx swallowkit scaffold shared/models/<name>.ts\`
1729
- 4. \`npx swallowkit dev\` — verify at http://localhost:3000/<name>
1730
- `;
1731
- fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), claudeMd);
1732
- console.log(' ✅ CLAUDE.md (Claude Code)');
1733
- // ── 3. .github/copilot-instructions.md (GitHub Copilot) ────────────
1734
- const ghDir = path.join(projectDir, '.github');
1735
- fs.mkdirSync(ghDir, { recursive: true });
1736
- const copilotInstructions = `# Copilot Instructions
1737
-
1738
- This project was generated by **SwallowKit**. See \`AGENTS.md\` in the project root for the full specification.
1739
-
1740
- ## Architecture (3-layer)
1741
-
1742
- \`\`\`
1743
- Frontend (Next.js App Router) → BFF (Next.js API Routes) → Backend (Azure Functions) → Cosmos DB
1744
- \`\`\`
1745
-
1746
- ## Key Rules
1747
-
1748
- 1. **BFF is proxy only** — \`app/api/\` routes call Azure Functions via \`callFunction()\`. No business logic, no direct DB access.
1749
- 2. **Zod = single source of truth** — Models live in \`shared/models/\`. Types are derived with \`z.infer<>\`. Never define types separately.
1750
- 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/\`.
1751
- 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.
1752
-
1753
- ## Naming
1754
-
1755
- - Schema/type: PascalCase, same name for both (\`export const Todo = z.object({...}); export type Todo = z.infer<typeof Todo>;\`)
1756
- - Files: kebab-case (\`shared/models/todo.ts\`, backend handlers under \`functions/\`)
1757
- - Cosmos DB containers: PascalCase + 's' (\`Todos\`), partition key always \`/id\`
1758
-
1759
- ## Managed Fields
1760
-
1761
- \`id\`, \`createdAt\`, \`updatedAt\` are auto-managed by the backend. Define them as \`optional()\` in schemas. Never trust client-sent values.
1762
- `;
1763
- fs.writeFileSync(path.join(ghDir, 'copilot-instructions.md'), copilotInstructions);
1764
- console.log(' ✅ .github/copilot-instructions.md (GitHub Copilot)');
1765
- // ── 4. .github/instructions/*.instructions.md (Copilot layer-specific) ──
1766
- const instructionsDir = path.join(ghDir, 'instructions');
1767
- fs.mkdirSync(instructionsDir, { recursive: true });
1768
- // 4a. shared/models — Zod schema layer
1769
- const sharedModelsInstructions = `---
1770
- applyTo: "shared/models/**"
1771
- ---
1772
-
1773
- # Shared Models — Zod Schema Rules
1774
-
1775
- Files in this directory are the **single source of truth** for data models across the entire application.
1776
-
1777
- ## Rules
1778
-
1779
- - Define Zod schemas using \`zod/v4\` (\`import { z } from 'zod/v4'\`).
1780
- - Use the **Zod official pattern**: the schema constant and the TypeScript type share the same name.
1781
- \`\`\`typescript
1782
- export const Todo = z.object({ ... });
1783
- export type Todo = z.infer<typeof Todo>;
1784
- \`\`\`
1785
- - Always include \`id: z.string()\`, \`createdAt: z.string().optional()\`, \`updatedAt: z.string().optional()\`. These are managed by the backend.
1786
- - Export a \`displayName\` string constant for UI display.
1787
- - Re-export every model from \`shared/index.ts\`.
1788
- - For relationships, use **nested schemas** (import and embed the related schema), not ID references.
1789
- - After editing a model, run \`npx swallowkit scaffold shared/models/<name>.ts\` to regenerate CRUD code.
1790
- `;
1791
- fs.writeFileSync(path.join(instructionsDir, 'shared-models.instructions.md'), sharedModelsInstructions);
1792
- // 4b. app/api — BFF layer
1793
- const bffInstructions = `---
1794
- applyTo: "app/api/**"
1795
- ---
1796
-
1797
- # BFF API Routes — Rules
1798
-
1799
- Files in \`app/api/\` are the **BFF (Backend for Frontend)** layer. They exist solely to proxy requests to Azure Functions.
1800
-
1801
- ## Rules
1802
-
1803
- - **Never** put business logic, database access, or direct Cosmos DB calls here.
1804
- - Use \`callFunction()\` from \`@/lib/api/call-function\` to forward requests to Azure Functions.
1805
- - You may validate input/output with Zod schemas before forwarding.
1806
- - Import schemas from \`@${projectName}/shared\`.
1807
-
1808
- ## Pattern
1809
-
1810
- \`\`\`typescript
1811
- import { callFunction } from '@/lib/api/call-function';
1812
- import { ModelSchema } from '@${projectName}/shared';
1813
- import { z } from 'zod/v4';
1814
-
1815
- export async function GET() {
1816
- return callFunction({
1817
- method: 'GET',
1818
- path: '/api/{model}',
1819
- responseSchema: z.array(ModelSchema),
1820
- });
1821
- }
1822
- \`\`\`
1823
- `;
1824
- fs.writeFileSync(path.join(instructionsDir, 'bff-routes.instructions.md'), bffInstructions);
1825
- // 4c. functions — Azure Functions backend layer
1826
- const functionsInstructions = `---
1827
- applyTo: "functions/**"
1828
- ---
1829
-
1830
- # Azure Functions — Backend Rules
1831
-
1832
- Files in \`functions/\` contain all business logic and data access for this application.
1833
-
1834
- ## Rules
1835
-
1836
- - Keep backend contracts aligned with \`shared/models/\` by rerunning \`swallowkit scaffold\` after schema changes.
1837
- - For TypeScript backends, use Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.
1838
- - For C#/Python backends, consume the generated OpenAPI-derived assets in \`functions/generated/\`.
1839
- - Auto-generate \`id\` (UUID), \`createdAt\`, and \`updatedAt\` on the backend. Never trust client-sent values.
1840
- - Container names are PascalCase + 's' (e.g., \`Todos\`). Partition key is always \`/id\`.
1841
-
1842
- ## Handler Pattern
1843
-
1844
- \`\`\`typescript
1845
- import { app } from '@azure/functions';
1846
- import { ModelSchema } from '@${projectName}/shared';
1847
-
1848
- app.http('{model}-get-all', {
1849
- methods: ['GET'],
1850
- route: '{model}',
1851
- authLevel: 'anonymous',
1852
- extraInputs: [cosmosInput],
1853
- handler: async (request, context) => {
1854
- const documents = context.extraInputs.get(cosmosInput);
1855
- const validated = z.array(ModelSchema).parse(documents);
1856
- return { status: 200, jsonBody: validated };
1857
- },
1858
- });
1859
- \`\`\`
1860
- `;
1861
- fs.writeFileSync(path.join(instructionsDir, 'azure-functions.instructions.md'), functionsInstructions);
1862
- console.log(' ✅ .github/instructions/ (Copilot layer-specific instructions)');
1863
- console.log(' - shared-models.instructions.md');
1864
- console.log(' - bff-routes.instructions.md');
1865
- console.log(' - azure-functions.instructions.md');
1866
- console.log('\n✅ AI agent files created\n');
1867
- console.log(' Supported agents:');
1868
- console.log(' - OpenAI Codex → AGENTS.md');
1869
- console.log(' - Claude Code → CLAUDE.md (+ AGENTS.md)');
1870
- console.log(' - GitHub Copilot → .github/copilot-instructions.md');
1871
- console.log(' - GitHub Copilot (edit) → .github/instructions/*.instructions.md');
1872
- console.log('');
1873
- }
1874
- async function createInfrastructure(projectDir, projectName, azureConfig, backendLanguage) {
1875
- console.log('📦 Creating infrastructure files (Bicep)...\n');
1876
- const infraDir = path.join(projectDir, 'infra');
1877
- const modulesDir = path.join(infraDir, 'modules');
1878
- fs.mkdirSync(modulesDir, { recursive: true });
1879
- const enableVNet = azureConfig.vnetOption !== 'none';
1880
- const functionsRuntime = getFunctionsRuntimeConfig(backendLanguage);
1881
- // main.bicep
1882
- const mainBicep = `targetScope = 'resourceGroup'
1883
-
1884
- @description('Project name')
1885
- param projectName string
1886
-
1887
- @description('Location for Functions and Cosmos DB')
1888
- param location string = resourceGroup().location
1889
-
1890
- @description('Location for Static Web App (must be explicitly provided)')
1891
- param swaLocation string
1846
+ @description('Static Web Apps hosting plan')
1847
+ @allowed([
1848
+ 'Free'
1849
+ 'Standard'
1850
+ ])
1851
+ param staticWebAppSku string = '${staticWebAppSku}'
1892
1852
 
1893
1853
  @description('Cosmos DB mode')
1894
1854
  @allowed(['freetier', 'serverless'])
@@ -1932,7 +1892,7 @@ module staticWebApp 'modules/staticwebapp.bicep' = {
1932
1892
  params: {
1933
1893
  name: 'swa-\${projectName}'
1934
1894
  location: swaLocation
1935
- sku: 'Standard'
1895
+ sku: staticWebAppSku
1936
1896
  appInsightsConnectionString: appInsightsSwa.outputs.connectionString
1937
1897
  }
1938
1898
  }
@@ -1973,16 +1933,11 @@ module cosmosPrivateEndpoint 'modules/private-endpoint-cosmos.bicep' = if (enabl
1973
1933
  params: {
1974
1934
  name: 'pe-cosmos-\${projectName}'
1975
1935
  location: location
1976
- cosmosAccountId: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.id : cosmosDbServerless.outputs.id
1977
- cosmosAccountName: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
1978
- subnetId: vnet.outputs.privateEndpointSubnetId
1979
- 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
1980
1940
  }
1981
- dependsOn: [
1982
- cosmosDbFreeTier
1983
- cosmosDbServerless
1984
- vnet
1985
- ]
1986
1941
  }
1987
1942
 
1988
1943
  // Azure Functions (Flex Consumption) - Deploy AFTER Cosmos DB
@@ -1994,16 +1949,14 @@ module functionsFlex 'modules/functions-flex.bicep' = {
1994
1949
  storageAccountName: 'stg\${uniqueString(resourceGroup().id, projectName)}'
1995
1950
  appInsightsConnectionString: appInsightsFunctions.outputs.connectionString
1996
1951
  swaDefaultHostname: staticWebApp.outputs.defaultHostname
1997
- cosmosDbEndpoint: cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
1998
- 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
1999
1954
  functionsRuntimeName: '${functionsRuntime.name}'
2000
1955
  functionsRuntimeVersion: '${functionsRuntime.version}'
2001
1956
  enableVNet: enableVNet
2002
- vnetSubnetId: enableVNet ? vnet.outputs.functionsSubnetId : ''
1957
+ vnetSubnetId: enableVNet ? vnet!.outputs.functionsSubnetId : ''
2003
1958
  }
2004
1959
  dependsOn: [
2005
- cosmosDbFreeTier
2006
- cosmosDbServerless
2007
1960
  cosmosPrivateEndpoint
2008
1961
  ]
2009
1962
  }
@@ -2012,23 +1965,17 @@ module functionsFlex 'modules/functions-flex.bicep' = {
2012
1965
  module cosmosDbRoleAssignmentFreeTier 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'freetier') {
2013
1966
  name: 'cosmosDbRoleAssignment'
2014
1967
  params: {
2015
- cosmosAccountName: cosmosDbFreeTier.outputs.accountName
1968
+ cosmosAccountName: cosmosDbFreeTier!.outputs.accountName
2016
1969
  functionsPrincipalId: functionsFlex.outputs.principalId
2017
1970
  }
2018
- dependsOn: [
2019
- functionsFlex
2020
- ]
2021
1971
  }
2022
1972
 
2023
1973
  module cosmosDbRoleAssignmentServerless 'modules/cosmosdb-role-assignment.bicep' = if (cosmosDbMode == 'serverless') {
2024
1974
  name: 'cosmosDbRoleAssignment'
2025
1975
  params: {
2026
- cosmosAccountName: cosmosDbServerless.outputs.accountName
1976
+ cosmosAccountName: cosmosDbServerless!.outputs.accountName
2027
1977
  functionsPrincipalId: functionsFlex.outputs.principalId
2028
1978
  }
2029
- dependsOn: [
2030
- functionsFlex
2031
- ]
2032
1979
  }
2033
1980
 
2034
1981
  // Update SWA config with Functions hostname (after Functions deployment)
@@ -2037,20 +1984,18 @@ module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
2037
1984
  params: {
2038
1985
  staticWebAppName: staticWebApp.outputs.name
2039
1986
  functionsDefaultHostname: functionsFlex.outputs.defaultHostname
1987
+ functionsAppName: functionsFlex.outputs.name
2040
1988
  appInsightsConnectionString: appInsightsSwa.outputs.connectionString
2041
1989
  }
2042
- dependsOn: [
2043
- functionsFlex
2044
- ]
2045
1990
  }
2046
1991
 
2047
1992
  output staticWebAppName string = staticWebApp.outputs.name
2048
1993
  output staticWebAppUrl string = staticWebApp.outputs.defaultHostname
2049
1994
  output functionsAppName string = functionsFlex.outputs.name
2050
1995
  output functionsAppUrl string = functionsFlex.outputs.defaultHostname
2051
- output cosmosDbAccountName string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.accountName : cosmosDbServerless.outputs.accountName
2052
- output cosmosDbEndpoint string = cosmosDbMode == 'freetier' ? cosmosDbFreeTier.outputs.endpoint : cosmosDbServerless.outputs.endpoint
2053
- 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
2054
1999
  output logAnalyticsWorkspaceName string = logAnalytics.outputs.name
2055
2000
  output logAnalyticsWorkspaceId string = logAnalytics.outputs.id
2056
2001
  output appInsightsSwaName string = appInsightsSwa.outputs.name
@@ -2058,7 +2003,7 @@ output appInsightsSwaConnectionString string = appInsightsSwa.outputs.connection
2058
2003
  output appInsightsFunctionsName string = appInsightsFunctions.outputs.name
2059
2004
  output appInsightsFunctionsConnectionString string = appInsightsFunctions.outputs.connectionString
2060
2005
  output vnetEnabled bool = enableVNet
2061
- output vnetName string = enableVNet ? vnet.outputs.name : ''
2006
+ output vnetName string = enableVNet ? vnet!.outputs.name : ''
2062
2007
  `;
2063
2008
  fs.writeFileSync(path.join(infraDir, 'main.bicep'), mainBicep);
2064
2009
  // main.parameters.json
@@ -2072,6 +2017,9 @@ output vnetName string = enableVNet ? vnet.outputs.name : ''
2072
2017
  "cosmosDbMode": {
2073
2018
  "value": "${azureConfig.cosmosDbMode}"
2074
2019
  },
2020
+ "staticWebAppSku": {
2021
+ "value": "${staticWebAppSku}"
2022
+ },
2075
2023
  "enableVNet": {
2076
2024
  "value": ${enableVNet}
2077
2025
  }
@@ -2126,31 +2074,7 @@ output defaultHostname string = staticWebApp.properties.defaultHostname
2126
2074
  `;
2127
2075
  fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
2128
2076
  // modules/staticwebapp-config.bicep (for updating config after Functions deployment)
2129
- const staticWebAppConfigBicep = `@description('Static Web App name')
2130
- param staticWebAppName string
2131
-
2132
- @description('Functions App default hostname for backend API calls')
2133
- param functionsDefaultHostname string
2134
-
2135
- @description('Application Insights connection string for SWA')
2136
- param appInsightsConnectionString string
2137
-
2138
- resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
2139
- name: staticWebAppName
2140
- }
2141
-
2142
- resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
2143
- parent: staticWebApp
2144
- name: 'appsettings'
2145
- properties: {
2146
- APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
2147
- ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
2148
- BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
2149
- }
2150
- }
2151
-
2152
- output configName string = staticWebAppConfig.name
2153
- `;
2077
+ const staticWebAppConfigBicep = buildStaticWebAppConfigBicepSource();
2154
2078
  fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
2155
2079
  // modules/loganalytics.bicep (Shared Log Analytics Workspace)
2156
2080
  const logAnalyticsBicep = `@description('Log Analytics workspace name')
@@ -2369,63 +2293,7 @@ output principalId string = functionApp.identity.principalId
2369
2293
  `;
2370
2294
  fs.writeFileSync(path.join(modulesDir, 'functions-flex.bicep'), functionsFlexBicep);
2371
2295
  // modules/cosmosdb-freetier.bicep (Free Tier)
2372
- const cosmosDbFreeTierBicep = `@description('Cosmos DB account name')
2373
- param accountName string
2374
-
2375
- @description('Database name')
2376
- param databaseName string
2377
-
2378
- @description('Location for Cosmos DB')
2379
- param location string
2380
-
2381
- @description('Public network access')
2382
- @allowed(['Enabled', 'Disabled'])
2383
- param publicNetworkAccess string = 'Enabled'
2384
-
2385
- // Cosmos DB Account (Free Tier)
2386
- resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = {
2387
- name: accountName
2388
- location: location
2389
- kind: 'GlobalDocumentDB'
2390
- properties: {
2391
- databaseAccountOfferType: 'Standard'
2392
- enableAutomaticFailover: false
2393
- enableFreeTier: true
2394
- publicNetworkAccess: publicNetworkAccess
2395
- disableLocalAuth: true
2396
- consistencyPolicy: {
2397
- defaultConsistencyLevel: 'Session'
2398
- }
2399
- locations: [
2400
- {
2401
- locationName: location
2402
- failoverPriority: 0
2403
- isZoneRedundant: false
2404
- }
2405
- ]
2406
- disableKeyBasedMetadataWriteAccess: true
2407
- }
2408
- }
2409
-
2410
- // Cosmos DB Database
2411
- resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-11-15' = {
2412
- parent: cosmosAccount
2413
- name: databaseName
2414
- properties: {
2415
- resource: {
2416
- id: databaseName
2417
- }
2418
- options: {
2419
- throughput: 1000
2420
- }
2421
- }
2422
- }
2423
-
2424
- output id string = cosmosAccount.id
2425
- output accountName string = cosmosAccount.name
2426
- output endpoint string = cosmosAccount.properties.documentEndpoint
2427
- output databaseName string = database.name
2428
- `;
2296
+ const cosmosDbFreeTierBicep = buildCosmosDbFreeTierBicepSource();
2429
2297
  fs.writeFileSync(path.join(modulesDir, 'cosmosdb-freetier.bicep'), cosmosDbFreeTierBicep);
2430
2298
  // modules/cosmosdb-serverless.bicep (Serverless)
2431
2299
  const cosmosDbServerlessBicep = `@description('Cosmos DB account name')
@@ -2515,10 +2383,11 @@ resource roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignment
2515
2383
  output roleAssignmentId string = roleAssignment.id
2516
2384
  `;
2517
2385
  fs.writeFileSync(path.join(modulesDir, 'cosmosdb-role-assignment.bicep'), cosmosDbRoleAssignmentBicep);
2518
- // VNet modules (only generate if VNet is enabled)
2519
- if (enableVNet) {
2520
- // modules/vnet.bicep
2521
- 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')
2522
2391
  param name string
2523
2392
 
2524
2393
  @description('Location for VNet')
@@ -2564,9 +2433,9 @@ output name string = vnet.name
2564
2433
  output functionsSubnetId string = vnet.properties.subnets[0].id
2565
2434
  output privateEndpointSubnetId string = vnet.properties.subnets[1].id
2566
2435
  `;
2567
- fs.writeFileSync(path.join(modulesDir, 'vnet.bicep'), vnetBicep);
2568
- // modules/private-endpoint-cosmos.bicep
2569
- 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')
2570
2439
  param name string
2571
2440
 
2572
2441
  @description('Location')
@@ -2644,24 +2513,28 @@ resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneG
2644
2513
  output privateEndpointId string = privateEndpoint.id
2645
2514
  output privateDnsZoneId string = privateDnsZone.id
2646
2515
  `;
2647
- fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
2648
- console.log('✅ VNet modules created\n');
2649
- }
2516
+ fs.writeFileSync(path.join(modulesDir, 'private-endpoint-cosmos.bicep'), cosmosPrivateEndpointBicep);
2517
+ console.log('✅ VNet module definitions created\n');
2650
2518
  console.log('✅ Infrastructure files created\n');
2651
2519
  }
2652
- function getGitHubFunctionsWorkflow(pm, backendLanguage) {
2520
+ function getGitHubFunctionsWorkflow(pm, backendLanguage, projectPm = pm) {
2653
2521
  const pmCmd = (0, package_manager_1.getCommands)(pm);
2654
2522
  const pnpmSetupStep = (0, package_manager_1.getCiSetupStep)(pm);
2523
+ const npmWorkspaceNormalizationStep = getGitHubNpmWorkspaceNormalizationStep(pm, projectPm);
2655
2524
  const commonSetup = ` - uses: actions/checkout@v4
2656
2525
 
2526
+ ${pnpmSetupStep ? `${pnpmSetupStep}\n` : ''}
2657
2527
  - name: Setup Node.js
2658
2528
  uses: actions/setup-node@v4
2659
2529
  with:
2660
2530
  node-version: '22'
2661
- ${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}` : ''}
2662
2535
  - name: Install dependencies
2663
2536
  run: |
2664
- ${pmCmd.ci}
2537
+ ${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
2665
2538
 
2666
2539
  - name: Build shared package
2667
2540
  run: |
@@ -2745,7 +2618,7 @@ jobs:
2745
2618
  ${commonSetup} - name: Setup .NET
2746
2619
  uses: actions/setup-dotnet@v4
2747
2620
  with:
2748
- dotnet-version: '8.0.x'
2621
+ dotnet-version: '10.0.x'
2749
2622
 
2750
2623
  - name: Publish Functions
2751
2624
  run: |
@@ -2804,16 +2677,18 @@ ${commonSetup} - name: Setup Python
2804
2677
  sku: flexconsumption
2805
2678
  `;
2806
2679
  }
2807
- function getAzureFunctionsPipeline(pm, backendLanguage) {
2680
+ function getAzureFunctionsPipeline(pm, backendLanguage, projectPm = pm) {
2808
2681
  const pmCmd = (0, package_manager_1.getCommands)(pm);
2809
2682
  const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
2683
+ const npmWorkspaceNormalizationStep = getAzurePipelinesNpmWorkspaceNormalizationStep(pm, projectPm);
2810
2684
  const commonSetup = ` - task: NodeTool@0
2811
2685
  inputs:
2812
2686
  versionSpec: '22.x'
2813
2687
  displayName: 'Install Node.js'
2814
2688
  ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
2689
+ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}\n` : ''}
2815
2690
  - script: |
2816
- ${pmCmd.ci}
2691
+ ${(0, package_manager_1.getCiInstallCommand)(pm, projectPm)}
2817
2692
  displayName: 'Install workspace dependencies'
2818
2693
 
2819
2694
  - script: |
@@ -2917,7 +2792,7 @@ variables:
2917
2792
  steps:
2918
2793
  ${commonSetup} - task: UseDotNet@2
2919
2794
  inputs:
2920
- version: '8.0.x'
2795
+ version: '10.0.x'
2921
2796
  displayName: 'Install .NET SDK'
2922
2797
 
2923
2798
  - script: |
@@ -2997,13 +2872,205 @@ ${commonSetup} - task: UsePythonVersion@0
2997
2872
  displayName: 'Deploy to Azure Functions'
2998
2873
  `;
2999
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
+ }
3000
3057
  async function createGitHubActionsWorkflows(projectDir, azureConfig, pm, backendLanguage) {
3001
3058
  console.log('📦 Creating GitHub Actions workflows...\n');
3002
- const pmCmd = (0, package_manager_1.getCommands)(pm);
3003
3059
  const workflowsDir = path.join(projectDir, '.github', 'workflows');
3004
3060
  fs.mkdirSync(workflowsDir, { recursive: true });
3005
- // deploy-swa.yml
3006
- 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
3007
3074
 
3008
3075
  on:
3009
3076
  push:
@@ -3016,6 +3083,7 @@ on:
3016
3083
  - 'shared/**'
3017
3084
  - 'public/**'
3018
3085
  - 'package.json'
3086
+ - '${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
3019
3087
  - 'next.config.js'
3020
3088
  - 'next.config.ts'
3021
3089
  workflow_dispatch:
@@ -3029,6 +3097,7 @@ on:
3029
3097
  - 'shared/**'
3030
3098
  - 'public/**'
3031
3099
  - 'package.json'
3100
+ - '${projectPm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
3032
3101
  - 'next.config.js'
3033
3102
  - 'next.config.ts'
3034
3103
 
@@ -3041,6 +3110,22 @@ jobs:
3041
3110
  - uses: actions/checkout@v4
3042
3111
  with:
3043
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)}
3044
3129
 
3045
3130
  - name: Deploy to Azure Static Web Apps
3046
3131
  if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
@@ -3052,84 +3137,21 @@ jobs:
3052
3137
  app_location: '/'
3053
3138
  api_location: ''
3054
3139
  output_location: ''
3140
+ app_build_command: 'npm run build'
3055
3141
  env:
3056
3142
  NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS: '1'
3057
3143
  `;
3058
- fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
3059
- // deploy-functions.yml
3060
- const functionsWorkflow = getGitHubFunctionsWorkflow(pm, backendLanguage);
3061
- fs.writeFileSync(path.join(workflowsDir, 'deploy-functions.yml'), functionsWorkflow);
3062
- console.log('✅ GitHub Actions workflows created\n');
3063
3144
  }
3064
3145
  async function createAzurePipelines(projectDir, pm, backendLanguage) {
3065
3146
  console.log('📦 Creating Azure Pipelines...\n');
3066
- const pmCmd = (0, package_manager_1.getCommands)(pm);
3067
- const azPipelinesSetup = (0, package_manager_1.getAzurePipelinesSetup)(pm);
3147
+ const pipelinesPm = pm;
3068
3148
  const pipelinesDir = path.join(projectDir, 'pipelines');
3069
3149
  fs.mkdirSync(pipelinesDir, { recursive: true });
3070
3150
  // swa.yml
3071
- const swaPipeline = `trigger:
3072
- branches:
3073
- include:
3074
- - main
3075
- paths:
3076
- include:
3077
- - app/**
3078
- - components/**
3079
- - lib/**
3080
- - shared/**
3081
- - public/**
3082
- - package.json
3083
- - next.config.js
3084
-
3085
- pr:
3086
- branches:
3087
- include:
3088
- - main
3089
- paths:
3090
- include:
3091
- - app/**
3092
- - components/**
3093
- - lib/**
3094
- - shared/**
3095
- - public/**
3096
- - package.json
3097
- - next.config.js
3098
-
3099
- pool:
3100
- vmImage: 'ubuntu-latest'
3101
-
3102
- variables:
3103
- - group: azure-deployment
3104
-
3105
- steps:
3106
- - task: NodeTool@0
3107
- inputs:
3108
- versionSpec: '22.x'
3109
- displayName: 'Install Node.js'
3110
- ${azPipelinesSetup ? `\n${azPipelinesSetup}\n` : ''}
3111
- - script: |
3112
- ${pmCmd.ci}
3113
- displayName: 'Install dependencies'
3114
-
3115
- - script: |
3116
- ${pmCmd.run} build
3117
- env:
3118
- NODE_ENV: production
3119
- displayName: 'Build Next.js app'
3120
-
3121
- - task: AzureStaticWebApp@0
3122
- condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
3123
- inputs:
3124
- app_location: '.'
3125
- output_location: '.next/standalone'
3126
- skip_app_build: true
3127
- azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN)
3128
- displayName: 'Deploy to Azure Static Web Apps'
3129
- `;
3151
+ const swaPipeline = buildAzureSwaPipeline(pipelinesPm, pm);
3130
3152
  fs.writeFileSync(path.join(pipelinesDir, 'swa.yml'), swaPipeline);
3131
3153
  // functions.yml
3132
- const functionsPipeline = getAzureFunctionsPipeline(pm, backendLanguage);
3154
+ const functionsPipeline = getAzureFunctionsPipeline(pipelinesPm, backendLanguage, pm);
3133
3155
  fs.writeFileSync(path.join(pipelinesDir, 'functions.yml'), functionsPipeline);
3134
3156
  console.log('✅ Azure Pipelines created\n');
3135
3157
  }