pyfig 0.1.0 → 0.3.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 (66) hide show
  1. package/README.md +154 -17
  2. package/SECURITY.md +5 -3
  3. package/dist/cjs/index.js +1504 -0
  4. package/dist/cjs/index.js.map +21 -0
  5. package/dist/cjs/package.json +3 -0
  6. package/dist/cjs/sync-resolver.js +1242 -0
  7. package/dist/cjs/sync-resolver.js.map +18 -0
  8. package/dist/cli.js +17 -6
  9. package/dist/cli.js.map +1 -1
  10. package/dist/client.d.ts +20 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +23 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/config.d.ts +45 -2
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +130 -9
  17. package/dist/config.js.map +1 -1
  18. package/dist/coordination.d.ts +25 -0
  19. package/dist/coordination.d.ts.map +1 -0
  20. package/dist/coordination.js +187 -0
  21. package/dist/coordination.js.map +1 -0
  22. package/dist/index.d.ts +9 -5
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +6 -3
  25. package/dist/index.js.map +1 -1
  26. package/dist/package-target.d.ts +12 -0
  27. package/dist/package-target.d.ts.map +1 -0
  28. package/dist/package-target.js +97 -0
  29. package/dist/package-target.js.map +1 -0
  30. package/dist/probes.d.ts +27 -0
  31. package/dist/probes.d.ts.map +1 -1
  32. package/dist/probes.js +140 -33
  33. package/dist/probes.js.map +1 -1
  34. package/dist/process.d.ts +2 -0
  35. package/dist/process.d.ts.map +1 -1
  36. package/dist/process.js +63 -1
  37. package/dist/process.js.map +1 -1
  38. package/dist/project-root.d.ts +3 -0
  39. package/dist/project-root.d.ts.map +1 -0
  40. package/dist/project-root.js +83 -0
  41. package/dist/project-root.js.map +1 -0
  42. package/dist/provision.d.ts +3 -1
  43. package/dist/provision.d.ts.map +1 -1
  44. package/dist/provision.js +375 -97
  45. package/dist/provision.js.map +1 -1
  46. package/dist/runners.d.ts +18 -0
  47. package/dist/runners.d.ts.map +1 -0
  48. package/dist/runners.js +151 -0
  49. package/dist/runners.js.map +1 -0
  50. package/dist/runtime-files.d.ts +3 -0
  51. package/dist/runtime-files.d.ts.map +1 -0
  52. package/dist/runtime-files.js +12 -0
  53. package/dist/runtime-files.js.map +1 -0
  54. package/dist/state.d.ts +10 -0
  55. package/dist/state.d.ts.map +1 -0
  56. package/dist/state.js +62 -0
  57. package/dist/state.js.map +1 -0
  58. package/dist/sync-resolver.d.ts +2 -0
  59. package/dist/sync-resolver.d.ts.map +1 -0
  60. package/dist/sync-resolver.js +24 -0
  61. package/dist/sync-resolver.js.map +1 -0
  62. package/dist/virtual-environment.d.ts +9 -0
  63. package/dist/virtual-environment.d.ts.map +1 -0
  64. package/dist/virtual-environment.js +42 -0
  65. package/dist/virtual-environment.js.map +1 -0
  66. package/package.json +26 -7
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pyfig
2
2
 
3
- `pyfig` finds and configures a compatible Python interpreter and Python package manager from Bun or Node.js. It validates versions with npm's `semver`, checks the usual `python3`/`python` and `pip3`/`pip` names, and uses uv when a compatible local tool is unavailable.
3
+ `pyfig` creates or reuses `<project-root>/.venv` and binds every Python and package subprocess to that environment from Bun or Node.js. A valid existing venv is the first choice. Otherwise Pyfig discovers a compatible interpreter, installs its checksum-verified uv and managed Python when necessary, and provisions the root venv without requiring Python or Node to be preinstalled alongside Bun.
4
4
 
5
5
  ## Install
