@chikhamx/voidx 1.0.0 → 1.1.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.
- package/bin/postinstall.js +319 -0
- package/bin/voidx.js +148 -59
- package/package.json +12 -2
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// Post-install: download a standalone Python, create venv, pip install voidx.
|
|
5
|
+
// Falls back to system Python if download fails.
|
|
6
|
+
|
|
7
|
+
const { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } = require("fs");
|
|
8
|
+
const { spawnSync } = require("child_process");
|
|
9
|
+
const { get, request } = require("https");
|
|
10
|
+
const { createGunzip } = require("zlib");
|
|
11
|
+
const { pipeline } = require("stream/promises");
|
|
12
|
+
const { createUnzip } = require("zlib");
|
|
13
|
+
const { join, dirname } = require("path");
|
|
14
|
+
const os = require("os");
|
|
15
|
+
const fs = require("fs");
|
|
16
|
+
const path = require("path");
|
|
17
|
+
const { execSync } = require("child_process");
|
|
18
|
+
|
|
19
|
+
const pkg = require("../package.json");
|
|
20
|
+
|
|
21
|
+
// ── Python build metadata ──────────────────────────────────────────────────
|
|
22
|
+
// Pin to a specific python-build-standalone release tag and CPython version.
|
|
23
|
+
// Update these when upgrading the bundled Python.
|
|
24
|
+
const PBS_TAG = "20260602";
|
|
25
|
+
const PBS_CPYTHON = "3.12.13";
|
|
26
|
+
const PBS_RELEASE_BASE = `https://github.com/astral-sh/python-build-standalone/releases/download/${PBS_TAG}`;
|
|
27
|
+
|
|
28
|
+
// ── Platform mapping ───────────────────────────────────────────────────────
|
|
29
|
+
function getPlatformInfo() {
|
|
30
|
+
const platform = os.platform();
|
|
31
|
+
const arch = os.arch();
|
|
32
|
+
|
|
33
|
+
const map = {
|
|
34
|
+
"darwin-arm64": { target: "aarch64-apple-darwin" },
|
|
35
|
+
"darwin-x64": { target: "x86_64-apple-darwin" },
|
|
36
|
+
"linux-arm64": { target: "aarch64-unknown-linux-gnu" },
|
|
37
|
+
"linux-x64": { target: "x86_64-unknown-linux-gnu" },
|
|
38
|
+
"win32-arm64": { target: "aarch64-pc-windows-msvc" },
|
|
39
|
+
"win32-x64": { target: "x86_64-pc-windows-msvc" },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const key = `${platform}-${arch}`;
|
|
43
|
+
const entry = map[key];
|
|
44
|
+
if (!entry) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return { platform, arch, target: entry.target };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getPbsFilename(target) {
|
|
51
|
+
return `cpython-${PBS_CPYTHON}+${PBS_TAG}-${target}-install_only_stripped.tar.gz`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Paths ──────────────────────────────────────────────────────────────────
|
|
55
|
+
function resolveDataHome(env) {
|
|
56
|
+
if (env.VOIDX_NPM_HOME) return path.resolve(env.VOIDX_NPM_HOME);
|
|
57
|
+
if (process.platform === "win32") {
|
|
58
|
+
return env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
|
|
59
|
+
}
|
|
60
|
+
return env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolvePythonDir(env) {
|
|
64
|
+
return path.join(resolveDataHome(env), "voidx", "python");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveVenvDir(env) {
|
|
68
|
+
if (env.VOIDX_NPM_VENV) return path.resolve(env.VOIDX_NPM_VENV);
|
|
69
|
+
return path.join(resolveDataHome(env), "voidx", "npm-venv");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolveVenvPython(venvDir) {
|
|
73
|
+
return process.platform === "win32"
|
|
74
|
+
? path.join(venvDir, "Scripts", "python.exe")
|
|
75
|
+
: path.join(venvDir, "bin", "python");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveVoidxExecutable(venvDir) {
|
|
79
|
+
return process.platform === "win32"
|
|
80
|
+
? path.join(venvDir, "Scripts", "voidx.exe")
|
|
81
|
+
: path.join(venvDir, "bin", "voidx");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveBundledPython(pythonDir, platform) {
|
|
85
|
+
// install_only_stripped layout:
|
|
86
|
+
// unix: python/python/bin/python3
|
|
87
|
+
// win32: python/python/python.exe
|
|
88
|
+
const installDir = path.join(pythonDir, "python");
|
|
89
|
+
if (platform === "win32") {
|
|
90
|
+
return path.join(installDir, "python.exe");
|
|
91
|
+
}
|
|
92
|
+
return path.join(installDir, "bin", "python3");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Download ───────────────────────────────────────────────────────────────
|
|
96
|
+
function downloadFile(url, dest) {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const doRequest = (currentUrl, redirects = 0) => {
|
|
99
|
+
if (redirects > 5) {
|
|
100
|
+
return reject(new Error(`Too many redirects downloading ${url}`));
|
|
101
|
+
}
|
|
102
|
+
const mod = currentUrl.startsWith("https") ? require("https") : require("http");
|
|
103
|
+
mod.get(currentUrl, { timeout: 60000 }, (res) => {
|
|
104
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
105
|
+
return doRequest(res.headers.location, redirects + 1);
|
|
106
|
+
}
|
|
107
|
+
if (res.statusCode !== 200) {
|
|
108
|
+
return reject(new Error(`HTTP ${res.statusCode} downloading ${currentUrl}`));
|
|
109
|
+
}
|
|
110
|
+
const total = parseInt(res.headers["content-length"] || "0", 10);
|
|
111
|
+
let downloaded = 0;
|
|
112
|
+
let lastPercent = -1;
|
|
113
|
+
|
|
114
|
+
res.on("data", (chunk) => {
|
|
115
|
+
downloaded += chunk.length;
|
|
116
|
+
if (total > 0) {
|
|
117
|
+
const pct = Math.floor((downloaded / total) * 100);
|
|
118
|
+
if (pct !== lastPercent && pct % 10 === 0) {
|
|
119
|
+
lastPercent = pct;
|
|
120
|
+
process.stderr.write(` ${pct}% (${Math.round(downloaded / 1024 / 1024)}/${Math.round(total / 1024 / 1024)} MB)\n`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const stream = createWriteStream(dest);
|
|
126
|
+
res.pipe(stream);
|
|
127
|
+
stream.on("finish", () => {
|
|
128
|
+
stream.close();
|
|
129
|
+
resolve(dest);
|
|
130
|
+
});
|
|
131
|
+
stream.on("error", reject);
|
|
132
|
+
res.on("error", reject);
|
|
133
|
+
}).on("error", reject);
|
|
134
|
+
};
|
|
135
|
+
doRequest(url);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Extract ────────────────────────────────────────────────────────────────
|
|
140
|
+
function extractTarGz(archive, destDir) {
|
|
141
|
+
// Use the system tar command — available on macOS, Linux, and modern Windows (10+)
|
|
142
|
+
const result = spawnSync("tar", ["-xzf", archive, "-C", destDir], {
|
|
143
|
+
encoding: "utf8",
|
|
144
|
+
windowsHide: true,
|
|
145
|
+
});
|
|
146
|
+
if (result.error) {
|
|
147
|
+
throw new Error(`Failed to extract Python: ${result.error.message}`);
|
|
148
|
+
}
|
|
149
|
+
if (result.status !== 0) {
|
|
150
|
+
throw new Error(`Failed to extract Python (tar exited ${result.status}).`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Version marker ─────────────────────────────────────────────────────────
|
|
155
|
+
function readMarker(markerPath) {
|
|
156
|
+
try { return readFileSync(markerPath, "utf8"); } catch { return ""; }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function writeMarker(markerPath, content) {
|
|
160
|
+
mkdirSync(dirname(markerPath), { recursive: true });
|
|
161
|
+
writeFileSync(markerPath, content);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── pip install ────────────────────────────────────────────────────────────
|
|
165
|
+
function pipInstall(venvPython, packageSpec, env) {
|
|
166
|
+
const pipEnv = Object.assign({}, env, {
|
|
167
|
+
PIP_NO_INPUT: "1",
|
|
168
|
+
PIP_DISABLE_PIP_VERSION_CHECK: "1",
|
|
169
|
+
PYTHON_KEYRING_BACKEND: "keyring.backends.null.Keyring",
|
|
170
|
+
});
|
|
171
|
+
const result = spawnSync(
|
|
172
|
+
venvPython,
|
|
173
|
+
["-m", "pip", "install", "--upgrade", "--progress-bar", "on", packageSpec],
|
|
174
|
+
{ encoding: "utf8", stdio: "inherit", windowsHide: true, env: pipEnv }
|
|
175
|
+
);
|
|
176
|
+
if (result.error) {
|
|
177
|
+
throw new Error(`pip install failed: ${result.error.message}`);
|
|
178
|
+
}
|
|
179
|
+
if (result.status !== 0) {
|
|
180
|
+
throw new Error("pip install failed. See errors above.");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── System Python fallback ─────────────────────────────────────────────────
|
|
185
|
+
function probeSystemPython() {
|
|
186
|
+
const candidates = [
|
|
187
|
+
{ command: "python3", args: [] },
|
|
188
|
+
{ command: "python", args: [] },
|
|
189
|
+
{ command: "python3.12", args: [] },
|
|
190
|
+
{ command: "python3.11", args: [] },
|
|
191
|
+
];
|
|
192
|
+
if (process.platform === "win32") {
|
|
193
|
+
candidates.push({ command: "py", args: ["-3"] });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const candidate of candidates) {
|
|
197
|
+
const result = spawnSync(
|
|
198
|
+
candidate.command,
|
|
199
|
+
[...candidate.args, "-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"],
|
|
200
|
+
{ encoding: "utf8", windowsHide: true }
|
|
201
|
+
);
|
|
202
|
+
if (result.error || result.status !== 0) continue;
|
|
203
|
+
const ver = (result.stdout || "").trim();
|
|
204
|
+
const match = /^(\d+)\.(\d+)/.exec(ver);
|
|
205
|
+
if (!match) continue;
|
|
206
|
+
const major = parseInt(match[1], 10);
|
|
207
|
+
const minor = parseInt(match[2], 10);
|
|
208
|
+
if (major > 3 || (major === 3 && minor >= 11)) {
|
|
209
|
+
return { command: candidate.command, args: candidate.args };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
216
|
+
async function main() {
|
|
217
|
+
const env = process.env;
|
|
218
|
+
const venvDir = resolveVenvDir(env);
|
|
219
|
+
const venvPython = resolveVenvPython(venvDir);
|
|
220
|
+
const executable = resolveVoidxExecutable(venvDir);
|
|
221
|
+
const packageSpec = env.VOIDX_NPM_PACKAGE_SPEC || `voidx==${pkg.version}`;
|
|
222
|
+
const markerPath = path.join(venvDir, ".voidx-npm-version");
|
|
223
|
+
const marker = `${pkg.version}\n${packageSpec}\n${PBS_TAG}\n${PBS_CPYTHON}\n`;
|
|
224
|
+
|
|
225
|
+
// Already set up and up-to-date?
|
|
226
|
+
if (existsSync(executable) && readMarker(markerPath) === marker) {
|
|
227
|
+
console.error(`\n✅ voidx ${pkg.version} ready (cached)\n`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Step 1: Get a Python interpreter (bundled or system)
|
|
232
|
+
let pythonForVenv;
|
|
233
|
+
const platformInfo = getPlatformInfo();
|
|
234
|
+
|
|
235
|
+
if (platformInfo && env.VOIDX_NPM_SKIP_BUNDLED_PYTHON !== "1") {
|
|
236
|
+
console.error(`\n🐍 Setting up voidx ${pkg.version}…\n`);
|
|
237
|
+
console.error(" [1/3] Downloading Python runtime…");
|
|
238
|
+
|
|
239
|
+
const pythonDir = resolvePythonDir(env);
|
|
240
|
+
const pbsFilename = getPbsFilename(platformInfo.target);
|
|
241
|
+
const pbsUrl = `${PBS_RELEASE_BASE}/${pbsFilename}`;
|
|
242
|
+
const archivePath = path.join(pythonDir, pbsFilename);
|
|
243
|
+
const bundledPython = resolveBundledPython(pythonDir, platformInfo.platform);
|
|
244
|
+
|
|
245
|
+
if (!existsSync(bundledPython)) {
|
|
246
|
+
try {
|
|
247
|
+
mkdirSync(pythonDir, { recursive: true });
|
|
248
|
+
|
|
249
|
+
if (!existsSync(archivePath)) {
|
|
250
|
+
console.error(` Downloading ${pbsFilename}…`);
|
|
251
|
+
await downloadFile(pbsUrl, archivePath);
|
|
252
|
+
console.error(" Download complete.");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
console.error(" Extracting Python runtime…");
|
|
256
|
+
extractTarGz(archivePath, pythonDir);
|
|
257
|
+
console.error(" Extraction complete.");
|
|
258
|
+
|
|
259
|
+
// Clean up archive to save disk
|
|
260
|
+
try { fs.unlinkSync(archivePath); } catch {}
|
|
261
|
+
} catch (err) {
|
|
262
|
+
console.error(`\n ⚠️ Bundled Python download failed: ${err.message}`);
|
|
263
|
+
console.error(" Falling back to system Python…\n");
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
console.error(" Using cached Python runtime.");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (existsSync(bundledPython)) {
|
|
270
|
+
pythonForVenv = { command: bundledPython, args: [], label: "bundled" };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Fallback to system Python
|
|
275
|
+
if (!pythonForVenv) {
|
|
276
|
+
const sysPython = probeSystemPython();
|
|
277
|
+
if (sysPython) {
|
|
278
|
+
pythonForVenv = sysPython;
|
|
279
|
+
console.error("\n Using system Python.\n");
|
|
280
|
+
} else {
|
|
281
|
+
console.error("\n ❌ No Python 3.11+ found. Please install Python 3.11+ and run voidx again.\n");
|
|
282
|
+
console.error(" macOS: brew install python@3.12");
|
|
283
|
+
console.error(" Linux: sudo apt install python3.12");
|
|
284
|
+
console.error(" Windows: https://python.org/downloads\n");
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Step 2: Create venv
|
|
290
|
+
console.error(" [2/3] Creating virtual environment…");
|
|
291
|
+
if (!existsSync(venvPython)) {
|
|
292
|
+
const venvResult = spawnSync(
|
|
293
|
+
pythonForVenv.command,
|
|
294
|
+
[...pythonForVenv.args, "-m", "venv", venvDir],
|
|
295
|
+
{ encoding: "utf8", stdio: "inherit", windowsHide: true }
|
|
296
|
+
);
|
|
297
|
+
if (venvResult.error) {
|
|
298
|
+
console.error(`\n ❌ Failed to create venv: ${venvResult.error.message}\n`);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
if (venvResult.status !== 0) {
|
|
302
|
+
console.error("\n ❌ Failed to create venv. See errors above.\n");
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Step 3: pip install
|
|
308
|
+
console.error(" [3/3] Installing voidx and dependencies…");
|
|
309
|
+
pipInstall(venvPython, packageSpec, env);
|
|
310
|
+
|
|
311
|
+
// Done
|
|
312
|
+
writeMarker(markerPath, marker);
|
|
313
|
+
console.error(`\n✅ voidx ${pkg.version} installed! Run: voidx\n`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
main().catch((err) => {
|
|
317
|
+
console.error(`\n❌ Setup failed: ${err.message}\n`);
|
|
318
|
+
process.exit(1);
|
|
319
|
+
});
|
package/bin/voidx.js
CHANGED
|
@@ -8,6 +8,13 @@ const path = require("path");
|
|
|
8
8
|
|
|
9
9
|
const pkg = require("../package.json");
|
|
10
10
|
|
|
11
|
+
// ── Configuration ──────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const PBS_TAG = "20260602";
|
|
14
|
+
const PBS_PYTHON_MAJOR = "3.12";
|
|
15
|
+
|
|
16
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
11
18
|
function main(argv = process.argv.slice(2), env = process.env) {
|
|
12
19
|
try {
|
|
13
20
|
const python = selectPython(env);
|
|
@@ -23,14 +30,17 @@ function main(argv = process.argv.slice(2), env = process.env) {
|
|
|
23
30
|
process.exit(code === null ? 1 : code);
|
|
24
31
|
});
|
|
25
32
|
child.on("error", (error) => {
|
|
26
|
-
fail(`Failed to start voidx
|
|
33
|
+
fail(`Failed to start voidx: ${error.message}`);
|
|
27
34
|
});
|
|
28
35
|
} catch (error) {
|
|
29
36
|
fail(error.message);
|
|
30
37
|
}
|
|
31
38
|
}
|
|
32
39
|
|
|
40
|
+
// ── Python selection ───────────────────────────────────────────────────────
|
|
41
|
+
|
|
33
42
|
function selectPython(env) {
|
|
43
|
+
// 1. Explicit override
|
|
34
44
|
const explicit = env.VOIDX_PYTHON;
|
|
35
45
|
if (explicit) {
|
|
36
46
|
const candidate = { command: explicit, args: [], label: explicit };
|
|
@@ -46,12 +56,26 @@ function selectPython(env) {
|
|
|
46
56
|
return candidate;
|
|
47
57
|
}
|
|
48
58
|
|
|
59
|
+
// 2. Bundled Python (downloaded by postinstall)
|
|
60
|
+
const bundledBin = resolveBundledPythonBin(env);
|
|
61
|
+
if (bundledBin && fs.existsSync(bundledBin)) {
|
|
62
|
+
const candidate = { command: bundledBin, args: [], label: "bundled" };
|
|
63
|
+
const probe = probePython(candidate);
|
|
64
|
+
if (probe.ok && isCompatible(probe.version)) {
|
|
65
|
+
return candidate;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 3. System Python
|
|
49
70
|
const candidates = [
|
|
50
71
|
{ command: "python3", args: [], label: "python3" },
|
|
51
72
|
{ command: "python", args: [], label: "python" },
|
|
73
|
+
{ command: "python3.13", args: [], label: "python3.13" },
|
|
74
|
+
{ command: "python3.12", args: [], label: "python3.12" },
|
|
75
|
+
{ command: "python3.11", args: [], label: "python3.11" },
|
|
52
76
|
];
|
|
53
77
|
if (process.platform === "win32") {
|
|
54
|
-
candidates.push({ command: "py", args: ["-3
|
|
78
|
+
candidates.push({ command: "py", args: ["-3"], label: "py -3" });
|
|
55
79
|
}
|
|
56
80
|
|
|
57
81
|
const oldVersions = [];
|
|
@@ -66,11 +90,14 @@ function selectPython(env) {
|
|
|
66
90
|
oldVersions.push(`${probe.versionText} at ${candidate.label}`);
|
|
67
91
|
}
|
|
68
92
|
|
|
93
|
+
const hint = pythonHint();
|
|
69
94
|
if (oldVersions.length > 0) {
|
|
70
|
-
throw new Error(
|
|
95
|
+
throw new Error(
|
|
96
|
+
`voidx requires Python 3.11+. Found ${oldVersions.join(", ")}.\n${hint}`
|
|
97
|
+
);
|
|
71
98
|
}
|
|
72
99
|
throw new Error(
|
|
73
|
-
|
|
100
|
+
`voidx requires Python 3.11+. No Python found.\n${hint}`
|
|
74
101
|
);
|
|
75
102
|
}
|
|
76
103
|
|
|
@@ -109,66 +136,23 @@ function isCompatible(version) {
|
|
|
109
136
|
return version[0] > 3 || (version[0] === 3 && version[1] >= 11);
|
|
110
137
|
}
|
|
111
138
|
|
|
112
|
-
function
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
throw new Error(`voidx executable not found in ${venvDir}.`);
|
|
117
|
-
}
|
|
118
|
-
return;
|
|
139
|
+
function pythonHint() {
|
|
140
|
+
if (process.platform === "darwin") {
|
|
141
|
+
return "Install Python 3.11+ via: brew install python@3.12\n" +
|
|
142
|
+
"Or reinstall voidx to get the bundled Python.";
|
|
119
143
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const marker = `${pkg.version}\n${packageSpec}\n`;
|
|
124
|
-
if (fs.existsSync(executable) && readFile(markerPath) === marker) {
|
|
125
|
-
debug(env, `Using cached npm-managed environment at ${venvDir}`);
|
|
126
|
-
return;
|
|
144
|
+
if (process.platform === "linux") {
|
|
145
|
+
return "Install Python 3.11+ via your package manager (apt/dnf).\n" +
|
|
146
|
+
"Or reinstall voidx to get the bundled Python.";
|
|
127
147
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (!fs.existsSync(venvPython)) {
|
|
132
|
-
debug(env, `Creating npm-managed Python environment at ${venvDir}`);
|
|
133
|
-
runChecked(
|
|
134
|
-
python.command,
|
|
135
|
-
[...python.args, "-m", "venv", venvDir],
|
|
136
|
-
"Failed to create the npm-managed Python environment.",
|
|
137
|
-
env
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
debug(env, `Installing ${packageSpec} into ${venvDir}`);
|
|
142
|
-
runChecked(
|
|
143
|
-
venvPython,
|
|
144
|
-
["-m", "pip", "install", "--upgrade", packageSpec],
|
|
145
|
-
`Failed to install ${packageSpec} into the npm-managed Python environment.`,
|
|
146
|
-
env
|
|
147
|
-
);
|
|
148
|
-
fs.writeFileSync(markerPath, marker);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function runChecked(command, args, errorMessage, env) {
|
|
152
|
-
const result = spawnSync(command, args, {
|
|
153
|
-
encoding: "utf8",
|
|
154
|
-
stdio: env.VOIDX_NPM_DEBUG === "1" ? "inherit" : "pipe",
|
|
155
|
-
windowsHide: true,
|
|
156
|
-
});
|
|
157
|
-
if (result.error) {
|
|
158
|
-
throw new Error(`${errorMessage} ${result.error.message}`);
|
|
159
|
-
}
|
|
160
|
-
if (result.status !== 0) {
|
|
161
|
-
const stderr = result.stderr ? result.stderr.trim() : "";
|
|
162
|
-
throw new Error(stderr ? `${errorMessage} ${stderr}` : errorMessage);
|
|
148
|
+
if (process.platform === "win32") {
|
|
149
|
+
return "Install Python 3.11+ from https://python.org/downloads\n" +
|
|
150
|
+
"Or reinstall voidx to get the bundled Python.";
|
|
163
151
|
}
|
|
152
|
+
return "Install Python 3.11+ or reinstall voidx.";
|
|
164
153
|
}
|
|
165
154
|
|
|
166
|
-
|
|
167
|
-
if (env.VOIDX_NPM_VENV) {
|
|
168
|
-
return path.resolve(env.VOIDX_NPM_VENV);
|
|
169
|
-
}
|
|
170
|
-
return path.join(resolveDataHome(env), "voidx", "npm-venv");
|
|
171
|
-
}
|
|
155
|
+
// ── Paths ──────────────────────────────────────────────────────────────────
|
|
172
156
|
|
|
173
157
|
function resolveDataHome(env) {
|
|
174
158
|
if (env.VOIDX_NPM_HOME) {
|
|
@@ -180,6 +164,27 @@ function resolveDataHome(env) {
|
|
|
180
164
|
return env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share");
|
|
181
165
|
}
|
|
182
166
|
|
|
167
|
+
function resolvePythonDir(env) {
|
|
168
|
+
if (env.VOIDX_NPM_PYTHON_DIR) {
|
|
169
|
+
return path.resolve(env.VOIDX_NPM_PYTHON_DIR);
|
|
170
|
+
}
|
|
171
|
+
return path.join(resolveDataHome(env), "voidx", "python");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function resolveBundledPythonBin(env) {
|
|
175
|
+
const pythonDir = resolvePythonDir(env);
|
|
176
|
+
return process.platform === "win32"
|
|
177
|
+
? path.join(pythonDir, "python", "python.exe")
|
|
178
|
+
: path.join(pythonDir, "python", "bin", `python${PBS_PYTHON_MAJOR}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function resolveVenvDir(env) {
|
|
182
|
+
if (env.VOIDX_NPM_VENV) {
|
|
183
|
+
return path.resolve(env.VOIDX_NPM_VENV);
|
|
184
|
+
}
|
|
185
|
+
return path.join(resolveDataHome(env), "voidx", "npm-venv");
|
|
186
|
+
}
|
|
187
|
+
|
|
183
188
|
function resolveVenvPython(venvDir) {
|
|
184
189
|
return process.platform === "win32"
|
|
185
190
|
? path.join(venvDir, "Scripts", "python.exe")
|
|
@@ -192,6 +197,89 @@ function resolveVoidxExecutable(venvDir) {
|
|
|
192
197
|
: path.join(venvDir, "bin", "voidx");
|
|
193
198
|
}
|
|
194
199
|
|
|
200
|
+
// ── Venv setup ─────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
function ensureVenv(python, venvDir, env) {
|
|
203
|
+
const executable = resolveVoidxExecutable(venvDir);
|
|
204
|
+
if (env.VOIDX_NPM_SKIP_BOOTSTRAP === "1") {
|
|
205
|
+
if (!fs.existsSync(executable)) {
|
|
206
|
+
throw new Error(`voidx executable not found in ${venvDir}.`);
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const markerPath = path.join(venvDir, ".voidx-npm-version");
|
|
212
|
+
const packageSpec = env.VOIDX_NPM_PACKAGE_SPEC || `voidx==${pkg.version}`;
|
|
213
|
+
// Marker must match postinstall.js format (includes PBS_TAG + PBS_CPYTHON)
|
|
214
|
+
const marker = `${pkg.version}\n${packageSpec}\n${PBS_TAG}\n3.12.13\n`;
|
|
215
|
+
if (fs.existsSync(executable) && readFile(markerPath) === marker) {
|
|
216
|
+
debug(env, `Using cached npm-managed environment at ${venvDir}`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fs.mkdirSync(path.dirname(venvDir), { recursive: true });
|
|
221
|
+
const venvPython = resolveVenvPython(venvDir);
|
|
222
|
+
|
|
223
|
+
const isFresh = !fs.existsSync(venvPython);
|
|
224
|
+
if (isFresh) {
|
|
225
|
+
console.error(
|
|
226
|
+
"\n⚙️ Setting up voidx environment (this only happens once)...\n"
|
|
227
|
+
);
|
|
228
|
+
const venvResult = spawnSync(
|
|
229
|
+
python.command,
|
|
230
|
+
[...python.args, "-m", "venv", venvDir],
|
|
231
|
+
{ encoding: "utf8", stdio: "inherit", windowsHide: true }
|
|
232
|
+
);
|
|
233
|
+
if (venvResult.error) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`Failed to create the Python virtual environment: ${venvResult.error.message}`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (venvResult.status !== 0) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
"Failed to create the Python virtual environment. See errors above."
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!fs.existsSync(executable) || readFile(markerPath) !== marker) {
|
|
246
|
+
console.error(
|
|
247
|
+
`\n📦 Downloading ${packageSpec} and dependencies… ` +
|
|
248
|
+
"(1–2 minutes on first run)\n"
|
|
249
|
+
);
|
|
250
|
+
const pipEnv = Object.assign({}, env, {
|
|
251
|
+
PIP_NO_INPUT: "1",
|
|
252
|
+
PIP_DISABLE_PIP_VERSION_CHECK: "1",
|
|
253
|
+
PYTHON_KEYRING_BACKEND: "keyring.backends.null.Keyring",
|
|
254
|
+
});
|
|
255
|
+
const result = spawnSync(
|
|
256
|
+
venvPython,
|
|
257
|
+
[
|
|
258
|
+
"-m",
|
|
259
|
+
"pip",
|
|
260
|
+
"install",
|
|
261
|
+
"--upgrade",
|
|
262
|
+
"--progress-bar",
|
|
263
|
+
"on",
|
|
264
|
+
packageSpec,
|
|
265
|
+
],
|
|
266
|
+
{ encoding: "utf8", stdio: "inherit", windowsHide: true, env: pipEnv }
|
|
267
|
+
);
|
|
268
|
+
if (result.error) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`Failed to install ${packageSpec}: ${result.error.message}`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (result.status !== 0) {
|
|
274
|
+
throw new Error(`Failed to install ${packageSpec}. See errors above.`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
fs.writeFileSync(markerPath, marker);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
282
|
+
|
|
195
283
|
function readFile(filePath) {
|
|
196
284
|
try {
|
|
197
285
|
return fs.readFileSync(filePath, "utf8");
|
|
@@ -220,5 +308,6 @@ module.exports = {
|
|
|
220
308
|
parseVersion,
|
|
221
309
|
resolveDataHome,
|
|
222
310
|
resolveVenvDir,
|
|
311
|
+
resolveBundledPythonBin,
|
|
223
312
|
selectPython,
|
|
224
313
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chikhamx/voidx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "npm launcher for voidx, a terminal AI coding agent.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"voidx": "bin/voidx.js"
|
|
@@ -11,6 +11,15 @@
|
|
|
11
11
|
"engines": {
|
|
12
12
|
"node": ">=16"
|
|
13
13
|
},
|
|
14
|
+
"os": [
|
|
15
|
+
"darwin",
|
|
16
|
+
"linux",
|
|
17
|
+
"win32"
|
|
18
|
+
],
|
|
19
|
+
"cpu": [
|
|
20
|
+
"x64",
|
|
21
|
+
"arm64"
|
|
22
|
+
],
|
|
14
23
|
"keywords": [
|
|
15
24
|
"ai",
|
|
16
25
|
"agent",
|
|
@@ -18,6 +27,7 @@
|
|
|
18
27
|
"cli"
|
|
19
28
|
],
|
|
20
29
|
"scripts": {
|
|
21
|
-
"
|
|
30
|
+
"postinstall": "node bin/postinstall.js",
|
|
31
|
+
"check": "node --check bin/voidx.js && node --check bin/postinstall.js"
|
|
22
32
|
}
|
|
23
33
|
}
|