pyfig 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +116 -123
  2. package/SECURITY.md +2 -2
  3. package/dist/cjs/index.js +525 -888
  4. package/dist/cjs/index.js.map +15 -15
  5. package/dist/cjs/sync-resolver.js +482 -676
  6. package/dist/cjs/sync-resolver.js.map +13 -12
  7. package/dist/cli.js +7 -10
  8. package/dist/cli.js.map +1 -1
  9. package/dist/client.d.ts +3 -8
  10. package/dist/client.d.ts.map +1 -1
  11. package/dist/client.js +13 -11
  12. package/dist/client.js.map +1 -1
  13. package/dist/config.d.ts +9 -18
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +22 -59
  16. package/dist/config.js.map +1 -1
  17. package/dist/index.d.ts +5 -6
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/package-target.d.ts +7 -11
  22. package/dist/package-target.d.ts.map +1 -1
  23. package/dist/package-target.js +41 -57
  24. package/dist/package-target.js.map +1 -1
  25. package/dist/probes.d.ts +1 -19
  26. package/dist/probes.d.ts.map +1 -1
  27. package/dist/probes.js +13 -74
  28. package/dist/probes.js.map +1 -1
  29. package/dist/process.d.ts +1 -1
  30. package/dist/process.d.ts.map +1 -1
  31. package/dist/process.js +2 -1
  32. package/dist/process.js.map +1 -1
  33. package/dist/project-root.d.ts +1 -1
  34. package/dist/project-root.d.ts.map +1 -1
  35. package/dist/project-root.js +3 -64
  36. package/dist/project-root.js.map +1 -1
  37. package/dist/provision-lock.d.ts +10 -0
  38. package/dist/provision-lock.d.ts.map +1 -0
  39. package/dist/provision-lock.js +24 -0
  40. package/dist/provision-lock.js.map +1 -0
  41. package/dist/provision.d.ts +18 -6
  42. package/dist/provision.d.ts.map +1 -1
  43. package/dist/provision.js +297 -235
  44. package/dist/provision.js.map +1 -1
  45. package/dist/python-version.d.ts +10 -0
  46. package/dist/python-version.d.ts.map +1 -0
  47. package/dist/python-version.js +17 -0
  48. package/dist/python-version.js.map +1 -0
  49. package/dist/runners.d.ts +3 -7
  50. package/dist/runners.d.ts.map +1 -1
  51. package/dist/runners.js +35 -115
  52. package/dist/runners.js.map +1 -1
  53. package/dist/sync-resolver.js +4 -13
  54. package/dist/sync-resolver.js.map +1 -1
  55. package/dist/virtual-environment.d.ts +3 -1
  56. package/dist/virtual-environment.d.ts.map +1 -1
  57. package/dist/virtual-environment.js +8 -1
  58. package/dist/virtual-environment.js.map +1 -1
  59. package/package.json +9 -10
  60. package/dist/coordination.d.ts +0 -25
  61. package/dist/coordination.d.ts.map +0 -1
  62. package/dist/coordination.js +0 -187
  63. package/dist/coordination.js.map +0 -1
  64. package/dist/state.d.ts +0 -10
  65. package/dist/state.d.ts.map +0 -1
  66. package/dist/state.js +0 -62
  67. package/dist/state.js.map +0 -1