6
6
 
@@ -11,31 +11,135 @@ bun add pyfig
11
11
  ## Library
12
12
 
13
13
  ```ts
14
- import { ensureEnvironment } from 'pyfig';
14
+ import { createPyfig } from 'pyfig';
15
15
 
16
- const environment = await ensureEnvironment({
16
+ const pyfig = createPyfig({
17
17
  pythonRange: '>=3.11.0 <4.0.0',
18
18
  pipRange: '>=23.0.0',
19
+ requireEmbedding: true, // require compatible CPython headers, library, and architecture
20
+ requireGil: true, // reject free-threaded CPython ABIs
19
21
  });
22
+ const environment = await pyfig.prepare();
20
23
 
21
24
  console.log(environment.python);
22
25
  console.log(environment.packageManager);
23
26
  ```
24
27
 
25
- Defaults are Python `>=3.10.0 <4.0.0`, pip `>=20.3.0`, managed Python `3.12`, and uv `0.13.6`. Pass any npm-compatible SemVer range.
28
+ Run Python, pip, and uv with standard argument arrays and child-process spawn options:
26
29
 
27
- If pip is unsuitable, `packageManager.kind` is `uv`; its `prefixArgs` is `['pip']`. If Python is unsuitable, pyfig installs the configured managed Python with uv and returns that interpreter.
30
+ ```ts
31
+ import {
32
+ pipSpawn,
33
+ pipSpawnSync,
34
+ pythonSpawn,
35
+ pythonSpawnSync,
36
+ uvSpawn,
37
+ uvSpawnSync,
38
+ } from 'pyfig';
39
+
40
+ const python = await pythonSpawn(['script.py', '--verbose'], { stdio: 'inherit' });
41
+ const pythonVersion = pythonSpawnSync(['--version'], { encoding: 'utf8' });
42
+
43
+ const pip = await pipSpawn(['install', 'ruff'], { stdio: 'inherit' });
44
+ const installed = pipSpawnSync(['list', '--format', 'json'], { encoding: 'utf8' });
45
+
46
+ const uv = await uvSpawn(['python', 'list'], { stdio: 'inherit' });
47
+ const uvVersion = uvSpawnSync(['--version'], { encoding: 'utf8' });
48
+ ```
49
+
50
+ The asynchronous helpers return `Promise<ChildProcess>` because discovery or installation may precede the spawn. Synchronous helpers perform the same setup in a child launched with `process.execPath`, so Bun stays Bun and Node stays Node; no `node` executable is assumed. Python execution only requires the venv, not pip or uv. Package helpers additionally resolve uv and receive `VIRTUAL_ENV`, the venv scripts directory at the front of `PATH`, and the exact venv interpreter.
51
+
52
+ Pass any `PyfigOptions` as the third argument when a helper needs a custom Python range, pip range, project root, cache directory, or installation policy.
53
+
54
+ Pyfig publishes matching ESM and CommonJS entry points. The root API intentionally contains only configuration constants, the scoped client, preparation helpers, spawn helpers, project-root discovery, public result types, and cache invalidation. Probe and lock internals are not package exports.
55
+
56
+ Defaults are Python `>=3.10.0 <4.0.0`, pip `>=20.3.0`, managed Python `3.12`, pinned uv `0.13.6`, and system uv `>=0.13.6 <1.0.0`.
57
+
58
+ Package mutation is always performed by uv, whose own target-environment lock protects concurrent installs and removals. When uv installation is explicitly disabled, the venv interpreter's `python -m pip` remains available for read-only commands such as `list`; mutable pip commands are rejected. Standalone `pip` and `pip3` executables are never discovered or executed.
59
+
60
+ Native consumers can set `requireEmbedding: true` to require CPython, `Python.h`, a linkable library, and architecture/pointer-width compatibility with the JavaScript runtime. Probe results include implementation, architecture, pointer width, release level, base executable, and library kind. Set `requireGil: true` when an addon targets the standard GIL-enabled ABI.
61
+
62
+ Given the same `PyfigOptions`, `pipSpawn(['install', 'xyz'])` and `uvSpawn(['pip', 'install', 'xyz'])` resolve the same project root, `.venv`, and interpreter. Target-changing arguments such as `--python`/`-p`, `--system`, `--target`/`-t`, `--prefix`, and `--user` are rejected. Target-changing environment variables are removed, persistent package-manager configuration is disabled for these commands, and uv receives the exact venv interpreter through `UV_PYTHON`.
63
+
64
+ Project-root discovery uses Pacwich for workspace repositories. Single-package projects recognize Bun, npm, pnpm, Yarn, uv, and `pyproject.toml` metadata before falling back to a Git root, the nearest package manifest, or the current directory. Set `projectRoot` or `PYFIG_PROJECT_ROOT` to make ownership explicit.
65
+
66
+ ## Decision flows
67
+
68
+ ### Python, venv, and package manager
69
+
70
+ ```mermaid
71
+ graph TD;
72
+ A[Ensure environment] --> V{Repository virtual environment usable};
73
+ V -->|Yes| Q{Compatible uv available};
74
+ V -->|No| B{Configured Python supplied};
75
+ B -->|Valid| P[Select base Python];
76
+ B -->|Invalid| X[Fail explicit configuration];
77
+ B -->|No| C{Managed uv Python compatible};
78
+ C -->|Yes| P;
79
+ C -->|No| D{System Python compatible};
80
+ D -->|Yes| P;
81
+ D -->|No| E{Python installation allowed};
82
+ E -->|No| X2[Fail without changes];
83
+ E -->|Yes| U[Install pinned uv];
84
+ U --> M[Install managed Python];
85
+ M --> P;
86
+
87
+ P --> W{uv available or installable};
88
+ W -->|Yes| UVV[Create virtual environment with uv];
89
+ W -->|No| SV[Create virtual environment with Python];
90
+ UVV --> Q;
91
+ SV --> Q;
92
+
93
+ Q -->|Yes| R[Use uv pip with selected Python];
94
+ Q -->|No| S{Python module pip compatible};
95
+ S -->|Read only| T[Use Python module pip];
96
+ S -->|Mutation or missing| X3[Require uv or fail];
97
+ ```
98
+
99
+ ### Direct uv spawning
100
+
101
+ ```mermaid
102
+ graph TD;
103
+ A[Start uv helper] --> B{Compatible uv available};
104
+ B -->|Yes| H[Spawn uv with pyfig cache environment];
105
+ B -->|No| C{Automatic setup enabled};
106
+ C -->|No| F[Fail without installing];
107
+ C -->|Yes| I[Download verified platform uv archive];
108
+ I --> J{Installed uv validates};
109
+ J -->|No| K[Fail installation];
110
+ J -->|Yes| H;
111
+ ```
28
112
 
