swallowkit 1.0.0-beta.23 → 1.0.0-beta.25

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.
@@ -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 {
@@ -100,6 +111,36 @@ export function buildNextDevArgs(pm: string, port: string): string[] {
100
111
  return pm === 'pnpm' ? ['exec', ...baseArgs] : baseArgs;
101
112
  }
102
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
+
103
144
  export function getPythonVirtualEnvPaths(functionsDir: string): {
104
145
  venvDir: string;
105
146
  binDir: string;
@@ -157,10 +198,20 @@ async function checkCoreTools(): Promise<boolean> {
157
198
  return checkCommand('func', ['--version']);
158
199
  }
159
200
 
160
- 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> {
161
210
  return new Promise((resolve) => {
162
211
  const checkProcess = spawn(command, args, {
163
- shell: true,
212
+ cwd: options?.cwd,
213
+ env: options?.env ?? process.env,
214
+ shell: options?.shell ?? true,
164
215
  stdio: 'pipe',
165
216
  });
166
217
 
@@ -174,26 +225,34 @@ async function checkCommand(command: string, args: string[] = ['--version']): Pr
174
225
  });
175
226
  }
176
227
 
177
- async function resolvePythonBootstrapCommand(): Promise<{ command: string; argsPrefix: string[]; label: string }> {
178
- const candidates = process.platform === 'win32'
179
- ? [
180
- { command: 'py', argsPrefix: ['-3.11'], label: 'py -3.11' },
181
- { command: 'python', argsPrefix: [], label: 'python' },
182
- ]
183
- : [
184
- { command: 'python3', argsPrefix: [], label: 'python3' },
185
- { command: 'python', argsPrefix: [], label: 'python' },
186
- ];
187
-
188
- for (const candidate of candidates) {
189
- if (await checkCommand(candidate.command, [...candidate.argsPrefix, '--version'])) {
190
- return candidate;
191
- }
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 };
192
238
  }
193
239
 
194
- throw new Error(
195
- '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
196
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 };
197
256
  }
198
257
 
199
258
  async function getCommandPath(command: string): Promise<string | null> {
@@ -256,6 +315,41 @@ async function captureCommandOutput(
256
315
  });
257
316
  }
258
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
+
259
353
  async function resolvePythonRuntimeDetails(
260
354
  functionsDir: string,
261
355
  env: NodeJS.ProcessEnv
@@ -331,47 +425,36 @@ async function bridgePythonCoreToolsForWindowsArm64(
331
425
  }
332
426
 
333
427
  async function preparePythonFunctionsEnvironment(functionsDir: string): Promise<NodeJS.ProcessEnv> {
334
- const { pythonExecutable } = getPythonVirtualEnvPaths(functionsDir);
335
- const hasUv = await checkCommand('uv', ['--version']);
336
-
337
- if (!fs.existsSync(pythonExecutable)) {
338
- if (hasUv) {
339
- console.log('📦 Creating Python virtual environment with uv...');
340
- await runCommand('uv', ['venv', '.venv', '--python', '3.11'], functionsDir, 'python virtual environment setup');
341
- } else {
342
- const bootstrap = await resolvePythonBootstrapCommand();
343
- console.log(`📦 Creating Python virtual environment with ${bootstrap.label}...`);
344
- await runCommand(
345
- bootstrap.command,
346
- [...bootstrap.argsPrefix, '-m', 'venv', '.venv'],
347
- functionsDir,
348
- 'python virtual environment setup'
349
- );
350
- }
351
- }
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
+ });
352
436
 
353
- const pythonEnv = buildPythonFunctionsEnv(process.env, functionsDir);
354
- 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
+ }
355
442
 
356
- if (hasUv) {
357
- await runCommand(
358
- 'uv',
359
- ['pip', 'install', '--python', pythonExecutable, '-r', 'requirements.txt'],
360
- functionsDir,
361
- 'python dependency installation',
362
- pythonEnv
363
- );
364
- } else {
365
- await runCommand('python', ['-m', 'pip', 'install', '--upgrade', 'pip'], functionsDir, 'python pip upgrade', pythonEnv);
366
- await runCommand(
367
- 'python',
368
- ['-m', 'pip', 'install', '-r', 'requirements.txt'],
369
- functionsDir,
370
- 'python dependency installation',
371
- pythonEnv
372
- );
443
+ console.log('📦 Creating Python virtual environment with uv...');
444
+ await runCommand(uvCommand, venvArgs, functionsDir, 'python virtual environment setup', uvEnv, false);
373
445
  }
