pyfig 0.6.3 → 0.7.1

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 (75) hide show
  1. package/README.md +77 -166
  2. package/SECURITY.md +16 -5
  3. package/dist/cjs/index.js +471 -980
  4. package/dist/cjs/index.js.map +6 -15
  5. package/dist/index.d.ts +2 -9
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -5
  8. package/dist/index.js.map +1 -1
  9. package/dist/process.d.ts +6 -12
  10. package/dist/process.d.ts.map +1 -1
  11. package/dist/process.js +35 -54
  12. package/dist/process.js.map +1 -1
  13. package/dist/pyfig.d.ts +31 -0
  14. package/dist/pyfig.d.ts.map +1 -0
  15. package/dist/pyfig.js +275 -0
  16. package/dist/pyfig.js.map +1 -0
  17. package/dist/uv.d.ts +3 -0
  18. package/dist/uv.d.ts.map +1 -0
  19. package/dist/uv.js +423 -0
  20. package/dist/uv.js.map +1 -0
  21. package/package.json +6 -12
  22. package/dist/cjs/sync-resolver.js +0 -1217
  23. package/dist/cjs/sync-resolver.js.map +0 -19
  24. package/dist/cli.d.ts +0 -3
  25. package/dist/cli.d.ts.map +0 -1
  26. package/dist/cli.js +0 -48
  27. package/dist/cli.js.map +0 -1
  28. package/dist/client.d.ts +0 -15
  29. package/dist/client.d.ts.map +0 -1
  30. package/dist/client.js +0 -25
  31. package/dist/client.js.map +0 -1
  32. package/dist/config.d.ts +0 -62
  33. package/dist/config.d.ts.map +0 -1
  34. package/dist/config.js +0 -112
  35. package/dist/config.js.map +0 -1
  36. package/dist/package-target.d.ts +0 -8
  37. package/dist/package-target.d.ts.map +0 -1
  38. package/dist/package-target.js +0 -81
  39. package/dist/package-target.js.map +0 -1
  40. package/dist/pep440.d.ts +0 -12
  41. package/dist/pep440.d.ts.map +0 -1
  42. package/dist/pep440.js +0 -21
  43. package/dist/pep440.js.map +0 -1
  44. package/dist/probes.d.ts +0 -37
  45. package/dist/probes.d.ts.map +0 -1
  46. package/dist/probes.js +0 -145
  47. package/dist/probes.js.map +0 -1
  48. package/dist/project-root.d.ts +0 -3
  49. package/dist/project-root.d.ts.map +0 -1
  50. package/dist/project-root.js +0 -22
  51. package/dist/project-root.js.map +0 -1
  52. package/dist/provision-lock.d.ts +0 -10
  53. package/dist/provision-lock.d.ts.map +0 -1
  54. package/dist/provision-lock.js +0 -24
  55. package/dist/provision-lock.js.map +0 -1
  56. package/dist/provision.d.ts +0 -26
  57. package/dist/provision.d.ts.map +0 -1
  58. package/dist/provision.js +0 -685
  59. package/dist/provision.js.map +0 -1
  60. package/dist/runners.d.ts +0 -14
  61. package/dist/runners.d.ts.map +0 -1
  62. package/dist/runners.js +0 -71
  63. package/dist/runners.js.map +0 -1
  64. package/dist/runtime-files.d.ts +0 -3
  65. package/dist/runtime-files.d.ts.map +0 -1
  66. package/dist/runtime-files.js +0 -12
  67. package/dist/runtime-files.js.map +0 -1
  68. package/dist/sync-resolver.d.ts +0 -2
  69. package/dist/sync-resolver.d.ts.map +0 -1
  70. package/dist/sync-resolver.js +0 -15
  71. package/dist/sync-resolver.js.map +0 -1
  72. package/dist/virtual-environment.d.ts +0 -11
  73. package/dist/virtual-environment.d.ts.map +0 -1
  74. package/dist/virtual-environment.js +0 -49
  75. package/dist/virtual-environment.js.map +0 -1
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # pyfig
2
2
 