29
113
  ## CLI
30
114
 
31
115
  ```sh
32
- bunx pyfig
33
- bunx pyfig --check
34
- bunx pyfig --python-range '>=3.12.0 <3.14.0' --pip-range '>=24.0.0'
35
- bunx pyfig --json
116
+ bunx --bun pyfig
117
+ bunx --bun pyfig --check
118
+ bunx --bun pyfig --python-range '>=3.12.0 <3.14.0' --pip-range '>=24.0.0'
119
+ bunx --bun pyfig --require-embedding --require-gil
120
+ bunx --bun pyfig --project-root /path/to/repository
121
+ bunx --bun pyfig --json
36
122
  ```
37
123
 
38
- `--check` prohibits installation and exits nonzero when no compatible environment exists.
124
+ `bunx --bun` runs the CLI with Bun itself and does not require a separate Node.js executable.
125
+
126
+ `--check` prohibits virtual-environment creation and tool installation, and exits nonzero when no compatible environment exists.
127
+
128
+ ## Public API
129
+
130
+ | Export | Responsibility |
131
+ | --- | --- |
132
+ | `createPyfig(options)` | Bind one immutable policy object to preparation and spawn operations. |
133
+ | `ensureEnvironment()` | Return the root venv Python and its safe package manager. |
134
+ | `ensureVirtualEnvironment()` | Return the root venv Python without requiring package tooling. |
135
+ | `ensureUv()` | Return a compatible system uv or the verified managed uv. |
136
+ | `pythonSpawn[Sync]()` | Run the root venv interpreter. |
137
+ | `pipSpawn[Sync]()` | Run package commands against the root venv; mutations require uv. |
138
+ | `uvSpawn[Sync]()` | Run uv; direct `uv pip` calls receive the same strict target policy. |
139
+ | `resolveProjectRoot()` | Resolve workspace, package-manager, Python, or Git ownership. |
140
+ | `clearPyfigCaches()` | Invalidate realm-local validated resolution caches. |
141
+
142
+ The standalone functions remain convenient for scripts. Long-lived consumers such as Pynect should prefer `createPyfig()` so every call shares one policy and the validated memoization path.
39
143
 