374
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);
375
458
  return bridgePythonCoreToolsForWindowsArm64(functionsDir, pythonEnv);
376
459
  }
377
460
 
@@ -556,6 +639,9 @@ async function startDevEnvironment(options: DevOptions) {
556
639
  let mockServer: ConnectorMockServer | null = null;
557
640
  let envLocalPath = '';
558
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;
559
645
 
560
646
  // Cleanup processes on Ctrl+C
561
647
  process.on('SIGINT', async () => {
@@ -712,19 +798,7 @@ async function startDevEnvironment(options: DevOptions) {
712
798
  await runCommand(pm, ['install'], functionsDir, `${pm} install`);
713
799
  }
714
800
  } else if (backendLanguage === 'csharp') {
715
- let cleanedArtifacts = false;
716
- for (const artifactPath of getCSharpFunctionsBuildArtifactPaths(functionsDir)) {
717
- if (!fs.existsSync(artifactPath)) {
718
- continue;
719
- }
720
-
721
- fs.rmSync(artifactPath, { recursive: true, force: true });
722
- cleanedArtifacts = true;
723
- }
724
-
725
- if (cleanedArtifacts) {
726
- console.log('🧹 Cleaned previous C# Functions build artifacts before func start.');
727
- }
801
+ console.log('ℹ️ C# Azure Functions can take longer on cold start while the worker builds.');
728
802
  } else {
729
803
  functionsEnv = await preparePythonFunctionsEnvironment(functionsDir);
730
804
  }
@@ -839,7 +913,11 @@ async function startDevEnvironment(options: DevOptions) {
839
913
  }
840
914
  });
841
915
 