3
- `pyfig` prepares uv and a compatible Python virtual environment from Bun or Node.js. It prefers a valid uv already on the system, reuses a compatible venv, and searches system Python with downloads disabled before considering any managed Python download.
4
-
5
- Pyfig never invokes standalone `pip`, never writes `pyproject.toml` or requirements files, and never creates a project-root `.venv`. When no compatible venv exists, its private environment is `<project-root>/node_modules/.pyfig/venv`.
3
+ `pyfig` resolves compatible uv and Python executables from Node.js or Bun and
4
+ exposes one guarded `uv run` spawn method. Its only managed state is a uv tool
5
+ cache; uv retains responsibility for Python, environments, and dependencies.
6
6
 
7
7
  ## Install
8
8
 
@@ -10,193 +10,100 @@ Pyfig never invokes standalone `pip`, never writes `pyproject.toml` or requireme
10
10
  bun add pyfig
11
11
  ```
12
12
 
13
+ Pyfig reuses a compatible uv from `PATH` or its OS cache. When neither exists,
14
+ it installs the greatest official uv release allowed by the requested version.
15
+ Python discovery and managed installation are delegated to
16
+ [`uv python find`](https://docs.astral.sh/uv/reference/cli/#uv-python-find).
17
+
13
18
  ## Use
14
19
 
15
20
  ```ts
16
- import { createPyfig } from 'pyfig';
21
+ import { pyfig } from 'pyfig';
17
22
 