40
144
  ## Configuration
41
145
 
@@ -44,26 +148,59 @@ Precedence is API/CLI option, environment variable, then default.
44
148
  | API option | Environment variable | Default |
45
149
  | --- | --- | --- |
46
150
  | `python` | `PYFIG_PYTHON` | auto-detect |
47
- | `pip` | `PYFIG_PIP` | auto-detect |
48
151
  | `pythonRange` | `PYFIG_PYTHON_RANGE` | `>=3.10.0 <4.0.0` |
49
152
  | `pipRange` | `PYFIG_PIP_RANGE` | `>=20.3.0` |
50
153
  | `managedPython` | `PYFIG_MANAGED_PYTHON` | `3.12` |
51
154
  | `uvVersion` | `PYFIG_UV_VERSION` | `0.13.6` |
155
+ | `uvRange` | `PYFIG_UV_RANGE` | `>=0.13.6 <1.0.0` |
52
156
  | `cacheDir` | `PYFIG_HOME` | OS cache directory |
53
- | `autoInstall` | `PYFIG_AUTO_INSTALL` | `true` |
157
+ | `projectRoot` | `PYFIG_PROJECT_ROOT` | auto-detected from current directory |
158
+ | `requireEmbedding` | `PYFIG_REQUIRE_EMBEDDING` | `false` |
159
+ | `requireGil` | `PYFIG_REQUIRE_GIL` | `false` |
160
+ | `autoInstall` | `PYFIG_AUTO_INSTALL` | `true` (fallback for the three policies below) |
161
+ | `installPython` | `PYFIG_INSTALL_PYTHON` | `autoInstall` |
162
+ | `installUv` | `PYFIG_INSTALL_UV` | `autoInstall` |
163
+ | `createVirtualEnvironment` | `PYFIG_CREATE_VIRTUAL_ENVIRONMENT` | `autoInstall` |
164
+ | `probeTimeoutMs` | `PYFIG_PROBE_TIMEOUT_MS` | `10000` |
165
+ | `installTimeoutMs` | `PYFIG_INSTALL_TIMEOUT_MS` | `600000` |
166
+ | `downloadTimeoutMs` | `PYFIG_DOWNLOAD_TIMEOUT_MS` | `300000` |
167
+ | `coordinationTimeoutMs` | `PYFIG_COORDINATION_TIMEOUT_MS` | install + download + 60 seconds |
168
+ | `cacheTtlMs` | `PYFIG_CACHE_TTL_MS` | `30000` (`0` disables completed-result caching) |
54
169
 
55
170
  Set `PYFIG_UV` to select an existing uv executable.
56
171
 