842
- 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
+ );
843
921
  } else if (!hasFunctions) {
844
922
  console.log('');
845
923
  console.log('ℹ️ functions/ directory not found. Starting Next.js only.');
@@ -935,8 +1013,8 @@ async function startDevEnvironment(options: DevOptions) {
935
1013
  // When --mock-connectors is active, bffTargetPort = mock port (7072); otherwise = Functions port (7071).
936
1014
  // Next.js may load .env.local values that override spawn env vars, so we must keep them in sync.
937
1015
  envLocalPath = path.join(process.cwd(), '.env.local');
938
- envLocalDefaultUrl = `http://${options.host || 'localhost'}:${functionsPort}`;
939
- const bffTargetUrl = `http://${options.host || 'localhost'}:${bffTargetPort}`;
1016
+ envLocalDefaultUrl = functionsBaseUrl;
1017
+ const bffTargetUrl = buildFunctionsBaseUrl(options.host, bffTargetPort);
940
1018
  try {
941
1019
  if (fs.existsSync(envLocalPath)) {
942
1020
  const envContent = fs.readFileSync(envLocalPath, 'utf-8');
@@ -953,8 +1031,8 @@ async function startDevEnvironment(options: DevOptions) {
953
1031
 
954
1032
  const nextEnv: NodeJS.ProcessEnv = {
955
1033
  ...process.env,
956
- BACKEND_FUNCTIONS_BASE_URL: `http://${options.host || 'localhost'}:${bffTargetPort}`,
957
- FUNCTIONS_BASE_URL: `http://${options.host || 'localhost'}:${bffTargetPort}`,
1034
+ BACKEND_FUNCTIONS_BASE_URL: bffTargetUrl,
1035
+ FUNCTIONS_BASE_URL: bffTargetUrl,
958
1036
  };
959
1037
 
960
1038
  const nextProcess = spawn(pm === 'pnpm' ? 'pnpm' : 'npx', nextArgs, {
@@ -987,19 +1065,31 @@ async function startDevEnvironment(options: DevOptions) {
987
1065
  process.exit(code || 0);
988
1066
  });
989
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
+
990
1078
  console.log('');
991
1079
  console.log('✅ SwallowKit development environment is running!');
992
1080
  console.log('');
993
1081
  console.log(`📱 Next.js: http://${options.host || 'localhost'}:${port}`);
994
1082
  if (hasFunctions && !options.noFunctions) {
995
- console.log(`⚡ Azure Functions: http://${options.host || 'localhost'}:${functionsPort}`);
1083
+ console.log(`${functionsReady ? '⚡ Azure Functions' : '⏳ Azure Functions (starting)'}: ${functionsBaseUrl}`);
996
1084
  }
997
1085
  if (mockServer) {
998
- console.log(`🔌 Mock Proxy: http://${options.host || 'localhost'}:${bffTargetPort} (BFF → here)`);
1086
+ console.log(`🔌 Mock Proxy: ${bffTargetUrl} (BFF → here)`);
999
1087
  }
1000
1088
  console.log('');
1001
- if (hasFunctions && !options.noFunctions) {
1089
+ if (hasFunctions && !options.noFunctions && functionsReady) {
1002
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.');
1003
1093
  }
1004
1094
  if (mockServer) {
1005
1095
  console.log('💡 Connector models served from mock server (Zod-generated data)');
@@ -1024,12 +1114,13 @@ async function runCommand(
1024
1114
  args: string[],
1025
1115
  cwd: string,
1026
1116
  label: string,
1027
- env?: NodeJS.ProcessEnv
1117
+ env?: NodeJS.ProcessEnv,
1118
+ shell = true
1028
1119
  ): Promise<void> {
1029
1120
  await new Promise<void>((resolve, reject) => {
1030
1121
  const child = spawn(command, args, {
1031
1122
  cwd,
1032
- shell: true,
1123
+ shell,
1033
1124
  stdio: 'inherit',
1034
1125
  env,
1035
1126
  });
@@ -19,6 +19,7 @@ import {
19
19
  buildCSharpCodegenToolManifestSource,
20
20
  buildPythonCodegenRequirementsSource,
21
21
  } from "../../core/scaffold/native-schema-generator";
22
+ import { getProjectLocalUvPaths } from "../../utils/python-uv";
22
23
 
23
24
  interface InitOptions {
24
25
  name: string;
@@ -1128,6 +1129,7 @@ public sealed class GreetFunction
1128
1129
 
1129
1130
  function createPythonFunctionsProject(projectDir: string, functionsDir: string): void {
1130
1131
  fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
1132
+ ensureProjectGitignoreEntry(projectDir, '.uv');
1131
1133
 
1132
1134
  fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
1133
1135
  azure-cosmos>=4.9.0
@@ -1175,6 +1177,22 @@ app.register_blueprint(greet_bp)
1175
1177
  `);
1176
1178
  }
1177
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
+
1178
1196
  async function createBffApiRoute(projectDir: string) {
1179
1197
  console.log('📦 Creating BFF API route...\n');
1180
1198
 
@@ -1414,8 +1432,9 @@ function createReadme(
1414
1432
  const backendScaffoldNote = backendLanguage === 'typescript'
1415
1433
  ? '- Azure Functions CRUD endpoints'
1416
1434
  : `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI export + native-generated ${backendLanguageLabel} schema assets`;
1435
+ const pythonUvPaths = getProjectLocalUvPaths(projectDir);
1417
1436
  const pythonLocalDevNote = backendLanguage === 'python'
1418
- ? `\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. Scaffold uses a separate \`functions/.codegen-venv\` for Python schema generation.\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`
1419
1438
  : '';
1420
1439
 
1421
1440
  const readme = `# ${projectName}
@@ -4,33 +4,45 @@ import { spawn, spawnSync } from "child_process";
4
4
  import { BackendLanguage } from "../../types";
5
5
  import { ModelInfo, toKebabCase } from "./model-parser";
6
6
  import { generateOpenApiDocument } from "./openapi-generator";
7
+ import {
8
+ buildProjectLocalUvEnv,
9
+ buildProjectLocalUvInstallerEnv,
10
+ buildUvPipInstallArgs,
11
+ buildUvVenvArgs,
12
+ getProjectLocalUvInstallerCommand,
13
+ getProjectLocalUvPaths,
14
+ getPythonProjectRoot,
15
+ } from "../../utils/python-uv";
7
16
 
8
17
  export const NSWAG_CONSOLECORE_VERSION = "14.7.1";
9
18
  export const PYTHON_SCHEMA_CODEGEN_REQUIREMENT = "datamodel-code-generator>=0.44.0,<1.0.0";
10
19
  const PYTHON_OUTPUT_MODEL_TYPE = "pydantic_v2.BaseModel";
11
20
 
12
- type PythonLauncher = {
13
- command: string;
14
- argsPrefix: string[];
15
- };
16
-
17
21
  function getMachineAwareStdio(): "inherit" | "pipe" {
18
22
  return process.env.SWALLOWKIT_MACHINE_OUTPUT === "1" ? "pipe" : "inherit";
19
23
  }
20
24
 
21
- function canRun(command: string, args: string[], cwd: string): boolean {
25
+ function canRun(command: string, args: string[], cwd: string, env?: NodeJS.ProcessEnv): boolean {
22
26
  const result = spawnSync(command, args, {
23
27
  cwd,
28
+ env,
24
29
  stdio: "ignore",
25
30
  });
26
31
 
27
32
  return !result.error && result.status === 0;
28
33
  }
29
34
 
30
- async function runCommand(command: string, args: string[], cwd: string, errorMessage: string): Promise<void> {
35
+ async function runCommand(
36
+ command: string,
37
+ args: string[],
38
+ cwd: string,
39
+ errorMessage: string,
40
+ env?: NodeJS.ProcessEnv
41
+ ): Promise<void> {
31
42
  await new Promise<void>((resolve, reject) => {
32
43
  const child = spawn(command, args, {
33
44
  cwd,
45
+ env,
34
46
  stdio: getMachineAwareStdio(),
35
47
  });
36
48
 
@@ -611,46 +623,63 @@ function getVirtualEnvPythonPath(venvDir: string): string {
611
623
  : path.join(venvDir, "bin", "python");
612
624
  }
613
625
 
614
- function detectSystemPythonLauncher(functionsRoot: string): PythonLauncher {
615
- const candidates: PythonLauncher[] = [
616
- { command: "python", argsPrefix: [] },
617
- { command: "py", argsPrefix: ["-3"] },
618
- { command: "python3", argsPrefix: [] },
619
- ];
626
+ async function ensureProjectLocalUvCommand(projectRoot: string): Promise<{ command: string; env: NodeJS.ProcessEnv }> {
627
+ const uvEnv = buildProjectLocalUvEnv(process.env, projectRoot);
628
+ const { localUvExecutable } = getProjectLocalUvPaths(projectRoot);
620
629
 
621
- for (const candidate of candidates) {
622
- if (canRun(candidate.command, [...candidate.argsPrefix, "--version"], functionsRoot)) {
623
- return candidate;
624
- }
630
+ if (canRun("uv", ["--version"], projectRoot)) {
631
+ return { command: "uv", env: uvEnv };
632
+ }
633
+
634
+ if (fs.existsSync(localUvExecutable) && canRun(localUvExecutable, ["--version"], projectRoot)) {
635
+ return { command: localUvExecutable, env: uvEnv };
625
636
  }
626
637
 
627
- throw new Error(
628
- "Python 3.11+ is required to generate backend schema assets.\n" +
629
- "Install Python and retry, or create functions/.codegen-venv manually."
638
+ const installer = getProjectLocalUvInstallerCommand();
639
+ await runCommand(
640
+ installer.command,
641
+ installer.args,
642
+ projectRoot,
643
+ "Failed to install project-local uv.",
644
+ buildProjectLocalUvInstallerEnv(process.env, projectRoot)
630
645
  );
646
+
647
+ if (!(fs.existsSync(localUvExecutable) && canRun(localUvExecutable, ["--version"], projectRoot))) {
648
+ throw new Error("Failed to install project-local uv.");
649
+ }
650
+
651
+ return { command: localUvExecutable, env: uvEnv };
631
652
  }
632
653
 
633
654
  async function ensurePythonCodegenEnvironment(functionsRoot: string): Promise<string> {
634
655
  const requirementsPath = ensurePythonCodegenProjectFiles(functionsRoot);
656
+ const projectRoot = getPythonProjectRoot(functionsRoot);
657
+ const { command: uvCommand, env: uvEnv } = await ensureProjectLocalUvCommand(projectRoot);
635
658
  const venvDir = path.join(functionsRoot, ".codegen-venv");
636
659
  const venvPython = getVirtualEnvPythonPath(venvDir);
637
660
 
638
- if (!fs.existsSync(venvPython)) {
639
- const launcher = detectSystemPythonLauncher(functionsRoot);
661
+ if (!canRun(venvPython, ["--version"], functionsRoot, uvEnv)) {
662
+ const venvArgs = buildUvVenvArgs(venvDir);
663
+ if (fs.existsSync(venvDir)) {
664
+ venvArgs.push("--clear");
665
+ }
666
+
640
667
  await runCommand(
641
- launcher.command,
642
- [...launcher.argsPrefix, "-m", "venv", venvDir],
668
+ uvCommand,
669
+ venvArgs,
643
670
  functionsRoot,
644
- "Failed to create the Python schema code generation virtual environment."
671
+ "Failed to create the Python schema code generation virtual environment.",
672
+ uvEnv
645
673
  );
646
674
  }
647
675
 
648
- if (!canRun(venvPython, ["-c", "import datamodel_code_generator"], functionsRoot)) {
676
+ if (!canRun(venvPython, ["-c", "import datamodel_code_generator"], functionsRoot, uvEnv)) {
649
677
  await runCommand(
650
- venvPython,
651
- ["-m", "pip", "install", "--disable-pip-version-check", "-r", requirementsPath],
678
+ uvCommand,
679
+ buildUvPipInstallArgs(venvPython, requirementsPath),
652
680
  functionsRoot,
653
- "Failed to install Python schema generation dependencies."
681
+ "Failed to install Python schema generation dependencies.",
682
+ uvEnv
654
683
  );
655
684
  }
656
685
 
@@ -0,0 +1,96 @@
1
+ import * as path from "path";
2
+
3
+ export const SWALLOWKIT_PYTHON_VERSION = "3.11";
4
+
5
+ export interface ProjectLocalUvPaths {
6
+ stateDir: string;
7
+ cacheDir: string;
8
+ pythonInstallDir: string;
9
+ toolDir: string;
10
+ toolBinDir: string;
11
+ localUvInstallDir: string;
12
+ localUvExecutable: string;
13
+ }
14
+
15
+ export function getPythonProjectRoot(functionsDir: string): string {
16
+ return path.dirname(functionsDir);
17
+ }
18
+
19
+ export function getProjectLocalUvPaths(projectRoot: string): ProjectLocalUvPaths {
20
+ const stateDir = path.join(projectRoot, ".uv");
21
+ const localUvInstallDir = path.join(stateDir, "bin");
22
+
23
+ return {
24
+ stateDir,
25
+ cacheDir: path.join(stateDir, "cache"),
26
+ pythonInstallDir: path.join(stateDir, "python"),
27
+ toolDir: path.join(stateDir, "tools"),
28
+ toolBinDir: path.join(stateDir, "tools", "bin"),
29
+ localUvInstallDir,
30
+ localUvExecutable: process.platform === "win32"
31
+ ? path.join(localUvInstallDir, "uv.exe")
32
+ : path.join(localUvInstallDir, "uv"),
33
+ };
34
+ }
35
+
36
+ export function buildProjectLocalUvEnv(
37
+ baseEnv: NodeJS.ProcessEnv,
38
+ projectRoot: string
39
+ ): NodeJS.ProcessEnv {
40
+ const uvPaths = getProjectLocalUvPaths(projectRoot);
41
+ const env: NodeJS.ProcessEnv = {
42
+ ...baseEnv,
43
+ UV_CACHE_DIR: uvPaths.cacheDir,
44
+ UV_PYTHON_INSTALL_DIR: uvPaths.pythonInstallDir,
45
+ UV_TOOL_DIR: uvPaths.toolDir,
46
+ UV_TOOL_BIN_DIR: uvPaths.toolBinDir,
47
+ UV_PYTHON_PREFERENCE: "only-managed",
48
+ };
49
+
50
+ if (process.platform === "win32") {
51
+ env.UV_PYTHON_NO_REGISTRY = "true";
52
+ }
53
+
54
+ return env;
55
+ }
56
+
57
+ export function buildProjectLocalUvInstallerEnv(
58
+ baseEnv: NodeJS.ProcessEnv,
59
+ projectRoot: string
60
+ ): NodeJS.ProcessEnv {
61
+ const uvPaths = getProjectLocalUvPaths(projectRoot);
62
+
63
+ return {
64
+ ...buildProjectLocalUvEnv(baseEnv, projectRoot),
65
+ UV_UNMANAGED_INSTALL: uvPaths.localUvInstallDir,
66
+ UV_NO_MODIFY_PATH: "1",
67
+ };
68
+ }
69
+
70
+ export function getProjectLocalUvInstallerCommand(): { command: string; args: string[] } {
71
+ if (process.platform === "win32") {
72
+ return {
73
+ command: "powershell",
74
+ args: [
75
+ "-NoProfile",
76
+ "-ExecutionPolicy",
77
+ "Bypass",
78
+ "-Command",
79
+ "$ProgressPreference = 'SilentlyContinue'; irm https://astral.sh/uv/install.ps1 | iex",
80
+ ],
81
+ };
82
+ }
83
+
84
+ return {
85
+ command: "sh",
86
+ args: ["-c", "curl -LsSf https://astral.sh/uv/install.sh | sh"],
87
+ };
88
+ }
89
+
90
+ export function buildUvVenvArgs(venvDir: string, pythonVersion = SWALLOWKIT_PYTHON_VERSION): string[] {
91
+ return ["venv", venvDir, "--python", pythonVersion];
92
+ }
93
+
94
+ export function buildUvPipInstallArgs(pythonExecutable: string, requirementsPath: string): string[] {
95
+ return ["pip", "install", "--python", pythonExecutable, "-r", requirementsPath];
96
+ }