@@ -66,20 +66,19 @@ var __export = (target, all) => {
66
66
  var import_node_fs5 = require("node:fs");
67
67
 
68
68
  // src/provision.ts
69
- var import_node_crypto3 = require("node:crypto");
69
+ var import_node_crypto = require("node:crypto");
70
70
  var import_node_fs3 = require("node:fs");
71
- var import_promises2 = require("node:fs/promises");
71
+ var import_promises = require("node:fs/promises");
72
72
  var import_node_fs4 = __toESM(require("node:fs"), 1);
73
73
  var import_node_os = __toESM(require("node:os"), 1);
74
- var import_node_path7 = __toESM(require("node:path"), 1);
74
+ var import_node_path5 = __toESM(require("node:path"), 1);
75
75
  var import_node_stream = require("node:stream");
76
- var import_promises3 = require("node:stream/promises");
77
- var import_extract_zip = __toESM(require("extract-zip"), 1);
76
+ var import_promises2 = require("node:stream/promises");
77
+ var import_fflate = require("fflate");
78
78
  var tar = __toESM(require("tar"), 1);
79
79
  var import_undici = require("undici");
80
80
 
81
81
  // src/config.ts
82
- var import_node_crypto = require("node:crypto");
83
82
  var import_node_path2 = __toESM(require("node:path"), 1);
84
83
  var envPaths = __toESM(require("env-paths"), 1);
85
84
  var import_semver = __toESM(require("semver"), 1);
@@ -87,69 +86,13 @@ var import_semver = __toESM(require("semver"), 1);
87
86
  // src/project-root.ts
88
87
  var import_node_fs = __toESM(require("node:fs"), 1);
89
88
  var import_node_path = __toESM(require("node:path"), 1);
90
- var import_pacwich = require("pacwich");
91
- var PACKAGE_MANAGER_LOCKFILES = [
92
- "bun.lock",
93
- "bun.lockb",
94
- "npm-shrinkwrap.json",
95
- "package-lock.json",
96
- "pnpm-lock.yaml",
97
- "yarn.lock"
98
- ];
99
- var PYTHON_PROJECT_MARKERS = [
100
- "pyproject.toml",
101
- "uv.lock"
102
- ];
103
89
  function resolveProjectRoot(startDirectory = process.cwd()) {
104
90
  const start = import_node_path.default.resolve(startDirectory);
105
- const workspaceRoot = findWorkspaceProjectRoot(start);
106
- if (workspaceRoot) {
107
- try {
108
- return import_node_path.default.resolve(import_pacwich.createFileSystemProject({
109
- rootDirectory: start,
110
- disableExecutableConfigs: true
111
- }).rootDirectory);
112
- } catch {
113
- return workspaceRoot;
114
- }
115
- }
116
- return findConventionalProjectRoot(start);
117
- }
118
- function findWorkspaceProjectRoot(startDirectory) {
119
- for (const directory of ancestors(startDirectory)) {
120
- const packageJson = import_node_path.default.join(directory, "package.json");
121
- if (!import_node_fs.default.existsSync(packageJson))
122
- continue;
123
- if (import_node_fs.default.existsSync(import_node_path.default.join(directory, "pnpm-workspace.yaml")))
124
- return directory;
125
- try {
126
- const manifest = JSON.parse(import_node_fs.default.readFileSync(packageJson, "utf8"));
127
- if (hasWorkspaceDeclaration(manifest.workspaces))
128
- return directory;
129
- } catch {}
130
- }
131
- return null;
132
- }
133
- function findConventionalProjectRoot(startDirectory) {
134
- let nearestPackageDirectory = null;
135
- let nearestPythonDirectory = null;
136
- for (const directory of ancestors(startDirectory)) {
137
- const hasPackage = import_node_fs.default.existsSync(import_node_path.default.join(directory, "package.json"));
138
- if (hasPackage && !nearestPackageDirectory)
139
- nearestPackageDirectory = directory;
140
- if (!nearestPythonDirectory && PYTHON_PROJECT_MARKERS.some((file) => import_node_fs.default.existsSync(import_node_path.default.join(directory, file)))) {
141
- nearestPythonDirectory = directory;
142
- }
143
- if (hasPackage && PACKAGE_MANAGER_LOCKFILES.some((file) => import_node_fs.default.existsSync(import_node_path.default.join(directory, file)))) {
144
- return directory;
145
- }
91
+ for (const directory of ancestors(start)) {
146
92
  if (import_node_fs.default.existsSync(import_node_path.default.join(directory, ".git")))
147
93
  return directory;
148
94
  }
149
- return nearestPackageDirectory ?? nearestPythonDirectory ?? startDirectory;
150
- }
151
- function hasWorkspaceDeclaration(value) {
152
- return Array.isArray(value) || value !== null && typeof value === "object";
95
+ return start;
153
96
  }
154
97
  function* ancestors(startDirectory) {
155
98
  let directory = startDirectory;
@@ -162,16 +105,26 @@ function* ancestors(startDirectory) {
162
105
  }
163
106
  }
164
107
 
108
+ // src/python-version.ts
109
+ var import_pep440 = require("@renovatebot/pep440");
110
+ function isValidPythonSpecifier(specifier) {
111
+ return import_pep440.validRange(specifier);
112
+ }
113
+ function isValidPythonVersion(version) {
114
+ return import_pep440.valid(version) !== null;
115
+ }
116
+ function pythonVersionSatisfies(version, specifier) {
117
+ return import_pep440.filter([version], specifier).length === 1;
118
+ }
119
+
165
120
  // src/config.ts
166
- var DEFAULT_PYTHON_RANGE = ">=3.10.0 <4.0.0";
167
- var DEFAULT_PIP_RANGE = ">=20.3.0";
121
+ var DEFAULT_REQUIRES_PYTHON = ">=3.10,<4";
168
122
  var DEFAULT_MANAGED_PYTHON = "3.12";
169
123
  var DEFAULT_UV_VERSION = "0.13.6";
170
- var DEFAULT_UV_RANGE = `>=${DEFAULT_UV_VERSION} <1.0.0`;
124
+ var DEFAULT_UV_RANGE = ">=0.10.0 <1.0.0";
171
125
  var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
172
126
  var DEFAULT_INSTALL_TIMEOUT_MS = 10 * 60000;
173
127
  var DEFAULT_DOWNLOAD_TIMEOUT_MS = 5 * 60000;
174
- var DEFAULT_CACHE_TTL_MS = 30000;
175
128
  var envPathsFunction = (() => {
176
129
  const imported = envPaths.default;
177
130
  const implementation = typeof imported === "function" ? imported : imported.default;
@@ -181,30 +134,26 @@ var envPathsFunction = (() => {
181
134
  })();
182
135
  function resolveOptions(options = {}) {
183
136
  const env = { ...options.env ?? process.env };
184
- const pythonRange = options.pythonRange ?? env["PYFIG_PYTHON_RANGE"] ?? DEFAULT_PYTHON_RANGE;
185
- const pipRange = options.pipRange ?? env["PYFIG_PIP_RANGE"] ?? DEFAULT_PIP_RANGE;
137
+ const requiresPython = options.requiresPython ?? env["PYFIG_REQUIRES_PYTHON"] ?? DEFAULT_REQUIRES_PYTHON;
186
138
  const uvRange = options.uvRange ?? env["PYFIG_UV_RANGE"] ?? DEFAULT_UV_RANGE;
187
- assertRange("Python", pythonRange);
188
- assertRange("pip", pipRange);
139
+ assertPythonSpecifier(requiresPython);
189
140
  assertRange("uv", uvRange);
190
141
  const managedPython = options.managedPython ?? env["PYFIG_MANAGED_PYTHON"] ?? DEFAULT_MANAGED_PYTHON;
191
142
  const uvVersion = options.uvVersion ?? env["PYFIG_UV_VERSION"] ?? DEFAULT_UV_VERSION;
192
- assertManagedPythonRequest(managedPython, pythonRange);
143
+ assertManagedPythonRequest(managedPython, requiresPython);
193
144
  assertUvVersion(uvVersion, uvRange);
194
145
  const probeTimeoutMs = resolveDuration("probeTimeoutMs", options.probeTimeoutMs, env["PYFIG_PROBE_TIMEOUT_MS"], DEFAULT_PROBE_TIMEOUT_MS);
195
146
  const installTimeoutMs = resolveDuration("installTimeoutMs", options.installTimeoutMs, env["PYFIG_INSTALL_TIMEOUT_MS"], DEFAULT_INSTALL_TIMEOUT_MS);
196
147
  const downloadTimeoutMs = resolveDuration("downloadTimeoutMs", options.downloadTimeoutMs, env["PYFIG_DOWNLOAD_TIMEOUT_MS"], DEFAULT_DOWNLOAD_TIMEOUT_MS);
197
148
  const coordinationTimeoutMs = resolveDuration("coordinationTimeoutMs", options.coordinationTimeoutMs, env["PYFIG_COORDINATION_TIMEOUT_MS"], installTimeoutMs + downloadTimeoutMs + 60000);
198
- const cacheTtlMs = resolveDuration("cacheTtlMs", options.cacheTtlMs, env["PYFIG_CACHE_TTL_MS"], DEFAULT_CACHE_TTL_MS, true);
199
149
  const configuredHome = options.cacheDir ?? env["PYFIG_HOME"];
200
150
  const cacheDir = configuredHome ? import_node_path2.default.resolve(configuredHome) : envPathsFunction("pyfig", { suffix: "" }).cache;
201
151
  const configuredProjectRoot = options.projectRoot ?? env["PYFIG_PROJECT_ROOT"];
202
- const projectRoot = configuredProjectRoot ? import_node_path2.default.resolve(configuredProjectRoot) : resolveProjectRoot();
152
+ const projectRoot = configuredProjectRoot ? import_node_path2.default.resolve(configuredProjectRoot) : resolveProjectRoot(env["INIT_CWD"] ?? process.cwd());
203
153
  const autoInstall = options.autoInstall ?? parseBoolean(env["PYFIG_AUTO_INSTALL"], true);
204
154
  return {
205
155
  python: options.python ?? env["PYFIG_PYTHON"] ?? "",
206
- pythonRange,
207
- pipRange,
156
+ requiresPython,
208
157
  managedPython,
209
158
  uvVersion,
210
159
  uvRange,
@@ -220,40 +169,16 @@ function resolveOptions(options = {}) {
220
169
  installTimeoutMs,
221
170
  downloadTimeoutMs,
222
171
  coordinationTimeoutMs,
223
- cacheTtlMs,
224
172
  env
225
173
  };
226
174
  }
227
- function configurationFingerprint(options) {
228
- const policy = [
229
- normalizedPath(options.projectRoot),
230
- normalizedPath(options.cacheDir),
231
- options.python,
232
- options.pythonRange,
233
- options.pipRange,
234
- options.managedPython,
235
- options.uvVersion,
236
- options.uvRange,
237
- options.requireEmbedding,
238
- options.requireGil,
239
- options.autoInstall,
240
- options.installPython,
241
- options.installUv,
242
- options.createVirtualEnvironment,
243
- options.probeTimeoutMs,
244
- options.installTimeoutMs,
245
- options.downloadTimeoutMs,
246
- options.coordinationTimeoutMs,
247
- options.cacheTtlMs,
248
- environmentFingerprint(options.env)
249
- ];
250
- return import_node_crypto.createHash("sha256").update(JSON.stringify(policy)).digest("hex");
251
- }
252
175
  function uvEnvironment(options) {
253
176
  return {
254
177
  ...options.env,
255
178
  UV_PYTHON_INSTALL_DIR: import_node_path2.default.join(options.cacheDir, "python"),
256
- UV_CACHE_DIR: import_node_path2.default.join(options.cacheDir, "uv-cache")
179
+ UV_CACHE_DIR: import_node_path2.default.join(options.cacheDir, "uv-cache"),
180
+ UV_PYTHON_PREFERENCE: "system",
181
+ UV_NO_CONFIG: "1"
257
182
  };
258
183
  }
259
184
  function assertRange(tool, range) {
@@ -269,14 +194,19 @@ function assertUvVersion(version, range) {
269
194
  throw new TypeError(`Pinned uv ${version} does not satisfy supported range ${range}.`);
270
195
  }
271
196
  }
272
- function assertManagedPythonRequest(request, pythonRange) {
197
+ function assertPythonSpecifier(specifier) {
198
+ if (!isValidPythonSpecifier(specifier)) {
199
+ throw new TypeError(`Invalid requires-python PEP 440 specifier: ${specifier}`);
200
+ }
201
+ }
202
+ function assertManagedPythonRequest(request, requiresPython) {
273
203
  if (!request || /[\\/\0\r\n]/u.test(request) || request.includes("..")) {
274
204
  throw new TypeError(`Invalid managed Python request: ${request}`);
275
205
  }
276
206
  if (/^\d+\.\d+(?:\.\d+)?$/u.test(request)) {
277
207
  const normalized = request.split(".").length === 2 ? `${request}.0` : request;
278
- if (!import_semver.default.satisfies(normalized, pythonRange)) {
279
- throw new TypeError(`Managed Python ${request} cannot satisfy configured range ${pythonRange}.`);
208
+ if (!pythonVersionSatisfies(normalized, requiresPython)) {
209
+ throw new TypeError(`Managed Python ${request} cannot satisfy requires-python ${requiresPython}.`);
280
210
  }
281
211
  }
282
212
  }
@@ -297,34 +227,161 @@ function parseBoolean(value, fallback) {
297
227
  return false;
298
228
  throw new TypeError(`Invalid boolean value: ${value}`);
299
229
  }
300
- function environmentFingerprint(env) {
301
- const entries = Object.entries(env).filter((entry) => entry[1] !== undefined).map(([key, value]) => [
302
- process.platform === "win32" ? key.toUpperCase() : key,
303
- value
304
- ]).sort(([left], [right]) => left.localeCompare(right));
305
- return import_node_crypto.createHash("sha256").update(JSON.stringify(entries)).digest("hex");
230
+
231
+ // src/provision-lock.ts
232
+ var import_loqkit = require("loqkit");
233
+ async function withProvisioningLock(options) {
234
+ let existing;
235
+ const result = await import_loqkit.withLock({ resource: options.key }, options.mutate, {
236
+ key: options.key,
237
+ name: "Pyfig provisioning",
238
+ namespace: "pyfig",
239
+ timeout: options.timeoutMs,
240
+ predicate: async () => {
241
+ const value = await options.check();
242
+ if (value === null)
243
+ return true;
244
+ existing = { value };
245
+ return false;
246
+ }
247
+ });
248
+ if (result.context.state === "executed")
249
+ return result.value;
250
+ if (existing)
251
+ return existing.value;
252
+ throw new Error(`Provisioning check for ${options.key} did not produce a resource.`);
253
+ }
254
+
255
+ // src/virtual-environment.ts
256
+ var import_node_fs2 = __toESM(require("node:fs"), 1);
257
+ var import_node_path3 = __toESM(require("node:path"), 1);
258
+ var VIRTUAL_ENV_DIRECTORY_NAME = import_node_path3.default.join("node_modules", ".pyfig", "venv");
259
+ function virtualEnvironmentDirectory(projectRoot) {
260
+ return import_node_path3.default.join(projectRoot, VIRTUAL_ENV_DIRECTORY_NAME);
261
+ }
262
+ function externalVirtualEnvironmentDirectories(projectRoot, env) {
263
+ const candidates = [env["VIRTUAL_ENV"], import_node_path3.default.join(projectRoot, ".venv")].filter((value) => Boolean(value)).map((value) => import_node_path3.default.resolve(value));
264
+ return [...new Map(candidates.map((value) => [canonicalPath(value), value])).values()];
306
265
  }
307
- function normalizedPath(value) {
308
- const resolved = import_node_path2.default.resolve(value);
309
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
266
+ function virtualEnvironmentPythonExecutable(virtualEnvironment) {
267
+ return import_node_path3.default.join(virtualEnvironment, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
268
+ }
269
+ function activateVirtualEnvironment(env, virtualEnvironment, pythonExecutable) {
270
+ const activated = { ...env };
271
+ const pathEntry = Object.keys(activated).find((key) => key.toUpperCase() === "PATH");
272
+ const existingPath = pathEntry ? activated[pathEntry] : undefined;
273
+ for (const key of Object.keys(activated)) {
274
+ const normalized = key.toUpperCase();
275
+ if (normalized === "PATH" || normalized === "PYTHONHOME" || normalized === "VIRTUAL_ENV") {
276
+ delete activated[key];
277
+ }
278
+ }
279
+ activated["PATH"] = [import_node_path3.default.dirname(pythonExecutable), existingPath].filter((value) => Boolean(value)).join(import_node_path3.default.delimiter);
280
+ activated["VIRTUAL_ENV"] = virtualEnvironment;
281
+ return activated;
282
+ }
283
+ function pathsEqual(left, right) {
284
+ return canonicalPath(left) === canonicalPath(right);
285
+ }
286
+ function canonicalPath(value) {
287
+ const resolved = import_node_path3.default.resolve(value);
288
+ let canonical = resolved;
289
+ try {
290
+ canonical = import_node_fs2.default.realpathSync.native(resolved);
291
+ } catch {}
292
+ return process.platform === "win32" ? canonical.toLowerCase() : canonical;
310
293
  }
311
294
 
312
- // src/coordination.ts
313
- var import_node_crypto2 = require("node:crypto");
314
- var import_node_child_process2 = require("node:child_process");
315
- var import_promises = require("node:fs/promises");
316
- var import_node_path4 = __toESM(require("node:path"), 1);
317
- var workerThreads = __toESM(require("node:worker_threads"), 1);
318
- var import_proper_lockfile = __toESM(require("proper-lockfile"), 1);
295
+ // src/package-target.ts
296
+ var TARGET_OPTIONS = new Set([
297
+ "--active",
298
+ "--config-file",
299
+ "--directory",
300
+ "--isolated",
301
+ "--prefix",
302
+ "--project",
303
+ "--python",
304
+ "--root",
305
+ "--system",
306
+ "--target",
307
+ "--user"
308
+ ]);
309
+ var TARGET_ENVIRONMENT_VARIABLES = new Set([
310
+ "CONDA_PREFIX",
311
+ "PIP_PREFIX",
312
+ "PIP_PYTHON",
313
+ "PIP_ROOT",
314
+ "PIP_TARGET",
315
+ "PIP_USER",
316
+ "PYTHONHOME",
317
+ "PYTHONUSERBASE",
318
+ "UV_CONFIG_FILE",
319
+ "UV_PROJECT",
320
+ "UV_PROJECT_ENVIRONMENT",
321
+ "UV_PYTHON",
322
+ "UV_PREFIX",
323
+ "UV_ROOT",
324
+ "UV_SYSTEM_PYTHON",
325
+ "UV_TARGET"
326
+ ]);
327
+ function uvCommandNeedsEnvironment(args) {
328
+ return args[0] === "pip" || args[0] === "run";
329
+ }
330
+ function targetUvArguments(args, python) {
331
+ if (!python.virtualEnvironment) {
332
+ throw new Error("Targeted uv commands require a virtual environment.");
333
+ }
334
+ assertNoTargetOverride(args);
335
+ if (args[0] === "run") {
336
+ return ["run", "--no-project", "--python", python.executable, ...args.slice(1)];
337
+ }
338
+ if (args[0] === "pip" && args[1]) {
339
+ return ["pip", args[1], "--python", python.executable, ...args.slice(2)];
340
+ }
341
+ return [...args];
342
+ }
343
+ function uvCommandEnvironment(env, python) {
344
+ const sanitized = sanitizeUvEnvironment(env);
345
+ if (!python?.virtualEnvironment)
346
+ return sanitized;
347
+ return {
348
+ ...activateVirtualEnvironment(sanitized, python.virtualEnvironment, python.executable),
349
+ UV_PYTHON: python.executable
350
+ };
351
+ }
352
+ function sanitizeUvEnvironment(env) {
353
+ const sanitized = { ...env };
354
+ for (const key of Object.keys(sanitized)) {
355
+ if (TARGET_ENVIRONMENT_VARIABLES.has(key.toUpperCase()))
356
+ delete sanitized[key];
357
+ }
358
+ return sanitized;
359
+ }
360
+ function assertNoTargetOverride(args) {
361
+ for (const argument of args.slice(1)) {
362
+ if (argument === "--")
363
+ return;
364
+ const [name] = argument.split("=", 1);
365
+ if (name && TARGET_OPTIONS.has(name)) {
366
+ throw new Error(`${name} cannot override the environment selected by Pyfig.`);
367
+ }
368
+ if (argument === "-p" || argument.startsWith("-p=") || /^-p[^-]/u.test(argument)) {
369
+ throw new Error("-p cannot override the environment selected by Pyfig.");
370
+ }
371
+ }
372
+ }
373
+
374
+ // src/probes.ts
375
+ var import_semver2 = __toESM(require("semver"), 1);
319
376
 
320
377
  // src/process.ts
321
378
  var import_node_child_process = require("node:child_process");
322
- var import_node_path3 = __toESM(require("node:path"), 1);
379
+ var import_node_path4 = __toESM(require("node:path"), 1);
323
380
  var import_which = __toESM(require("which"), 1);
324
381
  async function resolveExecutable(value, env) {
325
382
  if (!value)
326
383
  return null;
327
- if (import_node_path3.default.isAbsolute(value)) {
384
+ if (import_node_path4.default.isAbsolute(value)) {
328
385
  return await import_which.default(value, { nothrow: true, path: env["PATH"], pathExt: env["PATHEXT"] });
329
386
  }
330
387
  if (!/^[A-Za-z0-9_.-]+$/u.test(value))
@@ -341,14 +398,15 @@ function run(executable, args, env, timeoutMs) {
341
398
  });
342
399
  if (result.status !== 0 || result.error) {
343
400
  const detail = result.stderr?.trim() || result.error?.message || `exit ${result.status}`;
344
- throw new Error(`${import_node_path3.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`);
401
+ throw new Error(`${import_node_path4.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`);
345
402
  }
346
403
  return { stdout: result.stdout, stderr: result.stderr };
347
404
  }
348
- async function runAsync(executable, args, env, timeoutMs, signal) {
405
+ async function runAsync(executable, args, env, timeoutMs, signal, cwd) {
349
406
  return new Promise((resolve, reject) => {
350
407
  const child = import_node_child_process.spawn(executable, args, {
351
408
  env,
409
+ cwd,
352
410
  stdio: ["ignore", "pipe", "pipe"],
353
411
  windowsHide: true
354
412
  });
@@ -369,11 +427,11 @@ async function runAsync(executable, args, env, timeoutMs, signal) {
369
427
  const abort = () => {
370
428
  child.kill();
371
429
  const reason = signal?.reason;
372
- finish(reason instanceof Error ? reason : new Error(`${import_node_path3.default.basename(executable)} was aborted.`));
430
+ finish(reason instanceof Error ? reason : new Error(`${import_node_path4.default.basename(executable)} was aborted.`));
373
431
  };
374
432
  const timer = setTimeout(() => {
375
433
  child.kill();
376
- finish(new Error(`${import_node_path3.default.basename(executable)} timed out after ${timeoutMs}ms.`));
434
+ finish(new Error(`${import_node_path4.default.basename(executable)} timed out after ${timeoutMs}ms.`));
377
435
  }, timeoutMs);
378
436
  child.stdout.setEncoding("utf8");
379
437
  child.stderr.setEncoding("utf8");
@@ -381,21 +439,21 @@ async function runAsync(executable, args, env, timeoutMs, signal) {
381
439
  stdout += chunk;
382
440
  if (stdout.length > 4 * 1024 * 1024) {
383
441
  child.kill();
384
- finish(new Error(`${import_node_path3.default.basename(executable)} exceeded the output limit.`));
442
+ finish(new Error(`${import_node_path4.default.basename(executable)} exceeded the output limit.`));
385
443
  }
386
444
  });
387
445
  child.stderr.on("data", (chunk) => {
388
446
  stderr += chunk;
389
447
  if (stderr.length > 4 * 1024 * 1024) {
390
448
  child.kill();
391
- finish(new Error(`${import_node_path3.default.basename(executable)} exceeded the error-output limit.`));
449
+ finish(new Error(`${import_node_path4.default.basename(executable)} exceeded the error-output limit.`));
392
450
  }
393
451
  });
394
452
  child.once("error", (error) => finish(error));
395
453
  child.once("close", (code, childSignal) => {
396
454
  if (code !== 0) {
397
455
  const detail = stderr.trim() || (childSignal ? `signal ${childSignal}` : `exit ${code}`);
398
- finish(new Error(`${import_node_path3.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`));
456
+ finish(new Error(`${import_node_path4.default.basename(executable)} ${args.slice(0, 3).join(" ")} failed: ${detail}`));
399
457
  return;
400
458
  }
401
459
  finish(undefined, { stdout, stderr });
@@ -407,213 +465,6 @@ async function runAsync(executable, args, env, timeoutMs, signal) {
407
465
  });
408
466
  }
409
467
 
410
- // src/coordination.ts
411
- var realmMutexTails = new Map;
412
- var flockLookups = new Map;
413
- var FLOCK_READY = "PYFIG_FLOCK_READY";
414
- var FLOCK_HOLDER = [
415
- `process.stdout.write('${FLOCK_READY}\\n')`,
416
- "process.stdin.resume()",
417
- 'process.stdin.once("end",()=>process.exit(0))'
418
- ].join(";");
419
- async function withProvisioningLock(options) {
420
- const existing = await options.check();
421
- if (existing !== null)
422
- return existing;
423
- return withResourceLock(options, async (signal) => {
424
- const rechecked = await options.check();
425
- return rechecked ?? options.mutate(signal);
426
- });
427
- }
428
- async function withResourceLock(options, operation) {
429
- await import_promises.mkdir(options.target, { recursive: true });
430
- return withRealmMutex(options.key, () => withWorkerLock(options, () => withNativeFlock(options, () => withPortableLease(options, operation))));
431
- }
432
- async function withRealmMutex(key, operation) {
433
- const previous = realmMutexTails.get(key) ?? Promise.resolve();
434
- let releaseGate;
435
- const gate = new Promise((resolve) => {
436
- releaseGate = resolve;
437
- });
438
- const tail = previous.catch(() => {
439
- return;
440
- }).then(() => gate);
441
- realmMutexTails.set(key, tail);
442
- await previous.catch(() => {
443
- return;
444
- });
445
- try {
446
- return await operation();
447
- } finally {
448
- releaseGate();
449
- if (realmMutexTails.get(key) === tail)
450
- realmMutexTails.delete(key);
451
- }
452
- }
453
- async function withWorkerLock(options, operation) {
454
- const manager = workerThreads.locks;
455
- if (!manager?.request)
456
- return operation();
457
- const controller = new AbortController;
458
- const timeout = setTimeout(() => controller.abort(new Error(`Timed out waiting for worker lock ${options.key}.`)), options.timeoutMs);
459
- let entered = false;
460
- try {
461
- return await manager.request(workerLockName(options.key), { mode: "exclusive", signal: controller.signal }, async () => {
462
- entered = true;
463
- return operation();
464
- });
465
- } catch (error) {
466
- if (entered)
467
- throw error;
468
- return operation();
469
- } finally {
470
- clearTimeout(timeout);
471
- }
472
- }
473
- async function withNativeFlock(options, operation) {
474
- if (process.platform === "win32")
475
- return operation();
476
- const executable = await findFlock(process.env);
477
- if (!executable)
478
- return operation();
479
- const child = await acquireFlock(executable, `${options.target}.flock`, options.timeoutMs);
480
- if (!child)
481
- return operation();
482
- try {
483
- return await operation();
484
- } finally {
485
- await releaseFlock(child);
486
- }
487
- }
488
- async function findFlock(env) {
489
- const lookupKey = `${env["PATH"] ?? ""}\x00${env["PATHEXT"] ?? ""}`;
490
- let lookup = flockLookups.get(lookupKey);
491
- if (!lookup) {
492
- lookup = resolveExecutable("flock", env);
493
- flockLookups.set(lookupKey, lookup);
494
- }
495
- return lookup;
496
- }
497
- async function acquireFlock(executable, lockPath, timeoutMs) {
498
- const waitSeconds = Math.max(1, Math.ceil(Math.min(timeoutMs, 30000) / 1000));
499
- const child = import_node_child_process2.spawn(executable, ["-x", "-w", String(waitSeconds), lockPath, process.execPath, "-e", FLOCK_HOLDER], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
500
- child.stderr.resume();
501
- let output = "";
502
- const acquired = await new Promise((resolve) => {
503
- let settled = false;
504
- const finish = (value) => {
505
- if (settled)
506
- return;
507
- settled = true;
508
- clearTimeout(timer);
509
- resolve(value);
510
- };
511
- const timer = setTimeout(() => finish(false), Math.min(timeoutMs, 30000) + 5000);
512
- child.once("error", () => finish(false));
513
- child.once("exit", () => finish(false));
514
- child.stdout.setEncoding("utf8");
515
- child.stdout.on("data", (chunk) => {
516
- output += chunk;
517
- if (output.includes(FLOCK_READY))
518
- finish(true);
519
- });
520
- });
521
- if (acquired)
522
- return child;
523
- child.kill();
524
- return null;
525
- }
526
- async function releaseFlock(child) {
527
- if (child.exitCode !== null)
528
- return;
529
- child.stdin.end();
530
- await new Promise((resolve) => {
531
- const timer = setTimeout(() => {
532
- child.kill();
533
- resolve();
534
- }, 5000);
535
- child.once("exit", () => {
536
- clearTimeout(timer);
537
- resolve();
538
- });
539
- });
540
- }
541
- async function withPortableLease(options, operation) {
542
- const controller = new AbortController;
543
- let compromised = null;
544
- const retryIntervalMs = 500;
545
- const release = await import_proper_lockfile.default.lock(options.target, {
546
- realpath: true,
547
- stale: 120000,
548
- update: 15000,
549
- retries: {
550
- retries: Math.ceil(options.timeoutMs / retryIntervalMs),
551
- factor: 1,
552
- minTimeout: retryIntervalMs,
553
- maxTimeout: retryIntervalMs
554
- },
555
- onCompromised: (error) => {
556
- compromised = error;
557
- controller.abort(error);
558
- }
559
- });
560
- try {
561
- const result = await operation(controller.signal);
562
- if (compromised)
563
- throw compromised;
564
- return result;
565
- } finally {
566
- controller.abort();
567
- await release();
568
- }
569
- }
570
- function workerLockName(key) {
571
- return `pyfig:${import_node_crypto2.createHash("sha256").update(key).digest("hex")}`;
572
- }
573
- function cacheLockTarget(cacheDirectory, name) {
574
- return import_node_path4.default.join(cacheDirectory, "locks", name);
575
- }
576
-
577
- // src/probes.ts
578
- var import_node_path6 = __toESM(require("node:path"), 1);
579
- var import_semver2 = __toESM(require("semver"), 1);
580
-
581
- // src/virtual-environment.ts
582
- var import_node_fs2 = __toESM(require("node:fs"), 1);
583
- var import_node_path5 = __toESM(require("node:path"), 1);
584
- var VIRTUAL_ENV_DIRECTORY_NAME = ".venv";
585
- function virtualEnvironmentDirectory(projectRoot) {
586
- return import_node_path5.default.join(projectRoot, VIRTUAL_ENV_DIRECTORY_NAME);
587
- }
588
- function virtualEnvironmentPythonExecutable(virtualEnvironment) {
589
- return import_node_path5.default.join(virtualEnvironment, process.platform === "win32" ? "Scripts" : "bin", process.platform === "win32" ? "python.exe" : "python");
590
- }
591
- function activateVirtualEnvironment(env, virtualEnvironment, pythonExecutable) {
592
- const activated = { ...env };
593
- const pathEntry = Object.keys(activated).find((key) => key.toUpperCase() === "PATH");
594
- const existingPath = pathEntry ? activated[pathEntry] : undefined;
595
- for (const key of Object.keys(activated)) {
596
- const normalized = key.toUpperCase();
597
- if (normalized === "PATH" || normalized === "PYTHONHOME" || normalized === "VIRTUAL_ENV") {
598
- delete activated[key];
599
- }
600
- }
601
- activated["PATH"] = [import_node_path5.default.dirname(pythonExecutable), existingPath].filter((value) => Boolean(value)).join(import_node_path5.default.delimiter);
602
- activated["VIRTUAL_ENV"] = virtualEnvironment;
603
- return activated;
604
- }
605
- function pathsEqual(left, right) {
606
- return canonicalPath(left) === canonicalPath(right);
607
- }
608
- function canonicalPath(value) {
609
- const resolved = import_node_path5.default.resolve(value);
610
- let canonical = resolved;
611
- try {
612
- canonical = import_node_fs2.default.realpathSync.native(resolved);
613
- } catch {}
614
- return process.platform === "win32" ? canonical.toLowerCase() : canonical;
615
- }
616
-
617
468
  // src/probes.ts
618
469
  var PYTHON_PROBE = `
619
470
  import json
@@ -663,7 +514,15 @@ if os.path.isfile(header_path) and library_path:
663
514
  }
664
515
 
665
516
  release_level = sys.version_info.releaselevel
666
- release_suffix = "" if release_level == "final" else f"-{release_level}.{sys.version_info.serial}"
517
+ release_suffixes = {
518
+ "alpha": "a",
519
+ "beta": "b",
520
+ "candidate": "rc",
521
+ "final": "",
522
+ }
523
+ release_suffix = release_suffixes[release_level]
524
+ if release_suffix:
525
+ release_suffix += str(sys.version_info.serial)
667
526
 
668
527
  print(json.dumps({
669
528
  "executable": sys.executable,
@@ -683,24 +542,8 @@ print(json.dumps({
683
542
  "embedding": embedding,
684
543
  }))
685
544
  `;
686
- async function findPython(options) {
687
- const candidates = [];
688
- if (options.python)
689
- candidates.push({ executable: options.python, args: [] });
690
- if (process.platform === "win32") {
691
- candidates.push({ executable: "python", args: [] }, { executable: "python3", args: [] }, { executable: "py", args: ["-3"] });
692
- } else {
693
- candidates.push(...["python3.15", "python3.14", "python3.13", "python3.12", "python3.11", "python3.10"].map((executable) => ({ executable, args: [] })), { executable: "python3", args: [] }, { executable: "python", args: [] });
694
- }
695
- for (const candidate of candidates) {
696
- const info = await probePython(candidate, options);
697
- if (info && pythonSatisfiesRequirements(info, options))
698
- return info;
699
- }
700
- return null;
701
- }
702
- function pythonSatisfiesRequirements(python, options) {
703
- return import_semver2.default.satisfies(python.version, options.pythonRange) && (!options.requireEmbedding || python.implementation === "cpython" && python.embedding !== null && pythonArchitectureMatchesRuntime(python)) && (!options.requireGil || !python.gilDisabled);
545
+ function pythonSatisfiesPolicy(python, options) {
546
+ return pythonVersionSatisfies(python.version, options.requiresPython) && (!options.requireEmbedding || python.implementation === "cpython" && python.embedding !== null && pythonArchitectureMatchesRuntime(python)) && (!options.requireGil || !python.gilDisabled);
704
547
  }
705
548
  async function probePython(candidate, options) {
706
549
  const executable = await resolveExecutable(candidate.executable, options.env);
@@ -709,55 +552,11 @@ async function probePython(candidate, options) {
709
552
  try {
710
553
  const { stdout } = run(executable, [...candidate.args, "-c", PYTHON_PROBE], options.env, options.probeTimeoutMs);
711
554
  const info = JSON.parse(stdout.trim());
712
- return import_semver2.default.valid(info.version) ? info : null;
713
- } catch {
714
- return null;
715
- }
716
- }
717
- async function findPip(python, options) {
718
- if (!python.virtualEnvironment) {
719
- throw new Error("pip discovery requires a virtual-environment Python interpreter.");
720
- }
721
- const activeEnv = activateVirtualEnvironment(options.env, python.virtualEnvironment, python.executable);
722
- try {
723
- const { stdout } = run(python.executable, ["-m", "pip", "--version"], activeEnv, options.probeTimeoutMs);
724
- const details = parsePipVersionOutput(stdout);
725
- const version = normalizeVersion(details?.version);
726
- if (!version || !import_semver2.default.satisfies(version, options.pipRange))
727
- return null;
728
- if (!details || !pipBelongsToPython(details, python))
729
- return null;
730
- return {
731
- kind: "pip",
732
- executable: python.executable,
733
- prefixArgs: ["-m", "pip"],
734
- version,
735
- pythonExecutable: python.executable,
736
- virtualEnvironment: python.virtualEnvironment
737
- };
555
+ return isValidPythonVersion(info.version) ? info : null;
738
556
  } catch {
739
557
  return null;
740
558
  }
741
559
  }
742
- function parsePipVersionOutput(output) {
743
- const match = output.trim().match(/^pip\s+(\S+)\s+from\s+(.+?)\s+\(python\s+(\d+)\.(\d+)(?:\.\d+)?\)$/iu);
744
- if (!match?.[1] || !match[2] || !match[3] || !match[4])
745
- return null;
746
- return {
747
- version: match[1],
748
- location: match[2],
749
- pythonMajor: Number.parseInt(match[3], 10),
750
- pythonMinor: Number.parseInt(match[4], 10)
751
- };
752
- }
753
- function pipBelongsToPython(pip, python) {
754
- if (pip.pythonMajor !== python.major || pip.pythonMinor !== python.minor)
755
- return false;
756
- const prefix = normalizePathForComparison(import_node_path6.default.resolve(python.prefix));
757
- const location = normalizePathForComparison(import_node_path6.default.resolve(pip.location));
758
- const relative = import_node_path6.default.relative(prefix, location);
759
- return relative === "" || relative !== ".." && !relative.startsWith(`..${import_node_path6.default.sep}`) && !import_node_path6.default.isAbsolute(relative);
760
- }
761
560
  async function findUv(options) {
762
561
  const configured = options.env["PYFIG_UV"] ?? "uv";
763
562
  const executable = await resolveExecutable(configured, options.env);
@@ -778,9 +577,6 @@ function normalizeVersion(value) {
778
577
  return null;
779
578
  return import_semver2.default.valid(value) ?? import_semver2.default.coerce(value)?.version ?? null;
780
579
  }
781
- function normalizePathForComparison(value) {
782
- return process.platform === "win32" ? value.toLowerCase() : value;
783
- }
784
580
  function pythonArchitectureMatchesRuntime(python) {
785
581
  const architecture = python.architecture.toLowerCase();
786
582
  if (python.pointerBits !== (process.arch === "ia32" ? 32 : 64))
@@ -795,228 +591,223 @@ function pythonArchitectureMatchesRuntime(python) {
795
591
  return architecture === process.arch;
796
592
  }
797
593
 
798
- // src/state.ts
799
- var completed = new Map;
800
- var inFlight = new Map;
801
- var cacheGeneration = 0;
802
- async function memoizeAsync(namespace, key, ttlMs, validate, load) {
803
- const cacheKey = `${namespace}\x00${key}`;
804
- const cached = completed.get(cacheKey);
805
- if (cached && cached.expiresAt >= Date.now()) {
806
- try {
807
- if (await validate(cached.value))
808
- return cached.value;
809
- } catch {}
810
- completed.delete(cacheKey);
811
- }
812
- const pending = inFlight.get(cacheKey);
813
- if (pending)
814
- return pending;
815
- const generation = cacheGeneration;
816
- const loading = load().then((value) => {
817
- if (ttlMs > 0 && generation === cacheGeneration) {
818
- completed.set(cacheKey, { expiresAt: Date.now() + ttlMs, value });
819
- }
820
- return value;
821
- }).finally(() => {
822
- if (inFlight.get(cacheKey) === loading)
823
- inFlight.delete(cacheKey);
824
- });
825
- inFlight.set(cacheKey, loading);
826
- return loading;
827
- }
828
- function memoizeSync(namespace, key, ttlMs, validate, load) {
829
- const cacheKey = `${namespace}\x00${key}`;
830
- const cached = completed.get(cacheKey);
831
- if (cached && cached.expiresAt >= Date.now()) {
832
- try {
833
- if (validate(cached.value))
834
- return cached.value;
835
- } catch {}
836
- completed.delete(cacheKey);
837
- }
838
- const value = load();
839
- if (ttlMs > 0)
840
- completed.set(cacheKey, { expiresAt: Date.now() + ttlMs, value });
841
- return value;
842
- }
843
- function clearPyfigCaches() {
844
- cacheGeneration += 1;
845
- completed.clear();
846
- inFlight.clear();
847
- }
848
-
849
594
  // src/provision.ts
850
- async function ensurePython(options = {}) {
851
- const resolved = resolveOptions(options);
852
- return memoizeAsync("python", configurationFingerprint(resolved), resolved.cacheTtlMs, pythonIsPresent, () => ensurePythonResolved(resolved));
853
- }
854
- async function ensureVirtualEnvironment(options = {}) {
855
- const resolved = resolveOptions(options);
856
- return memoizeAsync("venv", configurationFingerprint(resolved), resolved.cacheTtlMs, (python) => pythonIsPresent(python) && Boolean(python.virtualEnvironment) && pathsEqual(python.virtualEnvironment, virtualEnvironmentDirectory(resolved.projectRoot)), () => ensureVirtualEnvironmentResolved(resolved));
857
- }
858
595
  async function ensureUv(options = {}) {
859
- const resolved = resolveOptions(options);
860
- return memoizeAsync("uv", configurationFingerprint(resolved), resolved.cacheTtlMs, (uv) => import_node_fs4.default.existsSync(uv.executable), () => ensureUvResolved(resolved));
596
+ return ensureUvResolved(resolveOptions(options));
861
597
  }
862
- async function ensureEnvironment(options = {}) {
598
+ async function ensureEnvironment(options = {}, dependencies = [], dependencyOptions = {}) {
863
599
  const resolved = resolveOptions(options);
864
- return memoizeAsync("environment", configurationFingerprint(resolved), resolved.cacheTtlMs, environmentIsPresent, async () => {
865
- const python = await ensureVirtualEnvironmentResolved(resolved);
866
- const packageManager = await ensurePipResolved(python, resolved);
867
- return { python, packageManager };
868
- });
600
+ const normalized = normalizeDependencies(dependencies);
601
+ const selections = normalizeDependencyOptions(dependencyOptions);
602
+ const uv = await ensureUvResolved(resolved);
603
+ const python = await ensureVirtualEnvironmentResolved(resolved, uv);
604
+ if (normalized.length > 0 || selections.extras.length > 0 || selections.groups.length > 0 || selections.allExtras) {
605
+ await installRuntimeDependencies(uv, python, normalized, selections, resolved);
606
+ }
607
+ return {
608
+ python,
609
+ uv,
610
+ managed: isManagedEnvironment(python, resolved)
611
+ };
869
612
  }
870
- async function ensureVirtualEnvironmentResolved(options) {
613
+ async function ensureVirtualEnvironmentResolved(options, uv) {
614
+ const existing = await findReusableEnvironment(options);
615
+ if (existing)
616
+ return existing;
871
617
  const directory = virtualEnvironmentDirectory(options.projectRoot);
872
618
  const check = async () => {
873
- const existing = await inspectVirtualEnvironment(directory, options);
874
- if (!existing)
875
- return null;
876
- if (pythonSatisfiesRequirements(existing, options) && await virtualEnvironmentMatchesConfiguredPython(existing, options)) {
877
- return existing;
878
- }
879
- throw incompatibleVirtualEnvironmentError(directory, existing, options);
619
+ const candidate = await inspectVirtualEnvironment(directory, options);
620
+ return candidate && pythonSatisfiesPolicy(candidate, options) && await virtualEnvironmentMatchesConfiguredPython(candidate, options) ? candidate : null;
880
621
  };
881
- const existing = await check();
882
- if (existing)
883
- return existing;
884
622
  if (!options.createVirtualEnvironment) {
885
- throw new Error(`No virtual environment at ${directory} satisfies the Python requirements; ` + "virtual-environment creation is disabled.");
623
+ throw new Error(`No compatible environment is available and creation is disabled. ` + `Pyfig would create ${directory}.`);
886
624
  }
887
- const basePython = await ensurePythonResolved(options);
888
- const preferredUv = await findAvailableUv(options) ?? (options.installUv ? await ensureUvResolved(options) : null);
625
+ const basePython = await ensureBasePythonResolved(uv, options);
889
626
  return withProvisioningLock({
890
627
  key: `venv:${canonicalPath(directory)}`,
891
- target: directory,
892
628
  timeoutMs: options.coordinationTimeoutMs,
893
- env: options.env,
894
629
  check,
895
- mutate: async (signal) => {
896
- const entries = await import_promises2.readdir(directory);
897
- const hasVenvConfiguration = import_node_fs4.default.existsSync(import_node_path7.default.join(directory, "pyvenv.cfg"));
898
- if (entries.length > 0 && !hasVenvConfiguration) {
899
- throw new Error(`Refusing to replace non-virtual-environment contents at ${directory}.`);
900
- }
901
- await createVirtualEnvironment(directory, basePython, preferredUv, options, signal);
902
- const installed = await inspectVirtualEnvironment(directory, options);
903
- if (!installed || !pythonSatisfiesRequirements(installed, options)) {
904
- throw new Error(`Python created ${directory}, but it does not satisfy the requested runtime capabilities.`);
630
+ mutate: async () => {
631
+ await import_promises.rm(directory, { recursive: true, force: true });
632
+ await import_promises.mkdir(import_node_path5.default.dirname(directory), { recursive: true });
633
+ await runAsync(uv.executable, [
634
+ "venv",
635
+ "--no-project",
636
+ "--no-config",
637
+ "--no-python-downloads",
638
+ "--python",
639
+ basePython.executable,
640
+ directory
641
+ ], isolatedUvEnvironment(options), options.installTimeoutMs);
642
+ const installed = await check();
643
+ if (!installed) {
644
+ throw new Error(`uv created ${directory}, but its Python does not satisfy the requested capabilities.`);
905
645
  }
906
646
  return installed;
907
647
  }
908
648
  });
909
649
  }
910
- async function createVirtualEnvironment(directory, basePython, preferredUv, options, signal) {
911
- if (preferredUv) {
912
- try {
913
- await createVirtualEnvironmentWithUv(preferredUv, directory, basePython, options, signal);
914
- return;
915
- } catch {}
650
+ async function findReusableEnvironment(options) {
651
+ if (options.python) {
652
+ const configured = await probePython({ executable: options.python, args: [] }, options);
653
+ if (!configured || !pythonSatisfiesPolicy(configured, options)) {
654
+ throw incompatibleConfiguredPython(options);
655
+ }
656
+ if (configured.virtualEnvironment)
657
+ return configured;
916
658
  }
917
- try {
918
- await runAsync(basePython.executable, ["-m", "venv", directory], withoutActiveVirtualEnvironment(options.env), options.installTimeoutMs, signal);
919
- return;
920
- } catch (systemVenvError) {
921
- if (preferredUv || !options.installUv)
922
- throw systemVenvError;
659
+ for (const directory of [
660
+ ...externalVirtualEnvironmentDirectories(options.projectRoot, options.env),
661
+ virtualEnvironmentDirectory(options.projectRoot)
662
+ ]) {
663
+ const info = await inspectVirtualEnvironment(directory, options);
664
+ if (info && pythonSatisfiesPolicy(info, options) && await virtualEnvironmentMatchesConfiguredPython(info, options)) {
665
+ return info;
666
+ }
923
667
  }
924
- throw new Error("Python could not create a virtual environment and uv installation is disabled.");
925
- }
926
- async function createVirtualEnvironmentWithUv(uv, directory, basePython, options, signal) {
927
- await runAsync(uv.executable, [
928
- "venv",
929
- "--no-project",
930
- "--no-config",
931
- "--no-python-downloads",
932
- "--allow-existing",
933
- "--python",
934
- basePython.executable,
935
- directory
936
- ], uvEnvironment(options), options.installTimeoutMs, signal);
937
- }
938
- async function inspectVirtualEnvironment(directory, options) {
939
- const executable = virtualEnvironmentPythonExecutable(directory);
940
- const info = await probePython({ executable, args: [] }, options);
941
- return info?.virtualEnvironment && pathsEqual(info.virtualEnvironment, directory) ? info : null;
668
+ return null;
942
669
  }
943
- async function ensurePythonResolved(options) {
944
- const existing = await discoverPython(options);
945
- if (existing)
946
- return existing;
670
+ async function ensureBasePythonResolved(uv, options) {
671
+ if (options.python) {
672
+ const configured = await probePython({ executable: options.python, args: [] }, options);
673
+ if (!configured || !pythonSatisfiesPolicy(configured, options)) {
674
+ throw incompatibleConfiguredPython(options);
675
+ }
676
+ if (!configured.virtualEnvironment)
677
+ return configured;
678
+ const base = await probePython({ executable: configured.baseExecutable, args: [] }, options);
679
+ if (base && pythonSatisfiesPolicy(base, options))
680
+ return base;
681
+ throw incompatibleConfiguredPython(options);
682
+ }
683
+ const system = await findPythonWithUv(uv, options, "system");
684
+ if (system)
685
+ return system;
686
+ const cachedManaged = await findPythonWithUv(uv, options, "managed");
687
+ if (cachedManaged)
688
+ return cachedManaged;
947
689
  if (!options.installPython) {
948
- const capability = options.requireEmbedding ? " with compatible CPython development headers, architecture, and a linkable library" : "";
949
- throw new Error(`No Python interpreter satisfies ${options.pythonRange}${capability}; ` + "managed Python installation is disabled.");
690
+ throw new Error(missingPythonMessage(options, "managed Python installation is disabled"));
950
691
  }
951
- const lockTarget = cacheLockTarget(options.cacheDir, "python");
952
692
  return withProvisioningLock({
953
- key: `python:${canonicalPath(lockTarget)}`,
954
- target: lockTarget,
693
+ key: `python:${canonicalPath(import_node_path5.default.join(options.cacheDir, "python"))}`,
955
694
  timeoutMs: options.coordinationTimeoutMs,
956
- env: options.env,
957
- check: () => discoverPython(options),
958
- mutate: async (signal) => {
959
- const uv = await ensureUvResolved(options);
960
- const uvEnv = uvEnvironment(options);
961
- await runAsync(uv.executable, ["python", "install", "--no-config", options.managedPython], uvEnv, options.installTimeoutMs, signal);
962
- const installed = await findPythonWithUv(uv, options, signal);
695
+ check: () => findPythonWithUv(uv, options, "managed"),
696
+ mutate: async () => {
697
+ await runAsync(uv.executable, ["python", "install", "--no-config", options.managedPython], isolatedUvEnvironment(options), options.installTimeoutMs);
698
+ const installed = await findPythonWithUv(uv, options, "managed");
963
699
  if (!installed) {
964
- throw new Error(`uv installed Python ${options.managedPython}, but it does not satisfy the requested ` + "runtime capabilities.");
700
+ throw new Error(missingPythonMessage(options, `uv installed ${options.managedPython}, but it remains incompatible`));
965
701
  }
966
702
  return installed;
967
703
  }
968
704
  });
969
705
  }
970
- async function discoverPython(options) {
971
- if (options.python) {
972
- const configured = await probePython({ executable: options.python, args: [] }, options);
973
- if (configured && pythonSatisfiesRequirements(configured, options))
974
- return configured;
975
- throw new Error(`Configured Python ${options.python} is unavailable or does not satisfy ${options.pythonRange}.`);
976
- }
977
- const existingUv = await findAvailableUv(options);
978
- if (existingUv) {
979
- const uvPython = await findPythonWithUv(existingUv, options);
980
- if (uvPython)
981
- return uvPython;
982
- }
983
- return findPython({ ...options, python: "" });
984
- }
985
- async function findPythonWithUv(uv, options, signal) {
986
- const requests = [undefined, options.managedPython];
987
- for (const requestedPython of requests) {
706
+ async function findPythonWithUv(uv, options, source) {
707
+ for (const request of ["", options.managedPython]) {
988
708
  const args = [
989
709
  "python",
990
710
  "find",
991
711
  "--no-project",
992
712
  "--no-config",
993
- "--managed-python",
994
- "--no-python-downloads"
713
+ "--no-python-downloads",
714
+ source === "system" ? "--system" : "--managed-python"
995
715
  ];
996
- if (requestedPython)
997
- args.push(requestedPython);
716
+ if (request)
717
+ args.push(request);
998
718
  try {
999
- const { stdout } = await runAsync(uv.executable, args, uvEnvironment(options), options.probeTimeoutMs, signal);
719
+ const { stdout } = await runAsync(uv.executable, args, explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
1000
720
  const info = await probePython({ executable: stdout.trim(), args: [] }, options);
1001
- if (info && pythonSatisfiesRequirements(info, options))
721
+ if (info && pythonSatisfiesPolicy(info, options))
1002
722
  return info;
1003
- } catch (error) {
1004
- if (signal?.aborted)
1005
- throw signal.reason ?? error;
1006
- }
723
+ } catch {}
1007
724
  }
725
+ try {
726
+ const { stdout } = await runAsync(uv.executable, [
727
+ "python",
728
+ "list",
729
+ "--no-config",
730
+ "--no-python-downloads",
731
+ "--only-installed",
732
+ "--output-format",
733
+ "json",
734
+ source === "system" ? "--no-managed-python" : "--managed-python"
735
+ ], explicitPythonSourceEnvironment(options), options.probeTimeoutMs);
736
+ const candidates = installedPythonPaths(stdout);
737
+ for (const executable of candidates) {
738
+ const info = await probePython({ executable, args: [] }, options);
739
+ if (info && !info.virtualEnvironment && pythonSatisfiesPolicy(info, options)) {
740
+ return info;
741
+ }
742
+ }
743
+ } catch {}
1008
744
  return null;
1009
745
  }
1010
- async function ensurePipResolved(python, options) {
1011
- const existingUv = await findAvailableUv(options);
1012
- if (existingUv)
1013
- return uvPackageManager(existingUv, python);
1014
- if (options.installUv)
1015
- return uvPackageManager(await ensureUvResolved(options), python);
1016
- const existingPip = await findPip(python, options);
1017
- if (existingPip)
1018
- return existingPip;
1019
- throw new Error(`The virtual environment's python -m pip does not satisfy ${options.pipRange}; ` + "uv installation is disabled.");
746
+ function installedPythonPaths(output) {
747
+ const parsed = JSON.parse(output);
748
+ if (!Array.isArray(parsed))
749
+ return [];
750
+ const paths = parsed.flatMap((entry) => {
751
+ if (!entry || typeof entry !== "object" || !("path" in entry))
752
+ return [];
753
+ const candidate = entry.path;
754
+ return typeof candidate === "string" && candidate ? [candidate] : [];
755
+ });
756
+ return [...new Map(paths.map((candidate) => [canonicalPath(candidate), candidate])).values()];
757
+ }
758
+ async function inspectVirtualEnvironment(directory, options) {
759
+ const executable = virtualEnvironmentPythonExecutable(directory);
760
+ const info = await probePython({ executable, args: [] }, options);
761
+ return info?.virtualEnvironment && pathsEqual(info.virtualEnvironment, directory) ? info : null;
762
+ }
763
+ async function installRuntimeDependencies(uv, python, dependencies, dependencyOptions, options) {
764
+ if (!python.virtualEnvironment) {
765
+ throw new Error("Runtime dependency installation requires a virtual environment.");
766
+ }
767
+ await withProvisioningLock({
768
+ key: `dependencies:${canonicalPath(python.virtualEnvironment)}`,
769
+ timeoutMs: options.coordinationTimeoutMs,
770
+ check: async () => null,
771
+ mutate: async () => {
772
+ const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-dependencies-"));
773
+ try {
774
+ const additions = import_node_path5.default.join(temporary, "runtime-dependencies.txt");
775
+ const resolved = import_node_path5.default.join(temporary, "resolved-dependencies.txt");
776
+ const pyproject = import_node_path5.default.join(options.projectRoot, "pyproject.toml");
777
+ const hasPyproject = import_node_fs4.default.existsSync(pyproject);
778
+ if (!hasPyproject && (dependencyOptions.extras.length > 0 || dependencyOptions.groups.length > 0 || dependencyOptions.allExtras)) {
779
+ throw new Error("Runtime extras and groups require an existing pyproject.toml.");
780
+ }
781
+ const sources = hasPyproject ? [pyproject] : [];
782
+ if (dependencies.length > 0) {
783
+ await import_promises.writeFile(additions, `${dependencies.join(`
784
+ `)}
785
+ `, "utf8");
786
+ sources.push(additions);
787
+ }
788
+ const selectionArgs = [
789
+ ...dependencyOptions.extras.flatMap((extra) => ["--extra", extra]),
790
+ ...dependencyOptions.groups.flatMap((group) => ["--group", group]),
791
+ ...dependencyOptions.allExtras ? ["--all-extras"] : []
792
+ ];
793
+ await runAsync(uv.executable, [
794
+ "pip",
795
+ "compile",
796
+ "--no-config",
797
+ "--python",
798
+ python.executable,
799
+ ...selectionArgs,
800
+ ...sources,
801
+ "--output-file",
802
+ resolved
803
+ ], targetUvEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
804
+ await runAsync(uv.executable, ["pip", "install", "--no-config", "--python", python.executable, "-r", resolved], targetUvEnvironment(options, python), options.installTimeoutMs, undefined, options.projectRoot);
805
+ } finally {
806
+ await import_promises.rm(temporary, { recursive: true, force: true });
807
+ }
808
+ return true;
809
+ }
810
+ });
1020
811
  }
1021
812
  async function ensureUvResolved(options) {
1022
813
  const existing = await findAvailableUv(options);
@@ -1027,15 +818,12 @@ async function ensureUvResolved(options) {
1027
818
  }
1028
819
  const installDir = uvInstallDirectory(options);
1029
820
  const executable = uvExecutable(options);
1030
- const lockTarget = cacheLockTarget(options.cacheDir, "uv");
1031
821
  return withProvisioningLock({
1032
- key: `uv:${canonicalPath(lockTarget)}`,
1033
- target: lockTarget,
822
+ key: `uv:${canonicalPath(installDir)}`,
1034
823
  timeoutMs: options.coordinationTimeoutMs,
1035
- env: options.env,
1036
824
  check: () => findAvailableUv(options),
1037
- mutate: async (signal) => {
1038
- await installUv(installDir, options, signal);
825
+ mutate: async () => {
826
+ await installUv(installDir, options);
1039
827
  const result = probeUv(executable, options);
1040
828
  if (!result || result.version !== options.uvVersion) {
1041
829
  throw new Error(`Expected managed uv ${options.uvVersion}, but the installed executable did not match.`);
@@ -1045,88 +833,134 @@ async function ensureUvResolved(options) {
1045
833
  });
1046
834
  }
1047
835
  async function findAvailableUv(options) {
1048
- const system = await findUv(options);
1049
- if (system)
1050
- return system;
836
+ const external = await findUv(options);
837
+ if (external)
838
+ return external;
1051
839
  const managed = probeUv(uvExecutable(options), options);
1052
840
  return managed?.version === options.uvVersion ? managed : null;
1053
841
  }
1054
- function uvInstallDirectory(options) {
1055
- return import_node_path7.default.join(options.cacheDir, "tools", "uv", options.uvVersion);
1056
- }
1057
- function uvExecutable(options) {
1058
- return import_node_path7.default.join(uvInstallDirectory(options), process.platform === "win32" ? "uv.exe" : "uv");
842
+ function normalizeDependencies(dependencies) {
843
+ const normalized = dependencies.map((dependency) => dependency.trim());
844
+ for (const dependency of normalized) {
845
+ if (!dependency || /[\0\r\n]/u.test(dependency)) {
846
+ throw new TypeError(`Invalid runtime dependency: ${JSON.stringify(dependency)}`);
847
+ }
848
+ }
849
+ return [...new Set(normalized)];
1059
850
  }
1060
- function uvPackageManager(uv, python) {
1061
- if (!python.virtualEnvironment) {
1062
- throw new Error("uv pip commands require a virtual-environment Python interpreter.");
851
+ function normalizeDependencyOptions(options) {
852
+ const extras = normalizeNames("extra", options.extras ?? []);
853
+ const groups = normalizeNames("dependency group", options.groups ?? []);
854
+ if (options.allExtras && extras.length > 0) {
855
+ throw new TypeError("allExtras cannot be combined with explicit extras.");
1063
856
  }
1064
- return {
1065
- kind: "uv",
1066
- executable: uv.executable,
1067
- prefixArgs: ["pip"],
1068
- version: uv.version,
1069
- pythonExecutable: python.executable,
1070
- virtualEnvironment: python.virtualEnvironment
1071
- };
857
+ return { extras, groups, allExtras: options.allExtras ?? false };
1072
858
  }
1073
- function withoutActiveVirtualEnvironment(env) {
1074
- const result = { ...env };
1075
- for (const key of Object.keys(result)) {
1076
- const normalized = key.toUpperCase();
1077
- if (normalized === "CONDA_PREFIX" || normalized === "VIRTUAL_ENV" || normalized === "PYTHONHOME") {
1078
- delete result[key];
859
+ function normalizeNames(kind, names) {
860
+ const normalized = names.map((name) => name.trim());
861
+ for (const name of normalized) {
862
+ if (!name || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(name)) {
863
+ throw new TypeError(`Invalid ${kind}: ${JSON.stringify(name)}`);
1079
864
  }
1080
865
  }
1081
- return result;
866
+ return [...new Set(normalized)];
867
+ }
868
+ function isManagedEnvironment(python, options) {
869
+ return Boolean(python.virtualEnvironment) && pathsEqual(python.virtualEnvironment, virtualEnvironmentDirectory(options.projectRoot));
1082
870
  }
1083
- async function installUv(installDir, options, signal) {
871
+ function isolatedUvEnvironment(options) {
872
+ return sanitizeUvEnvironment(uvEnvironment(options));
873
+ }
874
+ function explicitPythonSourceEnvironment(options) {
875
+ const env = isolatedUvEnvironment(options);
876
+ delete env["UV_PYTHON_PREFERENCE"];
877
+ return env;
878
+ }
879
+ function targetUvEnvironment(options, python) {
880
+ return uvCommandEnvironment(uvEnvironment(options), python);
881
+ }
882
+ function incompatibleConfiguredPython(options) {
883
+ return new Error(`Configured Python ${options.python} is unavailable or does not satisfy ` + `requires-python ${options.requiresPython}` + `${options.requireEmbedding ? " with CPython embedding files" : ""}.`);
884
+ }
885
+ function missingPythonMessage(options, reason) {
886
+ const embedding = options.requireEmbedding ? " with compatible CPython headers, library, architecture, and GIL ABI" : "";
887
+ return `No Python satisfies requires-python ${options.requiresPython}${embedding}; ${reason}.`;
888
+ }
889
+ async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, options) {
890
+ if (!options.python)
891
+ return true;
892
+ const configured = await probePython({ executable: options.python, args: [] }, options);
893
+ if (!configured || !pythonSatisfiesPolicy(configured, options))
894
+ return false;
895
+ if (configured.virtualEnvironment && pathsEqual(configured.executable, virtualEnvironment.executable)) {
896
+ return true;
897
+ }
898
+ const configuredBase = configured.virtualEnvironment ? configured.baseExecutable : configured.executable;
899
+ return pathsEqual(configuredBase, virtualEnvironment.baseExecutable);
900
+ }
901
+ function uvInstallDirectory(options) {
902
+ return import_node_path5.default.join(options.cacheDir, "tools", "uv", options.uvVersion);
903
+ }
904
+ function uvExecutable(options) {
905
+ return import_node_path5.default.join(uvInstallDirectory(options), process.platform === "win32" ? "uv.exe" : "uv");
906
+ }
907
+ async function installUv(installDir, options) {
1084
908
  const asset = uvAsset();
1085
909
  const baseUrl = `https://github.com/astral-sh/uv/releases/download/${options.uvVersion}`;
1086
- const temporary = await import_promises2.mkdtemp(import_node_path7.default.join(import_node_os.default.tmpdir(), "pyfig-uv-"));
910
+ const temporary = await import_promises.mkdtemp(import_node_path5.default.join(import_node_os.default.tmpdir(), "pyfig-uv-"));
1087
911
  const dispatcher = proxyDispatcher(options.env);
1088
912
  try {
1089
- const archive = import_node_path7.default.join(temporary, asset);
1090
- await download(`${baseUrl}/${asset}`, archive, options.downloadTimeoutMs, signal, dispatcher);
1091
- const expected = (await downloadText(`${baseUrl}/${asset}.sha256`, options.downloadTimeoutMs, signal, dispatcher)).trim().split(/\s+/u)[0];
913
+ const archive = import_node_path5.default.join(temporary, asset);
914
+ await download(`${baseUrl}/${asset}`, archive, options.downloadTimeoutMs, dispatcher);
915
+ const expected = (await downloadText(`${baseUrl}/${asset}.sha256`, options.downloadTimeoutMs, dispatcher)).trim().split(/\s+/u)[0];
1092
916
  const actual = await sha256File(archive);
1093
917
  if (!expected || expected.toLowerCase() !== actual.toLowerCase()) {
1094
918
  throw new Error(`uv archive checksum mismatch for ${asset}`);
1095
919
  }
1096
- const extracted = import_node_path7.default.join(temporary, "extracted");
1097
- await import_promises2.mkdir(extracted, { recursive: true });
1098
- signal.throwIfAborted();
1099
- if (asset.endsWith(".zip"))
1100
- await import_extract_zip.default(archive, { dir: extracted });
1101
- else
1102
- await tar.x({ file: archive, cwd: extracted, strict: true });
1103
- signal.throwIfAborted();
1104
- const source = await findFile(extracted, process.platform === "win32" ? "uv.exe" : "uv");
920
+ const extracted = import_node_path5.default.join(temporary, "extracted");
921
+ await import_promises.mkdir(extracted, { recursive: true });
922
+ const source = await extractUvExecutable(archive, asset, extracted);
1105
923
  if (!source)
1106
924
  throw new Error(`uv archive did not contain the expected executable: ${asset}`);
1107
925
  await publishUvAtomically(source, installDir, options);
1108
926
  } finally {
1109
927
  await dispatcher?.close();
1110
- await import_promises2.rm(temporary, { recursive: true, force: true });
928
+ await import_promises.rm(temporary, { recursive: true, force: true });
929
+ }
930
+ }
931
+ async function extractUvExecutable(archive, asset, destination) {
932
+ const executableName = process.platform === "win32" ? "uv.exe" : "uv";
933
+ if (!asset.endsWith(".zip")) {
934
+ await tar.x({ file: archive, cwd: destination, strict: true });
935
+ return findFile(destination, executableName);
1111
936
  }
937
+ const entries = import_fflate.unzipSync(await import_promises.readFile(archive), {
938
+ filter: ({ name }) => import_node_path5.default.posix.basename(name.replaceAll("\\", "/")) === executableName
939
+ });
940
+ const matches = Object.entries(entries).filter(([name]) => import_node_path5.default.posix.basename(name.replaceAll("\\", "/")) === executableName);
941
+ if (matches.length !== 1 || !matches[0])
942
+ return null;
943
+ const source = import_node_path5.default.join(destination, executableName);
944
+ await import_promises.writeFile(source, matches[0][1], { flag: "wx" });
945
+ return source;
1112
946
  }
1113
947
  async function publishUvAtomically(source, installDir, options) {
1114
- await import_promises2.mkdir(installDir, { recursive: true });
948
+ await import_promises.mkdir(installDir, { recursive: true });
1115
949
  const executable = uvExecutable(options);
1116
- const staged = import_node_path7.default.join(installDir, `.${import_node_path7.default.basename(executable)}.${process.pid}.${import_node_crypto3.randomUUID()}.tmp`);
950
+ const staged = import_node_path5.default.join(installDir, `.${import_node_path5.default.basename(executable)}.${process.pid}.${import_node_crypto.randomUUID()}.tmp`);
1117
951
  try {
1118
- await import_promises2.copyFile(source, staged);
952
+ await import_promises.copyFile(source, staged);
1119
953
  if (process.platform !== "win32")
1120
- await import_promises2.chmod(staged, 493);
954
+ await import_promises.chmod(staged, 493);
1121
955
  const stagedUv = probeUv(staged, options);
1122
956
  if (!stagedUv || stagedUv.version !== options.uvVersion) {
1123
957
  throw new Error(`Downloaded uv executable is not version ${options.uvVersion}.`);
1124
958
  }
1125
959
  if (process.platform === "win32")
1126
- await import_promises2.rm(executable, { force: true });
1127
- await import_promises2.rename(staged, executable);
960
+ await import_promises.rm(executable, { force: true });
961
+ await import_promises.rename(staged, executable);
1128
962
  } finally {
1129
- await import_promises2.rm(staged, { force: true });
963
+ await import_promises.rm(staged, { force: true });
1130
964
  }
1131
965
  }
1132
966
  function uvAsset() {
@@ -1141,23 +975,23 @@ function uvAsset() {
1141
975
  return `uv-${architecture}-unknown-linux-musl.tar.gz`;
1142
976
  throw new Error(`uv bootstrap does not support platform ${process.platform}.`);
1143
977
  }
1144
- async function download(url, destination, timeoutMs, externalSignal, dispatcher) {
978
+ async function download(url, destination, timeoutMs, dispatcher) {
1145
979
  const response = await import_undici.fetch(url, {
1146
980
  dispatcher,
1147
981
  redirect: "follow",
1148
- signal: combinedSignal(timeoutMs, externalSignal)
982
+ signal: AbortSignal.timeout(timeoutMs)
1149
983
  });
1150
984
  if (!response.ok || !response.body) {
1151
985
  await response.body?.cancel();
1152
986
  throw new Error(`Could not download ${url}: HTTP ${response.status}`);
1153
987
  }
1154
- await import_promises3.pipeline(import_node_stream.Readable.fromWeb(response.body), import_node_fs3.createWriteStream(destination, { flags: "wx" }));
988
+ await import_promises2.pipeline(import_node_stream.Readable.fromWeb(response.body), import_node_fs3.createWriteStream(destination, { flags: "wx" }));
1155
989
  }
1156
- async function downloadText(url, timeoutMs, externalSignal, dispatcher) {
990
+ async function downloadText(url, timeoutMs, dispatcher) {
1157
991
  const response = await import_undici.fetch(url, {
1158
992
  dispatcher,
1159
993
  redirect: "follow",
1160
- signal: combinedSignal(timeoutMs, externalSignal)
994
+ signal: AbortSignal.timeout(timeoutMs)
1161
995
  });
1162
996
  if (!response.ok) {
1163
997
  await response.body?.cancel();
@@ -1179,18 +1013,15 @@ function proxyDispatcher(env) {
1179
1013
  function environmentValue(env, lowercase, uppercase) {
1180
1014
  return env[lowercase] ?? env[uppercase];
1181
1015
  }
1182
- function combinedSignal(timeoutMs, externalSignal) {
1183
- return AbortSignal.any([externalSignal, AbortSignal.timeout(timeoutMs)]);
1184
- }
1185
1016
  async function sha256File(file) {
1186
- const hash = import_node_crypto3.createHash("sha256");
1017
+ const hash = import_node_crypto.createHash("sha256");
1187
1018
  for await (const chunk of import_node_fs3.createReadStream(file))
1188
1019
  hash.update(chunk);
1189
1020
  return hash.digest("hex");
1190
1021
  }
1191
1022
  async function findFile(directory, name) {
1192
- for (const entry of await import_promises2.readdir(directory, { withFileTypes: true })) {
1193
- const target = import_node_path7.default.join(directory, entry.name);
1023
+ for (const entry of await import_promises.readdir(directory, { withFileTypes: true })) {
1024
+ const target = import_node_path5.default.join(directory, entry.name);
1194
1025
  if (entry.isFile() && entry.name === name)
1195
1026
  return target;
1196
1027
  if (entry.isDirectory()) {
@@ -1201,36 +1032,11 @@ async function findFile(directory, name) {
1201
1032
  }
1202
1033
  return null;
1203
1034
  }
1204
- function incompatibleVirtualEnvironmentError(directory, python, options) {
1205
- const capabilities = options.requireEmbedding ? " or lacks compatible CPython embedding headers, library, or architecture" : "";
1206
- return new Error(`The existing virtual environment at ${directory} uses Python ${python.version}, ` + `which does not satisfy ${options.pythonRange}${capabilities}.`);
1207
- }
1208
- async function virtualEnvironmentMatchesConfiguredPython(virtualEnvironment, options) {
1209
- if (!options.python)
1210
- return true;
1211
- const configured = await probePython({ executable: options.python, args: [] }, options);
1212
- if (!configured || !pythonSatisfiesRequirements(configured, options))
1213
- return false;
1214
- if (configured.virtualEnvironment && pathsEqual(configured.executable, virtualEnvironment.executable)) {
1215
- return true;
1216
- }
1217
- const configuredBase = configured.virtualEnvironment ? configured.baseExecutable : configured.executable;
1218
- return pathsEqual(configuredBase, virtualEnvironment.baseExecutable);
1219
- }
1220
- function pythonIsPresent(python) {
1221
- return import_node_fs4.default.existsSync(python.executable);
1222
- }
1223
- function environmentIsPresent(environment) {
1224
- return pythonIsPresent(environment.python) && import_node_fs4.default.existsSync(environment.packageManager.executable);
1225
- }
1226
1035
 
1227
1036
  // src/sync-resolver.ts
1228
1037
  async function main() {
1229
1038
  const request = JSON.parse(import_node_fs5.readFileSync(0, "utf8"));
1230
- const result = request.target === "python" ? await ensurePython(request.options) : request.target === "venv" ? await ensureVirtualEnvironment(request.options) : request.target === "uv" ? await ensureUv(request.options) : request.target === "uv-pip" ? {
1231
- uv: await ensureUv(request.options),
1232
- python: await ensureVirtualEnvironment(request.options)
1233
- } : await ensureEnvironment(request.options);
1039
+ const result = request.target === "uv" ? await ensureUv(request.options) : await ensureEnvironment(request.options);
1234
1040
  process.stdout.write(JSON.stringify(result));
1235
1041
  }
1236
1042
  main().catch((error) => {
@@ -1239,4 +1045,4 @@ main().catch((error) => {
1239
1045
  process.exitCode = 1;
1240
1046
  });
1241
1047
 
1242
- //# debugId=37511558FADA2E3664756E2164756E21
1048
+ //# debugId=8D6E1E9C1BFA227064756E2164756E21