172
+ ## Platform support
173
+
174
+ Pyfig bootstraps uv on x64 and arm64 Windows, macOS, and Linux. Linux uses Astral's fully static musl artifact, which also runs on glibc systems. The practical platform minimums follow uv: Windows 10 or Server 2016 and macOS 13; macOS 12 may work when `realpath` is installed. Other architectures fail before downloading an incorrect artifact.
175
+
176
+ Node.js 22 through 26 and Bun 1.2+ are supported. Bun-only hosts do not need a `node` executable because synchronous bridges relaunch `process.execPath`. Other JavaScript runtimes are outside the supported contract unless they provide compatible Node built-ins and process semantics.
177
+
57
178
  ## Installation safety
58
179
 
59
- pyfig uses the official, version-pinned Astral installer: PowerShell on Windows and POSIX `sh` elsewhere. The installer chooses the supported platform and architecture. `UV_UNMANAGED_INSTALL` keeps uv inside pyfig's cache, and `UV_NO_MODIFY_PATH=1` prevents shell-profile changes.
180
+ Pyfig downloads the platform's version-pinned uv archive from Astral's GitHub release, streams it through a bounded request with environment-proxy support, verifies the adjacent SHA-256 checksum, validates the staged executable's exact version, and atomically publishes it into Pyfig's cache. It does not execute an installer script or modify shell profiles.
60
181
 
61
- uv and managed Python installation use `proper-lockfile` with a check-lock-check sequence:
182
+ Virtual-environment creation, uv installation, and managed Python installation share one coordination utility with this order:
62
183
 
63
184
  1. Check for a usable tool.
64
- 2. Acquire a cross-process filesystem lock.
65
- 3. Check again after acquiring the lock.
66
- 4. Install only when the tool is still absent.
185
+ 2. Queue behind the current JavaScript realm's mutex.
186
+ 3. Use Node's cross-worker `LockManager` when the runtime provides it.
187
+ 4. Use the system `flock` command when it is available and functional.
188
+ 5. Acquire the portable heartbeat-based filesystem lease used by every runtime.
189
+ 6. Check again and install only when the resource is still absent.
190
+
191
+ Long child operations are asynchronous so the filesystem lease heartbeat remains live. uv supplies the separate target-venv lock for package mutations. Resolution results are memoized for a short, configurable interval and revalidated against executable paths before reuse.
192
+
193
+ ## Publishing
194
+
195
+ Pyfig publishes only from a `v<package-version>` Git tag through `.github/workflows/publish.yml`. The workflow validates the exact tag, rejects a version already present on npm, builds and audits one tarball, and publishes that same archive through npm Trusted Publishing with provenance. It uses the GitHub `npm` environment and does not accept an npm token.
196
+
197
+ Before pushing a release tag, commit the matching `package.json` version and run:
198
+
199
+ ```sh
200
+ bun install --frozen-lockfile
201
+ bun run verify
202
+ bun run release:check -- v0.3.0
203
+ ```
67
204
 
68
205
  ## Development
69
206
 
package/SECURITY.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # Security
2
2
 
3
- pyfig can download and execute Astral's official uv installer and can use 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 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.
4
4
 
5
- The uv installer URL is pinned to the configured uv version and fetched over HTTPS. uv is installed into pyfig's cache without modifying shell profiles or `PATH`. Cross-process locks prevent concurrent installation into the same managed directories.
5
+ The uv release URL is pinned to an exact validated version and fetched over HTTPS with bounded redirects and timeouts. 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.
6
6
 
7
- Executable paths and version ranges are configuration, not untrusted request data. Do not let an untrusted user choose `python`, `pip`, `PYFIG_UV`, `cacheDir`, or related environment variables in a privileged process.
7
+ Provisioning uses realm, worker, optional native advisory, and portable filesystem coordination followed by a post-lock capability check. Package mutation uses uv's target-environment lock. Pyfig rejects package-manager arguments and removes environment/configuration values that could redirect an operation away from the repository `.venv`.
8
+
9
+ Executable paths, project roots, version ranges, 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.