18
- const pyfig = createPyfig({
19
- requiresPython: '>=3.11,<4',
20
- requiresUv: '>=0.10,<1',
23
+ const python = await pyfig({
24
+ uv: '>=0.10,<1',
25
+ python: '>=3.11,<4',
21
26
  requireEmbedding: true,
22
27
  requireGil: true,
23
28
  });
24
29
 
25
- const environment = await pyfig.prepare(
26
- ['requests>=2.32'],
27
- { extras: ['database'] },
28
- );
29
-
30
- console.log(environment.uv);
31
- console.log(environment.python);
32
- console.log(environment.managed);
33
-
34
- await pyfig.uv(
35
- ['run', '--', 'python', 'script.py'],
36
- { stdio: 'inherit' },
37
- );
38
- ```
39
-
40
- `prepare()` accepts PEP 508 dependency specifiers plus optional `extras`, `groups`, or `allExtras`. If the project has a `pyproject.toml`, uv resolves that read-only input together with a temporary dependency file. Pyfig installs the resolved result with `uv pip install -r`, which preserves unrelated packages. Temporary inputs are created outside the repository and removed afterward.
41
-
42
- The same uv-only surface handles direct commands:
30
+ console.log(python.uv);
31
+ console.log(python.python);
43
32
 
44
- ```ts
45
- import { uvSpawn, uvSpawnSync } from 'pyfig';
46
-
47
- await uvSpawn(['pip', 'list'], { stdio: 'inherit' });
48
-
49
- const result = uvSpawnSync(
50
- ['run', '--', 'python', '-c', 'import sys; print(sys.executable)'],
51
- { encoding: 'utf8' },
52
- );
33
+ python.run('python', '--version', { stdio: 'inherit' });
53
34
  ```
54
35
 
55
- For `uv pip` and `uv run`, Pyfig selects the venv and injects its exact interpreter. Caller-supplied target overrides such as `--python`, `--system`, `--target`, `--prefix`, and `--user` are rejected. Inherited target-changing environment variables are removed. Other uv commands run with Pyfig's cache and system-Python preference but are otherwise unchanged.
56
-
57
- The async spawn helper returns `Promise<ChildProcess>` because uv or environment setup may happen before spawning. The synchronous helper performs setup through `process.execPath`, so a Bun process remains Bun and does not assume a `node` command exists.
58
-
59
- ## Selection policy
60
-
61
- Pyfig uses this order:
62
-
63
- 1. A compatible configured or `PATH` uv.
64
- 2. Pyfig's cached, checksum-verified uv.
65
- 3. A compatible explicitly configured venv, active `VIRTUAL_ENV`, root `.venv`, or Pyfig-owned venv.
66
- 4. Every installed system Python reported by uv, with downloads disabled, until one passes all capability checks.
67
- 5. An already-cached uv-managed Python that passes the same checks.
68
- 6. A managed Python download, only when installation policy allows it.
36
+ `pyfig()` returns a `PythonContext` containing:
69
37
 
70
- An explicit `python` option is strict: an unavailable or incompatible interpreter is an error. Incompatible external venvs are preserved and skipped. Only an incompatible environment under `node_modules/.pyfig/venv` may be rebuilt.
71
-
72
- ![Pyfig prefers existing uv, compatible venvs, and system Python before managed downloads](docs/diagrams/python-selection.png)
73
-
74
- [Mermaid source](docs/diagrams/python-selection.mmd)
75
-
76
- Native consumers can set `requireEmbedding: true` to require CPython, `Python.h`, a linkable embedding library, and architecture/pointer-width compatibility with the JavaScript runtime. Set `requireGil: true` when the native addon requires the standard GIL-enabled ABI. These checks apply to system, existing-venv, cached, and downloaded interpreters alike.
38
+ | Field | Meaning |
39
+ | --- | --- |
40
+ | `uv` | Absolute path to the resolved uv executable. |
41
+ | `python` | Absolute path to the resolved Python interpreter. |
42
+ | `run(...args)` | Spawn `uv run` with that Python interpreter. |
77
43
 
78
- ## Runtime dependency resolution
44
+ `run()` treats a final object as Node `SpawnOptions`. When no `--` argument is
45
+ present, every string is treated as command argv:
79
46
 
80
47
  ```ts
81
- await pyfig.prepare(
82
- ['orjson>=3.10', 'httpx[http2]'],
83
- {
84
- extras: ['postgres'],
85
- groups: ['runtime'],
86
- },
87
- );
48
+ python.run('python', '--version');
49
+ // uv run --python /resolved/python -- python --version
88
50
  ```
89
51
 
90
- Resolution follows the equivalent of:
52
+ When `--` is present, strings before it are passed to uv and strings after it
53
+ are command argv:
91
54
 
92
- ```sh
93
- uv pip compile pyproject.toml /temporary/runtime-dependencies.txt \
94
- --extra postgres --group runtime \
95
- --python /selected/venv/python \
96
- --output-file /temporary/resolved-dependencies.txt
97
-
98
- uv pip install \
99
- --python /selected/venv/python \
100
- -r /temporary/resolved-dependencies.txt
55
+ ```ts
56
+ python.run('--no-project', '--with', 'httpx', '--', 'python', 'script.py', {
57
+ cwd: '/workspace',
58
+ stdio: 'inherit',
59
+ });
101
60
  ```
102
61
 
103
- When no `pyproject.toml` exists, only the temporary dependency input is compiled. Extras and dependency groups require an existing `pyproject.toml`. Pyfig does not use `uv add` or `uv pip sync`: the former would edit project metadata, while the latter could remove unrelated packages from the venv.
104
-
105
- ![Runtime dependencies are resolved in temporary files before installation](docs/diagrams/runtime-dependencies.png)
106
-
107
- [Mermaid source](docs/diagrams/runtime-dependencies.mmd)
108
-
109
- ## Project root
110
-
111
- Pyfig does not interpret `package.json`, workspace metadata, `pyproject.toml`, or lockfiles to choose ownership. Root precedence is:
112
-
113
- 1. `projectRoot`
114
- 2. `PYFIG_PROJECT_ROOT`
115
- 3. nearest Git worktree marker above `INIT_CWD`, when set, otherwise the current directory
116
- 4. that starting directory when no Git marker exists
117
-
118
- Set `projectRoot` explicitly in reusable libraries and unusual workspace layouts.
62
+ Pyfig inserts `--python <python>` immediately before the separator. Caller
63
+ supplied `--python <value>` and `--python=<value>` arguments are rejected before
64
+ `--`; Python is selected only by `pyfig()`. A `--python` appearing after
65
+ `--` belongs to the command and is left untouched.
119
66
 
120
- ## Public API
67
+ `pyfig()` accepts:
121
68
 
122
- | Export | Responsibility |
123
- | --- | --- |
124
- | `createPyfig(options)` | Create a scoped client with memoized base preparation. |
125
- | `client.prepare(dependencies?, options?)` | Select a venv and optionally resolve/install runtime dependencies. |
126
- | `client.pythonInfo()` | Return the selected interpreter and embedding metadata. |
127
- | `client.uvInfo()` | Return the selected uv executable and version. |
128
- | `client.uv()` / `client.uvSync()` | Spawn uv and pin pip/run commands to the selected venv. |
129
- | `ensureEnvironment(options?, dependencies?, dependencyOptions?)` | Standalone preparation API. |
130
- | `ensureUv(options?)` | Prefer existing uv, otherwise bootstrap verified uv when allowed. |
131
- | `uvSpawn()` / `uvSpawnSync()` | Standalone uv spawn API. |
132
- | `resolveProjectRoot(start?)` | Resolve the nearest Git root without reading manifests. |
133
-
134
- The package exposes matching ESM and CommonJS entry points. Probe, download, lock, and command-policy internals are not exported.
135
-
136
- ## Configuration
137
-
138
- Precedence is API or CLI option, environment variable, then default.
139
-
140
- `requiresPython` and `requiresUv` use PEP 440 specifiers. Comma-separated clauses, compatible releases (`~=`), exclusions (`!=`), wildcard equality, arbitrary equality (`===`), and pre-release versions are supported. For example: `requiresPython: '>=3.10,!=3.12.*,<4'` and `requiresUv: '==0.13.*'`. Pyfig reuses a compatible installed or cached uv first. When installation is necessary, it resolves the greatest published uv release satisfying `requiresUv` and containing the required platform assets.
141
-
142
- When embedding files are available, `PythonInfo.embedding.stableAbiLibraryPath` identifies the platform's Python 3 Stable ABI import library when the installation provides one. Consumers can use it to build a single Limited API binary for multiple CPython 3 minor versions while retaining `libraryPath` for version-specific embedding.
143
-
144
- | API option | Environment variable | Default |
69
+ | Option | Default | Meaning |
145
70
  | --- | --- | --- |
146
- | `python` | `PYFIG_PYTHON` | uv discovery |
147
- | `requiresPython` | `PYFIG_REQUIRES_PYTHON` | `>=3.10,<4` |
148
- | `managedPython` | `PYFIG_MANAGED_PYTHON` | `3.12` |
149
- | `requiresUv` | `PYFIG_REQUIRES_UV` | `>=0.10,<1` |
150
- | `cacheDir` | `PYFIG_HOME` | OS cache directory |
151
- | `projectRoot` | `PYFIG_PROJECT_ROOT` | `INIT_CWD`, Git root, or current directory |
152
- | `requireEmbedding` | `PYFIG_REQUIRE_EMBEDDING` | `false` |
153
- | `requireGil` | `PYFIG_REQUIRE_GIL` | `false` |
154
- | `autoInstall` | `PYFIG_AUTO_INSTALL` | `true` |
155
- | `installPython` | `PYFIG_INSTALL_PYTHON` | `autoInstall` |
156
- | `installUv` | `PYFIG_INSTALL_UV` | `autoInstall` |
157
- | `createVirtualEnvironment` | `PYFIG_CREATE_VIRTUAL_ENVIRONMENT` | `autoInstall` |
158
- | `probeTimeoutMs` | `PYFIG_PROBE_TIMEOUT_MS` | `10000` |
159
- | `installTimeoutMs` | `PYFIG_INSTALL_TIMEOUT_MS` | `600000` |
160
- | `downloadTimeoutMs` | `PYFIG_DOWNLOAD_TIMEOUT_MS` | `300000` |
161
- | `coordinationTimeoutMs` | `PYFIG_COORDINATION_TIMEOUT_MS` | install + download + 60 seconds |
162
-
163
- Set `PYFIG_UV` to choose an explicit uv executable. Set `PYFIG_AUTO_INSTALL=0` or use the CLI's `--check` flag on hosts where downloads and environment creation are prohibited.
164
-
165
- ## CLI
166
-
167
- ```sh
168
- bunx --bun pyfig
169
- bunx --bun pyfig --check
170
- bunx --bun pyfig --requires-python '>=3.12,<3.14'
171
- bunx --bun pyfig --requires-uv '>=0.12,<1'
172
- bunx --bun pyfig --require-embedding --require-gil
173
- bunx --bun pyfig --project-root /path/to/repository
174
- bunx --bun pyfig --json
175
- ```
176
-
177
- `bunx --bun` runs the CLI with Bun and does not require a separate Node.js executable.
178
-
179
- ## Concurrency and installation safety
180
-
181
- Pyfig resolves a compatible uv release from Astral's GitHub releases, downloads that exact archive over HTTPS, verifies its adjacent SHA-256 checksum, validates the staged executable version, and atomically publishes it in Pyfig's cache. A compatible cached executable is probed locally without a network request. Release metadata is cached on disk for 24 hours; stale metadata is refreshed under a Loqkit lock and remains available as a fallback when GitHub's API is temporarily unavailable. Set `GITHUB_TOKEN` or `GH_TOKEN` to authenticate release discovery on CI and other rate-limited hosts. Pyfig does not execute installer scripts or modify shell profiles.
71
+ | `uv` | `>=0.10,<1` | uv executable or PEP 440 version constraint. |
72
+ | `python` | system, then managed | Python executable or PEP 440 version constraint. |
73
+ | `requireEmbedding` | `false` | Require CPython headers, a linkable library, and matching architecture. |
74
+ | `requireGil` | `false` | Reject free-threaded Python builds. |
75
+ | `cwd` | `process.cwd()` | Directory used for uv configuration and project discovery. |
76
+ | `env` | `process.env` | Environment used to locate executables and invoke child processes. |
77
+
78
+ For both `uv` and `python`, Pyfig first checks whether the value resolves to an
79
+ executable. An executable wins over version parsing. An explicit uv executable
80
+ is trusted without a version check or setup. An explicit Python executable skips
81
+ version selection and installation, but it must still be a working Python and
82
+ satisfy requested embedding, architecture, and GIL capabilities. A value that
83
+ is not executable must be a valid PEP 440 specifier.
84
+
85
+ For a Python version specifier, Pyfig first asks uv for a matching system
86
+ interpreter. If it is absent or fails the requested capabilities, Pyfig asks uv
87
+ for a managed interpreter. For a uv version specifier, Pyfig checks `PATH`, then
88
+ its cache, then downloads a matching checksum-verified release. Cached
89
+ executables are used without a metadata or network request. Release metadata is
90
+ cached for 24 hours and refreshed under Loqkit's layered check-lock-check flow.
91
+ The cache lives below `%LOCALAPPDATA%\pyfig` on Windows,
92
+ `~/Library/Caches/pyfig` on macOS, and `${XDG_CACHE_HOME:-~/.cache}/pyfig` on
93
+ Linux.
94
+
95
+ Executable paths can be used directly:
182
96
 
183
- Provisioning mutations use Loqkit's layered in-memory, worker, native-file-lock-when-available, and portable-file coordination with capability checks before and after acquisition. Compatible managed uv executables are cached by resolved version; Loqkit coordinates both the `requiresUv` policy lookup and exact release publication. uv supplies its own target-environment lock for installs. Base client preparation is memoized; dependency preparation is intentionally rerun so uv can cheaply verify current inputs through its shared cache.
184
-
185
- ## Platform support
186
-
187
- Pyfig bootstraps uv on x64 and arm64 Windows, macOS, and Linux. Linux uses Astral's static musl artifact, which also runs on glibc. Node.js 22.11+ on the 22.x line, Node.js 24.10 through 26.x, and Bun 1.2+ are supported. Other JavaScript runtimes may work when they provide compatible Node built-ins and process semantics.
188
-
189
- ## Publishing
190
-
191
- Pyfig publishes from a `v<package-version>` Git tag through `.github/workflows/publish.yml`. The trusted-publishing workflow validates the tag, rejects an existing registry version, builds and audits one archive, and publishes that archive with provenance.
192
-
193
- From a clean, synchronized `main` branch:
194
-
195
- ```sh
196
- bun run release
97
+ ```ts
98
+ const python = await pyfig({
99
+ uv: '/opt/bin/uv',
100
+ python: '/opt/python/bin/python3',
101
+ requireEmbedding: true,
102
+ });
197
103
  ```
198
104
 
199
- The local gate derives the next version from remote GitHub tags, verifies locally, audits the package, commits, tags, and atomically pushes. Use `bun run release -- minor`, `bun run release -- major`, or `bun run release -- --dry-run` as needed. Tag publishing intentionally does not repeat the local test suite.
105
+ The runtime package exports `pyfig` and `PythonContext`. TypeScript also exposes
106
+ `PyfigOptions`. Matching ESM and CommonJS entry points are published.
200
107
 
201
108
  ## Development
202
109
 
@@ -204,7 +111,11 @@ The local gate derives the next version from remote GitHub tags, verifies locall
204
111
  bun install --ignore-scripts
205
112
  bun run verify
206
113
  bun run build
207
- bun pm pack --dry-run
208
114
  ```
209
115
 
210
- MIT licensed.
116
+ ## Publishing
117
+
118
+ From a clean, synchronized `main` branch, `bun run release` verifies locally,
119
+ builds and audits the package, commits the next version, creates its Git tag,
120
+ and pushes both atomically. The tag workflow publishes that exact package to
121
+ npm through trusted publishing.
package/SECURITY.md CHANGED
@@ -1,9 +1,20 @@
1
1
  # Security
2
2
 
3
- Pyfig can download Astral uv release archives and can ask uv to download Python distributions. Automatic installation is enabled by default; set `autoInstall: false`, pass `--check`, or set `PYFIG_AUTO_INSTALL=0` when downloads are not allowed.
3
+ Pyfig spawns only the uv executable resolved from `uv`, found on `PATH`, or
4
+ installed in its OS cache. Setup downloads an official Astral release archive
5
+ and adjacent SHA-256 checksum over HTTPS, verifies the archive before
6
+ publication, and probes the staged executable. It never runs an installer
7
+ script, invokes a shell by default, or modifies shell and project configuration.
4
8
 
5
- Pyfig resolves a uv release allowed by `requiresUv`, then fetches that exact release URL over HTTPS with bounded redirects and timeouts. GitHub release metadata is cached for 24 hours under Loqkit coordination, with validated stale metadata available if refresh fails. Pyfig verifies the adjacent SHA-256 checksum, validates the staged executable's reported version, and atomically publishes it into its cache. It never executes an installer script or modifies shell profiles or the host `PATH`. Proxy variables are honored through a per-request dispatcher without logging proxy credentials.
9
+ Compatible cached uv executables are used without network access. GitHub release
10
+ metadata is cached for 24 hours, with valid stale metadata retained as a network
11
+ fallback. Cache publication and metadata refresh use Loqkit's in-memory,
12
+ worker, native-flock-when-available, and portable file-lock coordination.
6
13
 
7
- Provisioning uses in-process, worker, optional native advisory, and portable filesystem coordination followed by a post-lock capability check. Runtime dependency mutation uses uv's target-environment lock. Pyfig pins uv commands to the selected venv and removes configuration values that could redirect them. It reads an existing `pyproject.toml` only as a resolver input and never writes project manifests or requirements files.
8
-
9
- Executable paths, project roots, version specifiers, package specifications, indexes, and proxy settings remain trusted configuration. Do not let an untrusted user choose `python`, `PYFIG_UV`, `cacheDir`, package arguments, or related environment variables in a privileged process. A checksum fetched from the same release protects transfer integrity, not a compromised upstream publisher.
14
+ The executable path, working directory, environment, version requests, uv
15
+ arguments, command arguments, and spawn options are trusted caller input. The
16
+ `run()` method can execute arbitrary commands, and callers can explicitly
17
+ enable shell execution through `SpawnOptions`. uv owns Python discovery and
18
+ managed-Python downloads. Supply an explicit uv executable or pre-populate the
19
+ cache when setup downloads are prohibited, and configure uv itself when Python
20
+ downloads must be offline or restricted by organizational policy.