swallowkit 0.4.0-beta.3 → 1.0.0-beta.10

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