swallowkit 1.0.0-beta.22 → 1.0.0-beta.24

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 (38) hide show
  1. package/README.ja.md +9 -4
  2. package/README.md +9 -4
  3. package/dist/cli/commands/dev.d.ts +14 -0
  4. package/dist/cli/commands/dev.d.ts.map +1 -1
  5. package/dist/cli/commands/dev.js +187 -54
  6. package/dist/cli/commands/dev.js.map +1 -1
  7. package/dist/cli/commands/init.d.ts.map +1 -1
  8. package/dist/cli/commands/init.js +33 -18
  9. package/dist/cli/commands/init.js.map +1 -1
  10. package/dist/cli/commands/scaffold.d.ts +0 -3
  11. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  12. package/dist/cli/commands/scaffold.js +3 -172
  13. package/dist/cli/commands/scaffold.js.map +1 -1
  14. package/dist/core/project/validation.js +2 -2
  15. package/dist/core/project/validation.js.map +1 -1
  16. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  17. package/dist/core/scaffold/model-parser.js +5 -6
  18. package/dist/core/scaffold/model-parser.js.map +1 -1
  19. package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
  20. package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -0
  21. package/dist/core/scaffold/native-schema-generator.js +677 -0
  22. package/dist/core/scaffold/native-schema-generator.js.map +1 -0
  23. package/dist/utils/python-uv.d.ts +21 -0
  24. package/dist/utils/python-uv.d.ts.map +1 -0
  25. package/dist/utils/python-uv.js +112 -0
  26. package/dist/utils/python-uv.js.map +1 -0
  27. package/package.json +1 -1
  28. package/src/__tests__/dev.test.ts +95 -0
  29. package/src/__tests__/model-parser.test.ts +44 -64
  30. package/src/__tests__/python-uv.test.ts +48 -0
  31. package/src/__tests__/scaffold.test.ts +54 -26
  32. package/src/cli/commands/dev.ts +258 -74
  33. package/src/cli/commands/init.ts +45 -19
  34. package/src/cli/commands/scaffold.ts +3 -213
  35. package/src/core/project/validation.ts +2 -2
  36. package/src/core/scaffold/model-parser.ts +7 -7
  37. package/src/core/scaffold/native-schema-generator.ts +798 -0
  38. package/src/utils/python-uv.ts +97 -0
@@ -1,5 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import { spawn, ChildProcess } from 'child_process';
3
+ import * as http from 'http';
4
+ import * as https from 'https';
3
5
  import * as path from 'path';
4
6
  import * as fs from 'fs';
5
7
  import * as os from 'os';
@@ -10,6 +12,15 @@ import { ModelInfo } from '../../core/scaffold/model-parser';
10
12
  import { applyDevSeedEnvironment, getContainerNameForModel, loadProjectModels } from './dev-seeds';
11
13
  import { BackendLanguage } from '../../types';
12
14
  import { detectFromProject, getCommands } from '../../utils/package-manager';
15
+ import {
16
+ buildProjectLocalUvEnv,
17
+ buildProjectLocalUvInstallerEnv,
18
+ buildUvPipInstallArgs,
19
+ buildUvVenvArgs,
20
+ getProjectLocalUvInstallerCommand,
21
+ getProjectLocalUvPaths,
22
+ getPythonProjectRoot,
23
+ } from '../../utils/python-uv';
13
24
  import { ConnectorMockServer } from '../../core/mock/connector-mock-server';
14
25
 
15
26
  export interface DevOptions {
@@ -27,6 +38,15 @@ type ParsedDevActionOptions = DevOptions & {
27
38
  functions?: boolean;
28
39
  };
29
40
 
41
+ interface FunctionsCoreToolsCommand {
42
+ command: string;
43
+ argsPrefix: string[];
44
+ label: string;
45
+ }
46
+
47
+ const MINIMUM_CSHARP_CORE_TOOLS_VERSION = '4.6.0';
48
+ const NPM_CORE_TOOLS_PACKAGE = 'azure-functions-core-tools@4';
49
+
30
50
  function normalizeParsedDevOptions(options: ParsedDevActionOptions): DevOptions {
31
51
  return {
32
52
  ...options,
@@ -38,11 +58,89 @@ export function buildFunctionsStartArgs(functionsPort: string): string[] {
38
58
  return ['start', '--port', functionsPort];
39
59
  }
40
60
 
61
+ export function parseCoreToolsVersion(output: string): string | null {
62
+ const match = output.match(/\d+\.\d+\.\d+/);
63
+ return match ? match[0] : null;
64
+ }
65
+
66
+ export function compareVersionNumbers(left: string, right: string): number {
67
+ const leftParts = left.split('.').map((value) => Number.parseInt(value, 10));
68
+ const rightParts = right.split('.').map((value) => Number.parseInt(value, 10));
69
+ const length = Math.max(leftParts.length, rightParts.length);
70
+
71
+ for (let index = 0; index < length; index += 1) {
72
+ const leftPart = leftParts[index] ?? 0;
73
+ const rightPart = rightParts[index] ?? 0;
74
+
75
+ if (leftPart !== rightPart) {
76
+ return leftPart - rightPart;
77
+ }
78
+ }
79
+
80
+ return 0;
81
+ }
82
+
83
+ export function buildFunctionsCoreToolsCommand(
84
+ backendLanguage: BackendLanguage,
85
+ installedVersion: string | null
86
+ ): FunctionsCoreToolsCommand {
87
+ if (
88
+ backendLanguage === 'csharp' &&
89
+ (!installedVersion || compareVersionNumbers(installedVersion, MINIMUM_CSHARP_CORE_TOOLS_VERSION) < 0)
90
+ ) {
91
+ const reason = installedVersion
92
+ ? `installed func ${installedVersion} is too old for C# isolated`
93
+ : 'func is not installed';
94
+
95
+ return {
96
+ command: 'npm',
97
+ argsPrefix: ['exec', '--yes', NPM_CORE_TOOLS_PACKAGE, '--'],
98
+ label: `npm exec ${NPM_CORE_TOOLS_PACKAGE} (${reason})`,
99
+ };
100
+ }
101
+
102
+ return {
103
+ command: 'func',
104
+ argsPrefix: [],
105
+ label: installedVersion ? `func ${installedVersion}` : 'func',
106
+ };
107
+ }
108
+
41
109
  export function buildNextDevArgs(pm: string, port: string): string[] {
42
110
  const baseArgs = ['next', 'dev', '--port', port, '--webpack'];
43
111
  return pm === 'pnpm' ? ['exec', ...baseArgs] : baseArgs;
44
112
  }
45
113
 
114
+ export function buildFunctionsBaseUrl(host: string | undefined, functionsPort: string): string {
115
+ return `http://${host || 'localhost'}:${functionsPort}`;
116
+ }
117
+
118
+ export function getFunctionsReadinessTimeoutMs(backendLanguage: BackendLanguage): number {
119
+ return backendLanguage === 'csharp' ? 90_000 : 30_000;
120
+ }
121
+
122
+ export async function waitForHttpServerReady(
123
+ url: string,
124
+ timeoutMs = 30_000,
125
+ intervalMs = 500
126
+ ): Promise<boolean> {
127
+ const deadline = Date.now() + timeoutMs;
128
+
129
+ while (Date.now() <= deadline) {
130
+ if (await probeHttpServer(url)) {
131
+ return true;
132
+ }
133
+
134
+ if (Date.now() + intervalMs > deadline) {
135
+ break;
136
+ }
137
+
138
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
139
+ }
140
+
141
+ return probeHttpServer(url);
142
+ }
143
+
46
144
  export function getPythonVirtualEnvPaths(functionsDir: string): {
47
145
  venvDir: string;
48
146
  binDir: string;
@@ -59,6 +157,13 @@ export function getPythonVirtualEnvPaths(functionsDir: string): {
59
157
  return { venvDir, binDir, pythonExecutable };
60
158
  }
61
159
 
160
+ export function getCSharpFunctionsBuildArtifactPaths(functionsDir: string): string[] {
161
+ return [
162
+ path.join(functionsDir, 'bin'),
163
+ path.join(functionsDir, 'obj'),
164
+ ];
165
+ }
166
+
62
167
  export function buildPythonFunctionsEnv(baseEnv: NodeJS.ProcessEnv, functionsDir: string): NodeJS.ProcessEnv {
63
168
  const { venvDir, binDir, pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
64
169
  const pathKey = getPathEnvKey(baseEnv);
@@ -93,10 +198,20 @@ async function checkCoreTools(): Promise<boolean> {
93
198
  return checkCommand('func', ['--version']);
94
199
  }
95
200
 
96
- async function checkCommand(command: string, args: string[] = ['--version']): Promise<boolean> {
201
+ async function checkCommand(
202
+ command: string,
203
+ args: string[] = ['--version'],
204
+ options?: {
205
+ cwd?: string;
206
+ env?: NodeJS.ProcessEnv;
207
+ shell?: boolean;
208
+ }
209
+ ): Promise<boolean> {
97
210
  return new Promise((resolve) => {
98
211
  const checkProcess = spawn(command, args, {
99
- shell: true,
212
+ cwd: options?.cwd,
213
+ env: options?.env ?? process.env,
214
+ shell: options?.shell ?? true,
100
215
  stdio: 'pipe',
101
216
  });
102
217
 
@@ -110,26 +225,34 @@ async function checkCommand(command: string, args: string[] = ['--version']): Pr
110
225
  });
111
226
  }
112
227
 
113
- async function resolvePythonBootstrapCommand(): Promise<{ command: string; argsPrefix: string[]; label: string }> {
114
- const candidates = process.platform === 'win32'
115
- ? [
116
- { command: 'py', argsPrefix: ['-3.11'], label: 'py -3.11' },
117
- { command: 'python', argsPrefix: [], label: 'python' },
118
- ]
119
- : [
120
- { command: 'python3', argsPrefix: [], label: 'python3' },
121
- { command: 'python', argsPrefix: [], label: 'python' },
122
- ];
123
-
124
- for (const candidate of candidates) {
125
- if (await checkCommand(candidate.command, [...candidate.argsPrefix, '--version'])) {
126
- return candidate;
127
- }
228
+ async function resolveProjectLocalUvCommand(projectRoot: string): Promise<{ command: string; env: NodeJS.ProcessEnv }> {
229
+ const uvEnv = buildProjectLocalUvEnv(process.env, projectRoot);
230
+ const { localUvExecutable } = getProjectLocalUvPaths(projectRoot);
231
+
232
+ if (await checkCommand('uv', ['--version'], { shell: false })) {
233
+ return { command: 'uv', env: uvEnv };
234
+ }
235
+
236
+ if (fs.existsSync(localUvExecutable) && await checkCommand(localUvExecutable, ['--version'], { shell: false })) {
237
+ return { command: localUvExecutable, env: uvEnv };
128
238
  }
129
239
 
130
- throw new Error(
131
- 'Python 3.11 was not found. Install Python 3.11 and make sure `python`, `python3`, or `py -3.11` is available.'
240
+ console.log('šŸ“¦ Installing project-local uv...');
241
+ const installer = getProjectLocalUvInstallerCommand();
242
+ await runCommand(
243
+ installer.command,
244
+ installer.args,
245
+ projectRoot,
246
+ 'uv installation',
247
+ buildProjectLocalUvInstallerEnv(process.env, projectRoot),
248
+ false
132
249
  );
250
+
251
+ if (!(fs.existsSync(localUvExecutable) && await checkCommand(localUvExecutable, ['--version'], { shell: false }))) {
252
+ throw new Error('Failed to install project-local uv.');
253
+ }
254
+
255
+ return { command: localUvExecutable, env: uvEnv };
133
256
  }
134
257
 
135
258
  async function getCommandPath(command: string): Promise<string | null> {
@@ -143,6 +266,18 @@ async function getCommandPath(command: string): Promise<string | null> {
143
266
  return firstLine || null;
144
267
  }
145
268
 
269
+ async function resolveInstalledCoreToolsVersion(): Promise<string | null> {
270
+ if (!(await checkCoreTools())) {
271
+ return null;
272
+ }
273
+
274
+ try {
275
+ return parseCoreToolsVersion(await captureCommandOutput('func', ['--version']));
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
280
+
146
281
  async function captureCommandOutput(
147
282
  command: string,
148
283
  args: string[],
@@ -180,6 +315,41 @@ async function captureCommandOutput(
180
315
  });
181
316
  }
182
317
 
318
+ async function probeHttpServer(url: string): Promise<boolean> {
319
+ return new Promise((resolve) => {
320
+ const target = new URL(url);
321
+ const requestFactory = target.protocol === 'https:' ? https.request : http.request;
322
+ let settled = false;
323
+ const finish = (value: boolean) => {
324
+ if (!settled) {
325
+ settled = true;
326
+ resolve(value);
327
+ }
328
+ };
329
+
330
+ const request = requestFactory(
331
+ {
332
+ hostname: target.hostname,
333
+ port: target.port,
334
+ path: target.pathname || '/',
335
+ method: 'GET',
336
+ timeout: 1000,
337
+ },
338
+ (response) => {
339
+ response.resume();
340
+ finish(true);
341
+ }
342
+ );
343
+
344
+ request.on('timeout', () => {
345
+ request.destroy();
346
+ finish(false);
347
+ });
348
+ request.on('error', () => finish(false));
349
+ request.end();
350
+ });
351
+ }
352
+
183
353
  async function resolvePythonRuntimeDetails(
184
354
  functionsDir: string,
185
355
  env: NodeJS.ProcessEnv
@@ -255,47 +425,36 @@ async function bridgePythonCoreToolsForWindowsArm64(
255
425
  }
256
426
 
257
427
  async function preparePythonFunctionsEnvironment(functionsDir: string): Promise<NodeJS.ProcessEnv> {
258
- const { pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
259
- const hasUv = await checkCommand('uv', ['--version']);
260
-
261
- if (!fs.existsSync(pythonExecutable)) {
262
- if (hasUv) {
263
- console.log('šŸ“¦ Creating Python virtual environment with uv...');
264
- await runCommand('uv', ['venv', '.venv', '--python', '3.11'], functionsDir, 'python virtual environment setup');
265
- } else {
266
- const bootstrap = await resolvePythonBootstrapCommand();
267
- console.log(`šŸ“¦ Creating Python virtual environment with ${bootstrap.label}...`);
268
- await runCommand(
269
- bootstrap.command,
270
- [...bootstrap.argsPrefix, '-m', 'venv', '.venv'],
271
- functionsDir,
272
- 'python virtual environment setup'
273
- );
274
- }
275
- }
428
+ const projectRoot = getPythonProjectRoot(functionsDir);
429
+ const { command: uvCommand, env: uvEnv } = await resolveProjectLocalUvCommand(projectRoot);
430
+ const { venvDir, pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
431
+ const hasUsableVirtualEnv = fs.existsSync(pythonExecutable) && await checkCommand(pythonExecutable, ['--version'], {
432
+ cwd: functionsDir,
433
+ env: uvEnv,
434
+ shell: false,
435
+ });
276
436
 
277
- const pythonEnv = buildPythonFunctionsEnv(process.env, functionsDir);
278
- console.log(`šŸ“¦ Installing Python Azure Functions dependencies${hasUv ? ' with uv' : ''}...`);
437
+ if (!hasUsableVirtualEnv) {
438
+ const venvArgs = buildUvVenvArgs('.venv');
439
+ if (fs.existsSync(venvDir)) {
440
+ venvArgs.push('--clear');
441
+ }
279
442
 
280
- if (hasUv) {
281
- await runCommand(
282
- 'uv',
283
- ['pip', 'install', '--python', pythonExecutable, '-r', 'requirements.txt'],
284
- functionsDir,
285
- 'python dependency installation',
286
- pythonEnv
287
- );
288
- } else {
289
- await runCommand('python', ['-m', 'pip', 'install', '--upgrade', 'pip'], functionsDir, 'python pip upgrade', pythonEnv);
290
- await runCommand(
291
- 'python',
292
- ['-m', 'pip', 'install', '-r', 'requirements.txt'],
293
- functionsDir,
294
- 'python dependency installation',
295
- pythonEnv
296
- );
443
+ console.log('šŸ“¦ Creating Python virtual environment with uv...');
444
+ await runCommand(uvCommand, venvArgs, functionsDir, 'python virtual environment setup', uvEnv, false);
297
445
  }
298
446
 
447
+ console.log('šŸ“¦ Installing Python Azure Functions dependencies with uv...');
448
+ await runCommand(
449
+ uvCommand,
450
+ buildUvPipInstallArgs(pythonExecutable, 'requirements.txt'),
451
+ functionsDir,
452
+ 'python dependency installation',
453
+ uvEnv,
454
+ false
455
+ );
456
+
457
+ const pythonEnv = buildPythonFunctionsEnv(uvEnv, functionsDir);
299
458
  return bridgePythonCoreToolsForWindowsArm64(functionsDir, pythonEnv);
300
459
  }
301
460
 
@@ -338,7 +497,7 @@ export function buildDevCommand(
338
497
  .option('--host <host>', 'Host name', 'localhost')
339
498
  .option('--open', 'Open browser automatically', false)
340
499
  .option('--verbose', 'Show verbose logs', false)
341
- .option('--no-functions', 'Skip Azure Functions startup', false)
500
+ .option('--no-functions', 'Skip Azure Functions startup')
342
501
  .option('--seed-env <environment>', 'Replace Cosmos DB Emulator data from dev-seeds/<environment> before startup')
343
502
  .option('--mock-connectors', 'Start mock server for connector models (serves Zod-generated data)', false)
344
503
  .action(async (options: ParsedDevActionOptions) => {
@@ -480,6 +639,9 @@ async function startDevEnvironment(options: DevOptions) {
480
639
  let mockServer: ConnectorMockServer | null = null;
481
640
  let envLocalPath = '';
482
641
  let envLocalDefaultUrl = ''; // default Functions URL to restore on shutdown
642
+ const functionsBaseUrl = buildFunctionsBaseUrl(options.host, functionsPort);
643
+ let functionsReadinessPromise: Promise<boolean> | null = null;
644
+ let functionsReady = !!options.noFunctions;
483
645
 
484
646
  // Cleanup processes on Ctrl+C
485
647
  process.on('SIGINT', async () => {
@@ -532,11 +694,14 @@ async function startDevEnvironment(options: DevOptions) {
532
694
  const functionsDir = path.join(process.cwd(), 'functions');
533
695
  const hasFunctions = fs.existsSync(functionsDir) && hasFunctionsProject(functionsDir, backendLanguage);
534
696
 
697
+ let functionsCoreToolsCommand: FunctionsCoreToolsCommand | null = null;
698
+ let installedCoreToolsVersion: string | null = null;
699
+
535
700
  if (hasFunctions && !options.noFunctions) {
536
- // Check if Azure Functions Core Tools is installed
537
- const coreToolsInstalled = await checkCoreTools();
538
-
539
- if (!coreToolsInstalled) {
701
+ installedCoreToolsVersion = await resolveInstalledCoreToolsVersion();
702
+ functionsCoreToolsCommand = buildFunctionsCoreToolsCommand(backendLanguage, installedCoreToolsVersion);
703
+
704
+ if (functionsCoreToolsCommand.command === 'func' && !installedCoreToolsVersion) {
540
705
  console.log('');
541
706
  console.log('āš ļø Azure Functions Core Tools not found.');
542
707
  console.log('');
@@ -585,6 +750,8 @@ async function startDevEnvironment(options: DevOptions) {
585
750
  // Skip Azure Functions startup
586
751
  options.noFunctions = true;
587
752
  }
753
+ } else if (functionsCoreToolsCommand.command !== 'func') {
754
+ console.log(`ā„¹ļø Using ${functionsCoreToolsCommand.label}.`);
588
755
  }
589
756
 
590
757
  if (!options.noFunctions) {
@@ -631,8 +798,7 @@ async function startDevEnvironment(options: DevOptions) {
631
798
  await runCommand(pm, ['install'], functionsDir, `${pm} install`);
632
799
  }
633
800
  } else if (backendLanguage === 'csharp') {
634
- console.log('šŸ“¦ Building C# Azure Functions project...');
635
- await runCommand('dotnet', ['build'], functionsDir, 'dotnet build');
801
+ console.log('ā„¹ļø C# Azure Functions can take longer on cold start while the worker builds.');
636
802
  } else {
637
803
  functionsEnv = await preparePythonFunctionsEnvironment(functionsDir);
638
804
  }
@@ -685,7 +851,8 @@ async function startDevEnvironment(options: DevOptions) {
685
851
  }
686
852
 
687
853
  // Azure Functions ć‚’čµ·å‹•
688
- const funcProcess = spawn('func', buildFunctionsStartArgs(functionsPort), {
854
+ const functionsCommand = functionsCoreToolsCommand ?? buildFunctionsCoreToolsCommand(backendLanguage, installedCoreToolsVersion);
855
+ const funcProcess = spawn(functionsCommand.command, [...functionsCommand.argsPrefix, ...buildFunctionsStartArgs(functionsPort)], {
689
856
  cwd: functionsDir,
690
857
  shell: true,
691
858
  stdio: 'pipe', // Always pipe to capture output
@@ -746,7 +913,11 @@ async function startDevEnvironment(options: DevOptions) {
746
913
  }
747
914
  });
748
915
 
749
- console.log(`āœ… Azure Functions started (port: ${functionsPort})`);
916
+ console.log(`ā³ Waiting for Azure Functions to accept requests at ${functionsBaseUrl}...`);
917
+ functionsReadinessPromise = waitForHttpServerReady(
918
+ functionsBaseUrl,
919
+ getFunctionsReadinessTimeoutMs(backendLanguage)
920
+ );
750
921
  } else if (!hasFunctions) {
751
922
  console.log('');
752
923
  console.log('ā„¹ļø functions/ directory not found. Starting Next.js only.');
@@ -842,8 +1013,8 @@ async function startDevEnvironment(options: DevOptions) {
842
1013
  // When --mock-connectors is active, bffTargetPort = mock port (7072); otherwise = Functions port (7071).
843
1014
  // Next.js may load .env.local values that override spawn env vars, so we must keep them in sync.
844
1015
  envLocalPath = path.join(process.cwd(), '.env.local');
845
- envLocalDefaultUrl = `http://${options.host || 'localhost'}:${functionsPort}`;
846
- const bffTargetUrl = `http://${options.host || 'localhost'}:${bffTargetPort}`;
1016
+ envLocalDefaultUrl = functionsBaseUrl;
1017
+ const bffTargetUrl = buildFunctionsBaseUrl(options.host, bffTargetPort);
847
1018
  try {
848
1019
  if (fs.existsSync(envLocalPath)) {
849
1020
  const envContent = fs.readFileSync(envLocalPath, 'utf-8');
@@ -860,8 +1031,8 @@ async function startDevEnvironment(options: DevOptions) {
860
1031
 
861
1032
  const nextEnv: NodeJS.ProcessEnv = {
862
1033
  ...process.env,
863
- BACKEND_FUNCTIONS_BASE_URL: `http://${options.host || 'localhost'}:${bffTargetPort}`,
864
- FUNCTIONS_BASE_URL: `http://${options.host || 'localhost'}:${bffTargetPort}`,
1034
+ BACKEND_FUNCTIONS_BASE_URL: bffTargetUrl,
1035
+ FUNCTIONS_BASE_URL: bffTargetUrl,
865
1036
  };
866
1037
 
867
1038
  const nextProcess = spawn(pm === 'pnpm' ? 'pnpm' : 'npx', nextArgs, {
@@ -894,19 +1065,31 @@ async function startDevEnvironment(options: DevOptions) {
894
1065
  process.exit(code || 0);
895
1066
  });
896
1067
 
1068
+ if (functionsReadinessPromise) {
1069
+ functionsReady = await functionsReadinessPromise;
1070
+ console.log('');
1071
+ if (functionsReady) {
1072
+ console.log(`āœ… Azure Functions ready (port: ${functionsPort})`);
1073
+ } else {
1074
+ console.log(`āš ļø Azure Functions is still starting: ${functionsBaseUrl}`);
1075
+ }
1076
+ }
1077
+
897
1078
  console.log('');
898
1079
  console.log('āœ… SwallowKit development environment is running!');
899
1080
  console.log('');
900
1081
  console.log(`šŸ“± Next.js: http://${options.host || 'localhost'}:${port}`);
901
1082
  if (hasFunctions && !options.noFunctions) {
902
- console.log(`⚔ Azure Functions: http://${options.host || 'localhost'}:${functionsPort}`);
1083
+ console.log(`${functionsReady ? '⚔ Azure Functions' : 'ā³ Azure Functions (starting)'}: ${functionsBaseUrl}`);
903
1084
  }
904
1085
  if (mockServer) {
905
- console.log(`šŸ”Œ Mock Proxy: http://${options.host || 'localhost'}:${bffTargetPort} (BFF → here)`);
1086
+ console.log(`šŸ”Œ Mock Proxy: ${bffTargetUrl} (BFF → here)`);
906
1087
  }
907
1088
  console.log('');
908
- if (hasFunctions && !options.noFunctions) {
1089
+ if (hasFunctions && !options.noFunctions && functionsReady) {
909
1090
  console.log('šŸ’” Azure Functions and Next.js BFF are connected');
1091
+ } else if (hasFunctions && !options.noFunctions) {
1092
+ console.log('šŸ’” Azure Functions is still warming up; BFF routes can fail until the backend responds.');
910
1093
  }
911
1094
  if (mockServer) {
912
1095
  console.log('šŸ’” Connector models served from mock server (Zod-generated data)');
@@ -931,12 +1114,13 @@ async function runCommand(
931
1114
  args: string[],
932
1115
  cwd: string,
933
1116
  label: string,
934
- env?: NodeJS.ProcessEnv
1117
+ env?: NodeJS.ProcessEnv,
1118
+ shell = true
935
1119
  ): Promise<void> {
936
1120
  await new Promise<void>((resolve, reject) => {
937
1121
  const child = spawn(command, args, {
938
1122
  cwd,
939
- shell: true,
1123
+ shell,
940
1124
  stdio: 'inherit',
941
1125
  env,
942
1126
  });
@@ -15,6 +15,11 @@ import {
15
15
  getFunctionsStartScript,
16
16
  } from "../../utils/package-manager";
17
17
  import { syncProjectManifest } from "../../core/project/manifest";
18
+ import {
19
+ buildCSharpCodegenToolManifestSource,
20
+ buildPythonCodegenRequirementsSource,
21
+ } from "../../core/scaffold/native-schema-generator";
22
+ import { getProjectLocalUvPaths } from "../../utils/python-uv";
18
23
 
19
24
  interface InitOptions {
20
25
  name: string;
@@ -473,13 +478,6 @@ async function addSwallowKitFiles(
473
478
  ...buildGeneratedProjectDependencies(projectName),
474
479
  };
475
480
 
476
- if (backendLanguage !== "typescript") {
477
- packageJson.devDependencies = {
478
- ...packageJson.devDependencies,
479
- '@openapitools/openapi-generator-cli': '^2.21.0',
480
- };
481
- }
482
-
483
481
  packageJson.scripts = {
484
482
  ...packageJson.scripts,
485
483
  'build': getBuildScript(pm),
@@ -955,8 +953,9 @@ tsconfig.json
955
953
  !dist/**/*.js
956
954
  `);
957
955
  } else if (backendLanguage === 'python') {
958
- gitignoreLines.unshift('.venv', '__pycache__', '.python_packages');
956
+ gitignoreLines.unshift('.venv', '.codegen-venv', '__pycache__', '.python_packages');
959
957
  fs.writeFileSync(path.join(functionsDir, '.funcignore'), `.venv
958
+ .codegen-venv
960
959
  __pycache__
961
960
  .pytest_cache
962
961
  .mypy_cache
@@ -1083,6 +1082,11 @@ function createCSharpFunctionsProject(projectDir: string, functionsDir: string):
1083
1082
  const csprojName = `${projectPascal}.Functions.csproj`;
1084
1083
 
1085
1084
  fs.writeFileSync(path.join(functionsDir, csprojName), buildCSharpFunctionsProjectSource());
1085
+ fs.mkdirSync(path.join(functionsDir, '.config'), { recursive: true });
1086
+ fs.writeFileSync(
1087
+ path.join(functionsDir, '.config', 'dotnet-tools.json'),
1088
+ buildCSharpCodegenToolManifestSource()
1089
+ );
1086
1090
 
1087
1091
  fs.writeFileSync(path.join(functionsDir, 'Program.cs'), buildCSharpFunctionsProgramSource());
1088
1092
 
@@ -1125,11 +1129,16 @@ public sealed class GreetFunction
1125
1129
 
1126
1130
  function createPythonFunctionsProject(projectDir: string, functionsDir: string): void {
1127
1131
  fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
1132
+ ensureProjectGitignoreEntry(projectDir, '.uv');
1128
1133
 
1129
1134
  fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
1130
1135
  azure-cosmos>=4.9.0
1131
1136
  azure-identity>=1.19.0
1132
1137
  `);
1138
+ fs.writeFileSync(
1139
+ path.join(functionsDir, 'requirements.codegen.txt'),
1140
+ buildPythonCodegenRequirementsSource()
1141
+ );
1133
1142
 
1134
1143
  const blueprintsDir = path.join(functionsDir, 'blueprints');
1135
1144
  fs.mkdirSync(blueprintsDir, { recursive: true });
@@ -1168,6 +1177,22 @@ app.register_blueprint(greet_bp)
1168
1177
  `);
1169
1178
  }
1170
1179
 
1180
+ function ensureProjectGitignoreEntry(projectDir: string, entry: string): void {
1181
+ const gitignorePath = path.join(projectDir, '.gitignore');
1182
+ if (!fs.existsSync(gitignorePath)) {
1183
+ return;
1184
+ }
1185
+
1186
+ const current = fs.readFileSync(gitignorePath, 'utf8');
1187
+ const lines = current.split(/\r?\n/);
1188
+ if (lines.includes(entry)) {
1189
+ return;
1190
+ }
1191
+
1192
+ const normalized = current.endsWith('\n') ? current : `${current}\n`;
1193
+ fs.writeFileSync(gitignorePath, `${normalized}${entry}\n`);
1194
+ }
1195
+
1171
1196
  async function createBffApiRoute(projectDir: string) {
1172
1197
  console.log('šŸ“¦ Creating BFF API route...\n');
1173
1198
 
@@ -1398,17 +1423,18 @@ function createReadme(
1398
1423
  const backendLanguageLabel = getBackendLanguageLabel(backendLanguage);
1399
1424
  const schemaBridgeDescription = backendLanguage === 'typescript'
1400
1425
  ? 'Zod (shared between frontend and backend)'
1401
- : `Zod + OpenAPI bridge (Zod in shared/, generated ${backendLanguageLabel} schemas in functions/generated/)`;
1426
+ : `Zod + OpenAPI export (Zod in shared/, native-generated ${backendLanguageLabel} schemas in functions/generated/)`;
1402
1427
  const functionsTree = backendLanguage === 'typescript'
1403
1428
  ? `│ └── src/\n│ └── greet.ts # Sample function`
1404
1429
  : backendLanguage === 'csharp'
1405
- ? `│ ā”œā”€ā”€ Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ # OpenAPI-derived C# models`
1406
- : `│ ā”œā”€ā”€ blueprints/\n│ │ └── greet.py\n│ └── generated/ # OpenAPI-derived Python models`;
1430
+ ? `│ ā”œā”€ā”€ Crud/\n│ │ └── GreetFunction.cs\n│ └── generated/ # Native-generated C# schema assets`
1431
+ : `│ ā”œā”€ā”€ blueprints/\n│ │ └── greet.py\n│ └── generated/ # Native-generated Python schema assets`;
1407
1432
  const backendScaffoldNote = backendLanguage === 'typescript'
1408
1433
  ? '- Azure Functions CRUD endpoints'
1409
- : `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI spec + generated ${backendLanguageLabel} schema assets`;
1434
+ : `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI export + native-generated ${backendLanguageLabel} schema assets`;
1435
+ const pythonUvPaths = getProjectLocalUvPaths(projectDir);
1410
1436
  const pythonLocalDevNote = backendLanguage === 'python'
1411
- ? `\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`
1437
+ ? `\n**Python local dev note**: SwallowKit uses \`uv\` for Python backends and keeps the managed Python runtime under \`${path.relative(projectDir, pythonUvPaths.pythonInstallDir)}\`. Local Azure Functions runs from \`functions/.venv\`, schema generation uses \`functions/.codegen-venv\`, and \`swallowkit dev\` bootstraps a project-local \`uv\` binary automatically when needed. Keep \`functions/requirements.txt\` and \`functions/requirements.codegen.txt\` as the dependency sources of truth.\n`
1412
1438
  : '';
1413
1439
 
1414
1440
  const readme = `# ${projectName}
@@ -1626,14 +1652,14 @@ function createAiAgentFiles(projectDir: string, projectName: string, backendLang
1626
1652
  const functionsStructureLine = backendLanguage === 'typescript'
1627
1653
  ? `│ └── src/ # HTTP trigger handlers with Cosmos DB bindings`
1628
1654
  : backendLanguage === 'csharp'
1629
- ? `│ ā”œā”€ā”€ Crud/ # C# HTTP trigger handlers\n│ └── generated/ # OpenAPI-derived C# schema assets`
1630
- : `│ ā”œā”€ā”€ blueprints/ # Python HTTP trigger handlers\n│ └── generated/ # OpenAPI-derived Python schema assets`;
1655
+ ? `│ ā”œā”€ā”€ Crud/ # C# HTTP trigger handlers\n│ └── generated/ # Native-generated C# schema assets`
1656
+ : `│ ā”œā”€ā”€ blueprints/ # Python HTTP trigger handlers\n│ └── generated/ # Native-generated Python schema assets`;
1631
1657
  const backendSchemaNote = backendLanguage === 'typescript'
1632
1658
  ? `- The shared package (\`@${projectName}/shared\`) is consumed by both Next.js and Azure Functions as a workspace dependency.`
1633
- : `- 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.`;
1659
+ : `- 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/\` with native ${backendLanguageLabel} tooling.`;
1634
1660
  const backendRulesNote = backendLanguage === 'typescript'
1635
1661
  ? `- 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.`
1636
- : `- 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\`.`;
1662
+ : `- 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 native-generated schema assets in \`functions/generated/\` to keep backend contracts aligned.\n- The backend should still own \`id\`, \`createdAt\`, and \`updatedAt\`.`;
1637
1663
 
1638
1664
  // ── 1. AGENTS.md (Codex / generic agents) ──────────────────────────
1639
1665
 
@@ -1748,7 +1774,7 @@ Key rules:
1748
1774
 
1749
1775
  - ${backendRulesNote}
1750
1776
 
1751
- ${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.`}
1777
+ ${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 the native schema assets under \`functions/generated/\` in sync.`}
1752
1778
 
1753
1779
  ${backendLanguage === 'typescript' ? `\`\`\`typescript
1754
1780
  // functions/src/{model}.ts
@@ -2018,7 +2044,7 @@ Files in \`functions/\` contain all business logic and data access for this appl
2018
2044
 
2019
2045
  - Keep backend contracts aligned with \`shared/models/\` by rerunning \`swallowkit scaffold\` after schema changes.
2020
2046
  - For TypeScript backends, use Cosmos DB **input/output bindings** (\`extraInputs\`/\`extraOutputs\`) for reads and writes.
2021
- - For C#/Python backends, consume the generated OpenAPI-derived assets in \`functions/generated/\`.
2047
+ - For C#/Python backends, consume the native-generated assets in \`functions/generated/\`.
2022
2048
  - Auto-generate \`id\` (UUID), \`createdAt\`, and \`updatedAt\` on the backend. Never trust client-sent values.
2023
2049
  - Container names are PascalCase + 's' (e.g., \`Todos\`). Partition key defaults to \`/id\` but can be customized per model.
2024
2050