@chikhamx/voidx 3.1.1 → 3.2.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 (3) hide show
  1. package/bin/postinstall.js +431 -431
  2. package/bin/voidx.js +391 -391
  3. package/package.json +33 -33
package/bin/voidx.js CHANGED
@@ -1,391 +1,391 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- const { spawn, spawnSync } = require("child_process");
5
- const fs = require("fs");
6
- const os = require("os");
7
- const path = require("path");
8
-
9
- const pkg = require("../package.json");
10
-
11
- // ── Configuration ──────────────────────────────────────────────────────────
12
-
13
- const PBS_TAG = "20260602";
14
- const PBS_CPYTHON = "3.12.13";
15
- const PBS_PYTHON_MAJOR = "3.12";
16
-
17
- // ── Main ───────────────────────────────────────────────────────────────────
18
-
19
- function main(argv = process.argv.slice(2), env = process.env) {
20
- try {
21
- const python = selectPython(env);
22
- const venvDir = resolveVenvDir(env);
23
- ensureVenv(python, venvDir, env);
24
- const executable = resolveVoidxExecutable(venvDir);
25
- const childEnv = {
26
- ...env,
27
- VOIDX_LAUNCHED_BY_NPM: "1",
28
- VOIDX_NPM_PACKAGE_VERSION: pkg.version,
29
- };
30
- const child = spawn(executable, argv, { stdio: "inherit", env: childEnv });
31
- child.on("exit", (code, signal) => {
32
- if (signal) {
33
- process.kill(process.pid, signal);
34
- return;
35
- }
36
- process.exit(code === null ? 1 : code);
37
- });
38
- child.on("error", (error) => {
39
- fail(`Failed to start voidx: ${error.message}`);
40
- });
41
- } catch (error) {
42
- fail(error.message);
43
- }
44
- }
45
-
46
- // ── Python selection ───────────────────────────────────────────────────────
47
-
48
- function selectPython(env) {
49
- // 1. Explicit override (for advanced users / debugging)
50
- const explicit = env.VOIDX_PYTHON;
51
- if (explicit) {
52
- const candidate = { command: explicit, args: [], label: explicit };
53
- const probe = probePython(candidate);
54
- if (!probe.ok) {
55
- throw new Error(probe.reason || `Unable to run Python at ${explicit}.`);
56
- }
57
- if (!isCompatible(probe.version)) {
58
- throw new Error(
59
- `voidx requires Python 3.11+. Found Python ${probe.versionText} at ${explicit}.`
60
- );
61
- }
62
- return candidate;
63
- }
64
-
65
- // 2. Bundled Python only — voidx runs in its own isolated environment
66
- const bundledBin = resolveBundledPythonBin(env);
67
- if (bundledBin && fs.existsSync(bundledBin)) {
68
- const candidate = { command: bundledBin, args: [], label: "bundled" };
69
- const probe = probePython(candidate);
70
- if (probe.ok && isCompatible(probe.version)) {
71
- return candidate;
72
- }
73
- }
74
-
75
- // 3. Bundled Python not found — try to bootstrap it (postinstall may have failed)
76
- console.error("\n⚙️ Bundled Python not found, running setup…\n");
77
- const postinstallScript = path.join(path.dirname(__filename), "postinstall.js");
78
- if (fs.existsSync(postinstallScript)) {
79
- const result = spawnSync(process.execPath, [postinstallScript], {
80
- stdio: "inherit",
81
- windowsHide: true,
82
- env: { ...env },
83
- });
84
- if (result.status !== 0) {
85
- console.error(" Setup failed. Try reinstalling:");
86
- console.error(" npm install -g @chikhamx/voidx");
87
- }
88
- }
89
-
90
- // Retry after bootstrap
91
- if (bundledBin && fs.existsSync(bundledBin)) {
92
- const candidate = { command: bundledBin, args: [], label: "bundled" };
93
- const probe = probePython(candidate);
94
- if (probe.ok && isCompatible(probe.version)) {
95
- return candidate;
96
- }
97
- }
98
-
99
- throw new Error(
100
- "voidx bundled Python not found. Reinstall to set up the isolated runtime:\n" +
101
- " npm install -g @chikhamx/voidx\n" +
102
- " or: curl -fsSL https://raw.githubusercontent.com/chikhamx/voidx/master/scripts/install.sh | bash"
103
- );
104
- }
105
-
106
- function probePython(candidate) {
107
- const code = [
108
- "import sys",
109
- "print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')",
110
- ].join("; ");
111
- const result = spawnSync(candidate.command, [...candidate.args, "-c", code], {
112
- encoding: "utf8",
113
- windowsHide: true,
114
- });
115
- if (result.error) {
116
- return { ok: false, reason: result.error.message };
117
- }
118
- if (result.status !== 0) {
119
- return { ok: false, reason: (result.stderr || "").trim() };
120
- }
121
- const versionText = (result.stdout || "").trim();
122
- const version = parseVersion(versionText);
123
- if (!version) {
124
- return { ok: false, reason: `Unable to parse Python version: ${versionText}` };
125
- }
126
- return { ok: true, version, versionText };
127
- }
128
-
129
- function parseVersion(value) {
130
- const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value);
131
- if (!match) {
132
- return null;
133
- }
134
- return match.slice(1).map((part) => Number.parseInt(part, 10));
135
- }
136
-
137
- function isCompatible(version) {
138
- return version[0] > 3 || (version[0] === 3 && version[1] >= 11);
139
- }
140
-
141
- // ── Paths ──────────────────────────────────────────────────────────────────
142
-
143
- function resolveDataHome(env) {
144
- if (env.VOIDX_NPM_HOME) {
145
- return path.resolve(env.VOIDX_NPM_HOME);
146
- }
147
- if (process.platform === "win32") {
148
- return env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
149
- }
150
- return env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share");
151
- }
152
-
153
- function resolvePythonDir(env) {
154
- if (env.VOIDX_NPM_PYTHON_DIR) {
155
- return path.resolve(env.VOIDX_NPM_PYTHON_DIR);
156
- }
157
- return path.join(resolveDataHome(env), "voidx", "python");
158
- }
159
-
160
- function resolveBundledPythonBin(env) {
161
- const pythonDir = resolvePythonDir(env);
162
- return process.platform === "win32"
163
- ? path.join(pythonDir, "python", "python.exe")
164
- : path.join(pythonDir, "python", "bin", "python3");
165
- }
166
-
167
- function resolveVenvDir(env) {
168
- if (env.VOIDX_NPM_VENV) {
169
- return path.resolve(env.VOIDX_NPM_VENV);
170
- }
171
- // Use the same venv directory as install.sh — single environment, no duplicates
172
- return path.join(resolveDataHome(env), "voidx", "venv");
173
- }
174
-
175
- function resolveVenvPython(venvDir) {
176
- return process.platform === "win32"
177
- ? path.join(venvDir, "Scripts", "python.exe")
178
- : path.join(venvDir, "bin", "python");
179
- }
180
-
181
- function resolveVoidxExecutable(venvDir) {
182
- return process.platform === "win32"
183
- ? path.join(venvDir, "Scripts", "voidx.exe")
184
- : path.join(venvDir, "bin", "voidx");
185
- }
186
-
187
- // ── Venv setup ─────────────────────────────────────────────────────────────
188
-
189
- function ensureVenv(python, venvDir, env) {
190
- const executable = resolveVoidxExecutable(venvDir);
191
- if (env.VOIDX_NPM_SKIP_BOOTSTRAP === "1") {
192
- if (!fs.existsSync(executable)) {
193
- throw new Error(`voidx executable not found in ${venvDir}.`);
194
- }
195
- return;
196
- }
197
-
198
- const markerPath = path.join(venvDir, ".voidx-install-version");
199
- const marker = `${pkg.version}\n${PBS_TAG}\n${PBS_CPYTHON}\n`;
200
- if (fs.existsSync(executable) && readFile(markerPath) === marker) {
201
- debug(env, `Using cached environment at ${venvDir}`);
202
- return;
203
- }
204
-
205
- // v2 → v3 migration: clean up legacy data before first v3 run
206
- runV2CleanupIfNeeded(venvDir, env);
207
-
208
- fs.mkdirSync(path.dirname(venvDir), { recursive: true });
209
- const venvPython = resolveVenvPython(venvDir);
210
-
211
- // If venv exists but is corrupted (python binary missing), nuke and rebuild
212
- if (fs.existsSync(venvDir) && !fs.existsSync(venvPython)) {
213
- console.error(" Existing venv is corrupted, rebuilding…");
214
- try {
215
- fs.rmSync(venvDir, { recursive: true, force: true });
216
- } catch (err) {
217
- console.error(` Failed to remove corrupted venv: ${err.message}`);
218
- }
219
- }
220
-
221
- const isFresh = !fs.existsSync(venvPython);
222
- if (isFresh) {
223
- console.error(
224
- "\n⚙️ Setting up voidx environment (this only happens once)...\n"
225
- );
226
- const venvResult = spawnSync(
227
- python.command,
228
- [...python.args, "-m", "venv", venvDir],
229
- { encoding: "utf8", stdio: "inherit", windowsHide: true }
230
- );
231
- if (venvResult.error) {
232
- throw new Error(
233
- `Failed to create the Python virtual environment: ${venvResult.error.message}`
234
- );
235
- }
236
- if (venvResult.status !== 0) {
237
- throw new Error(
238
- "Failed to create the Python virtual environment. See errors above."
239
- );
240
- }
241
- }
242
-
243
- if (!fs.existsSync(executable) || readFile(markerPath) !== marker) {
244
- const packageSpec = env.VOIDX_NPM_PACKAGE_SPEC || `voidx==${pkg.version}`;
245
- console.error(
246
- `\n📦 Downloading ${packageSpec} and dependencies… ` +
247
- "(1–2 minutes on first run)\n"
248
- );
249
-
250
- // Upgrade pip first to avoid resolver bugs
251
- const pipUpgradeEnv = Object.assign({}, env, {
252
- PIP_NO_INPUT: "1",
253
- PIP_DISABLE_PIP_VERSION_CHECK: "1",
254
- PYTHON_KEYRING_BACKEND: "keyring.backends.null.Keyring",
255
- });
256
- const pipUpgradeResult = spawnSync(
257
- venvPython,
258
- ["-m", "pip", "install", "--upgrade", "pip", "--no-cache-dir"],
259
- { encoding: "utf8", stdio: "inherit", windowsHide: true, env: pipUpgradeEnv }
260
- );
261
- if (pipUpgradeResult.error || pipUpgradeResult.status !== 0) {
262
- console.error(" ⚠️ Failed to upgrade pip, continuing with current version…");
263
- }
264
-
265
- const pipEnv = Object.assign({}, env, {
266
- PIP_NO_INPUT: "1",
267
- PIP_DISABLE_PIP_VERSION_CHECK: "1",
268
- PYTHON_KEYRING_BACKEND: "keyring.backends.null.Keyring",
269
- });
270
-
271
- const pipArgs = ["-m", "pip", "install", "--upgrade", "--no-cache-dir", "--progress-bar", "on"];
272
-
273
- // Support custom PyPI index
274
- const pipIndex = env.VOIDX_NPM_PIP_INDEX;
275
- if (pipIndex) {
276
- pipArgs.push("-i", pipIndex);
277
- try {
278
- const indexUrl = new URL(pipIndex);
279
- pipArgs.push("--trusted-host", indexUrl.hostname);
280
- } catch {}
281
- }
282
-
283
- pipArgs.push(packageSpec);
284
-
285
- const result = spawnSync(
286
- venvPython,
287
- pipArgs,
288
- { encoding: "utf8", stdio: "inherit", windowsHide: true, env: pipEnv }
289
- );
290
- if (result.error) {
291
- throw new Error(
292
- `Failed to install ${packageSpec}: ${result.error.message}`
293
- );
294
- }
295
- if (result.status !== 0) {
296
- throw new Error(`Failed to install ${packageSpec}. See errors above.`);
297
- }
298
- }
299
-
300
- fs.writeFileSync(markerPath, marker);
301
- }
302
-
303
- // ── v2 → v3 migration ─────────────────────────────────────────────────────
304
-
305
- function runV2CleanupIfNeeded(venvDir, env) {
306
- const markerPath = path.join(venvDir, ".voidx-install-version");
307
- const oldMarker = readFile(markerPath).trim();
308
- if (!oldMarker) return; // fresh install, nothing to migrate
309
-
310
- const oldVersion = parseVersion(oldMarker.split("\n")[0]);
311
- if (!oldVersion) return;
312
-
313
- // Only run cleanup when upgrading from < 3.0.0
314
- const v3 = [3, 0, 0];
315
- if (
316
- oldVersion[0] > v3[0] ||
317
- (oldVersion[0] === v3[0] && oldVersion[1] > v3[1]) ||
318
- (oldVersion[0] === v3[0] && oldVersion[1] === v3[1] && oldVersion[2] >= v3[2])
319
- ) {
320
- return;
321
- }
322
-
323
- console.error("\n🔄 Upgrading from v2 — cleaning up legacy data…\n");
324
-
325
- const venvPython = resolveVenvPython(venvDir);
326
- if (!fs.existsSync(venvPython)) return; // venv not built yet, will clean after install
327
-
328
- // Download and run the cleanup script from GitHub
329
- const scriptUrl =
330
- "https://raw.githubusercontent.com/chikhamx/voidx/master/scripts/clean_v2_data.py";
331
- const tmpScript = path.join(os.tmpdir(), "voidx-clean-v2-data.py");
332
-
333
- try {
334
- const curlResult = spawnSync(
335
- process.platform === "win32" ? "curl.exe" : "curl",
336
- ["-fsSL", scriptUrl, "-o", tmpScript],
337
- { encoding: "utf8", windowsHide: true, timeout: 15000 }
338
- );
339
- if (curlResult.error || curlResult.status !== 0) {
340
- console.error(" ⚠️ Could not download v2 cleanup script, skipping.");
341
- return;
342
- }
343
-
344
- const pyResult = spawnSync(
345
- venvPython,
346
- [tmpScript],
347
- { encoding: "utf8", stdio: "inherit", windowsHide: true, timeout: 30000 }
348
- );
349
- if (pyResult.error || pyResult.status !== 0) {
350
- console.error(" ⚠️ v2 cleanup script failed, continuing anyway.");
351
- }
352
-
353
- try { fs.unlinkSync(tmpScript); } catch {}
354
- } catch (err) {
355
- console.error(` ⚠️ v2 cleanup error: ${err.message}`);
356
- }
357
- }
358
-
359
- // ── Helpers ────────────────────────────────────────────────────────────────
360
-
361
- function readFile(filePath) {
362
- try {
363
- return fs.readFileSync(filePath, "utf8");
364
- } catch {
365
- return "";
366
- }
367
- }
368
-
369
- function debug(env, message) {
370
- if (env.VOIDX_NPM_DEBUG === "1") {
371
- console.error(`[voidx npm] ${message}`);
372
- }
373
- }
374
-
375
- function fail(message) {
376
- console.error(message);
377
- process.exit(1);
378
- }
379
-
380
- if (require.main === module) {
381
- main();
382
- }
383
-
384
- module.exports = {
385
- isCompatible,
386
- parseVersion,
387
- resolveDataHome,
388
- resolveVenvDir,
389
- resolveBundledPythonBin,
390
- selectPython,
391
- };
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { spawn, spawnSync } = require("child_process");
5
+ const fs = require("fs");
6
+ const os = require("os");
7
+ const path = require("path");
8
+
9
+ const pkg = require("../package.json");
10
+
11
+ // ── Configuration ──────────────────────────────────────────────────────────
12
+
13
+ const PBS_TAG = "20260602";
14
+ const PBS_CPYTHON = "3.12.13";
15
+ const PBS_PYTHON_MAJOR = "3.12";
16
+
17
+ // ── Main ───────────────────────────────────────────────────────────────────
18
+
19
+ function main(argv = process.argv.slice(2), env = process.env) {
20
+ try {
21
+ const python = selectPython(env);
22
+ const venvDir = resolveVenvDir(env);
23
+ ensureVenv(python, venvDir, env);
24
+ const executable = resolveVoidxExecutable(venvDir);
25
+ const childEnv = {
26
+ ...env,
27
+ VOIDX_LAUNCHED_BY_NPM: "1",
28
+ VOIDX_NPM_PACKAGE_VERSION: pkg.version,
29
+ };
30
+ const child = spawn(executable, argv, { stdio: "inherit", env: childEnv });
31
+ child.on("exit", (code, signal) => {
32
+ if (signal) {
33
+ process.kill(process.pid, signal);
34
+ return;
35
+ }
36
+ process.exit(code === null ? 1 : code);
37
+ });
38
+ child.on("error", (error) => {
39
+ fail(`Failed to start voidx: ${error.message}`);
40
+ });
41
+ } catch (error) {
42
+ fail(error.message);
43
+ }
44
+ }
45
+
46
+ // ── Python selection ───────────────────────────────────────────────────────
47
+
48
+ function selectPython(env) {
49
+ // 1. Explicit override (for advanced users / debugging)
50
+ const explicit = env.VOIDX_PYTHON;
51
+ if (explicit) {
52
+ const candidate = { command: explicit, args: [], label: explicit };
53
+ const probe = probePython(candidate);
54
+ if (!probe.ok) {
55
+ throw new Error(probe.reason || `Unable to run Python at ${explicit}.`);
56
+ }
57
+ if (!isCompatible(probe.version)) {
58
+ throw new Error(
59
+ `voidx requires Python 3.11+. Found Python ${probe.versionText} at ${explicit}.`
60
+ );
61
+ }
62
+ return candidate;
63
+ }
64
+
65
+ // 2. Bundled Python only — voidx runs in its own isolated environment
66
+ const bundledBin = resolveBundledPythonBin(env);
67
+ if (bundledBin && fs.existsSync(bundledBin)) {
68
+ const candidate = { command: bundledBin, args: [], label: "bundled" };
69
+ const probe = probePython(candidate);
70
+ if (probe.ok && isCompatible(probe.version)) {
71
+ return candidate;
72
+ }
73
+ }
74
+
75
+ // 3. Bundled Python not found — try to bootstrap it (postinstall may have failed)
76
+ console.error("\n⚙️ Bundled Python not found, running setup…\n");
77
+ const postinstallScript = path.join(path.dirname(__filename), "postinstall.js");
78
+ if (fs.existsSync(postinstallScript)) {
79
+ const result = spawnSync(process.execPath, [postinstallScript], {
80
+ stdio: "inherit",
81
+ windowsHide: true,
82
+ env: { ...env },
83
+ });
84
+ if (result.status !== 0) {
85
+ console.error(" Setup failed. Try reinstalling:");
86
+ console.error(" npm install -g @chikhamx/voidx");
87
+ }
88
+ }
89
+
90
+ // Retry after bootstrap
91
+ if (bundledBin && fs.existsSync(bundledBin)) {
92
+ const candidate = { command: bundledBin, args: [], label: "bundled" };
93
+ const probe = probePython(candidate);
94
+ if (probe.ok && isCompatible(probe.version)) {
95
+ return candidate;
96
+ }
97
+ }
98
+
99
+ throw new Error(
100
+ "voidx bundled Python not found. Reinstall to set up the isolated runtime:\n" +
101
+ " npm install -g @chikhamx/voidx\n" +
102
+ " or: curl -fsSL https://raw.githubusercontent.com/chikhamx/voidx/master/scripts/install.sh | bash"
103
+ );
104
+ }
105
+
106
+ function probePython(candidate) {
107
+ const code = [
108
+ "import sys",
109
+ "print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')",
110
+ ].join("; ");
111
+ const result = spawnSync(candidate.command, [...candidate.args, "-c", code], {
112
+ encoding: "utf8",
113
+ windowsHide: true,
114
+ });
115
+ if (result.error) {
116
+ return { ok: false, reason: result.error.message };
117
+ }
118
+ if (result.status !== 0) {
119
+ return { ok: false, reason: (result.stderr || "").trim() };
120
+ }
121
+ const versionText = (result.stdout || "").trim();
122
+ const version = parseVersion(versionText);
123
+ if (!version) {
124
+ return { ok: false, reason: `Unable to parse Python version: ${versionText}` };
125
+ }
126
+ return { ok: true, version, versionText };
127
+ }
128
+
129
+ function parseVersion(value) {
130
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value);
131
+ if (!match) {
132
+ return null;
133
+ }
134
+ return match.slice(1).map((part) => Number.parseInt(part, 10));
135
+ }
136
+
137
+ function isCompatible(version) {
138
+ return version[0] > 3 || (version[0] === 3 && version[1] >= 11);
139
+ }
140
+
141
+ // ── Paths ──────────────────────────────────────────────────────────────────
142
+
143
+ function resolveDataHome(env) {
144
+ if (env.VOIDX_NPM_HOME) {
145
+ return path.resolve(env.VOIDX_NPM_HOME);
146
+ }
147
+ if (process.platform === "win32") {
148
+ return env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
149
+ }
150
+ return env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share");
151
+ }
152
+
153
+ function resolvePythonDir(env) {
154
+ if (env.VOIDX_NPM_PYTHON_DIR) {
155
+ return path.resolve(env.VOIDX_NPM_PYTHON_DIR);
156
+ }
157
+ return path.join(resolveDataHome(env), "voidx", "python");
158
+ }
159
+
160
+ function resolveBundledPythonBin(env) {
161
+ const pythonDir = resolvePythonDir(env);
162
+ return process.platform === "win32"
163
+ ? path.join(pythonDir, "python", "python.exe")
164
+ : path.join(pythonDir, "python", "bin", "python3");
165
+ }
166
+
167
+ function resolveVenvDir(env) {
168
+ if (env.VOIDX_NPM_VENV) {
169
+ return path.resolve(env.VOIDX_NPM_VENV);
170
+ }
171
+ // Use the same venv directory as install.sh — single environment, no duplicates
172
+ return path.join(resolveDataHome(env), "voidx", "venv");
173
+ }
174
+
175
+ function resolveVenvPython(venvDir) {
176
+ return process.platform === "win32"
177
+ ? path.join(venvDir, "Scripts", "python.exe")
178
+ : path.join(venvDir, "bin", "python");
179
+ }
180
+
181
+ function resolveVoidxExecutable(venvDir) {
182
+ return process.platform === "win32"
183
+ ? path.join(venvDir, "Scripts", "voidx.exe")
184
+ : path.join(venvDir, "bin", "voidx");
185
+ }
186
+
187
+ // ── Venv setup ─────────────────────────────────────────────────────────────
188
+
189
+ function ensureVenv(python, venvDir, env) {
190
+ const executable = resolveVoidxExecutable(venvDir);
191
+ if (env.VOIDX_NPM_SKIP_BOOTSTRAP === "1") {
192
+ if (!fs.existsSync(executable)) {
193
+ throw new Error(`voidx executable not found in ${venvDir}.`);
194
+ }
195
+ return;
196
+ }
197
+
198
+ const markerPath = path.join(venvDir, ".voidx-install-version");
199
+ const marker = `${pkg.version}\n${PBS_TAG}\n${PBS_CPYTHON}\n`;
200
+ if (fs.existsSync(executable) && readFile(markerPath) === marker) {
201
+ debug(env, `Using cached environment at ${venvDir}`);
202
+ return;
203
+ }
204
+
205
+ // v2 → v3 migration: clean up legacy data before first v3 run
206
+ runV2CleanupIfNeeded(venvDir, env);
207
+
208
+ fs.mkdirSync(path.dirname(venvDir), { recursive: true });
209
+ const venvPython = resolveVenvPython(venvDir);
210
+
211
+ // If venv exists but is corrupted (python binary missing), nuke and rebuild
212
+ if (fs.existsSync(venvDir) && !fs.existsSync(venvPython)) {
213
+ console.error(" Existing venv is corrupted, rebuilding…");
214
+ try {
215
+ fs.rmSync(venvDir, { recursive: true, force: true });
216
+ } catch (err) {
217
+ console.error(` Failed to remove corrupted venv: ${err.message}`);
218
+ }
219
+ }
220
+
221
+ const isFresh = !fs.existsSync(venvPython);
222
+ if (isFresh) {
223
+ console.error(
224
+ "\n⚙️ Setting up voidx environment (this only happens once)...\n"
225
+ );
226
+ const venvResult = spawnSync(
227
+ python.command,
228
+ [...python.args, "-m", "venv", venvDir],
229
+ { encoding: "utf8", stdio: "inherit", windowsHide: true }
230
+ );
231
+ if (venvResult.error) {
232
+ throw new Error(
233
+ `Failed to create the Python virtual environment: ${venvResult.error.message}`
234
+ );
235
+ }
236
+ if (venvResult.status !== 0) {
237
+ throw new Error(
238
+ "Failed to create the Python virtual environment. See errors above."
239
+ );
240
+ }
241
+ }
242
+
243
+ if (!fs.existsSync(executable) || readFile(markerPath) !== marker) {
244
+ const packageSpec = env.VOIDX_NPM_PACKAGE_SPEC || `voidx==${pkg.version}`;
245
+ console.error(
246
+ `\n📦 Downloading ${packageSpec} and dependencies… ` +
247
+ "(1–2 minutes on first run)\n"
248
+ );
249
+
250
+ // Upgrade pip first to avoid resolver bugs
251
+ const pipUpgradeEnv = Object.assign({}, env, {
252
+ PIP_NO_INPUT: "1",
253
+ PIP_DISABLE_PIP_VERSION_CHECK: "1",
254
+ PYTHON_KEYRING_BACKEND: "keyring.backends.null.Keyring",
255
+ });
256
+ const pipUpgradeResult = spawnSync(
257
+ venvPython,
258
+ ["-m", "pip", "install", "--upgrade", "pip", "--no-cache-dir"],
259
+ { encoding: "utf8", stdio: "inherit", windowsHide: true, env: pipUpgradeEnv }
260
+ );
261
+ if (pipUpgradeResult.error || pipUpgradeResult.status !== 0) {
262
+ console.error(" ⚠️ Failed to upgrade pip, continuing with current version…");
263
+ }
264
+
265
+ const pipEnv = Object.assign({}, env, {
266
+ PIP_NO_INPUT: "1",
267
+ PIP_DISABLE_PIP_VERSION_CHECK: "1",
268
+ PYTHON_KEYRING_BACKEND: "keyring.backends.null.Keyring",
269
+ });
270
+
271
+ const pipArgs = ["-m", "pip", "install", "--upgrade", "--no-cache-dir", "--progress-bar", "on"];
272
+
273
+ // Support custom PyPI index
274
+ const pipIndex = env.VOIDX_NPM_PIP_INDEX;
275
+ if (pipIndex) {
276
+ pipArgs.push("-i", pipIndex);
277
+ try {
278
+ const indexUrl = new URL(pipIndex);
279
+ pipArgs.push("--trusted-host", indexUrl.hostname);
280
+ } catch {}
281
+ }
282
+
283
+ pipArgs.push(packageSpec);
284
+
285
+ const result = spawnSync(
286
+ venvPython,
287
+ pipArgs,
288
+ { encoding: "utf8", stdio: "inherit", windowsHide: true, env: pipEnv }
289
+ );
290
+ if (result.error) {
291
+ throw new Error(
292
+ `Failed to install ${packageSpec}: ${result.error.message}`
293
+ );
294
+ }
295
+ if (result.status !== 0) {
296
+ throw new Error(`Failed to install ${packageSpec}. See errors above.`);
297
+ }
298
+ }
299
+
300
+ fs.writeFileSync(markerPath, marker);
301
+ }
302
+
303
+ // ── v2 → v3 migration ─────────────────────────────────────────────────────
304
+
305
+ function runV2CleanupIfNeeded(venvDir, env) {
306
+ const markerPath = path.join(venvDir, ".voidx-install-version");
307
+ const oldMarker = readFile(markerPath).trim();
308
+ if (!oldMarker) return; // fresh install, nothing to migrate
309
+
310
+ const oldVersion = parseVersion(oldMarker.split("\n")[0]);
311
+ if (!oldVersion) return;
312
+
313
+ // Only run cleanup when upgrading from < 3.0.0
314
+ const v3 = [3, 0, 0];
315
+ if (
316
+ oldVersion[0] > v3[0] ||
317
+ (oldVersion[0] === v3[0] && oldVersion[1] > v3[1]) ||
318
+ (oldVersion[0] === v3[0] && oldVersion[1] === v3[1] && oldVersion[2] >= v3[2])
319
+ ) {
320
+ return;
321
+ }
322
+
323
+ console.error("\n🔄 Upgrading from v2 — cleaning up legacy data…\n");
324
+
325
+ const venvPython = resolveVenvPython(venvDir);
326
+ if (!fs.existsSync(venvPython)) return; // venv not built yet, will clean after install
327
+
328
+ // Download and run the cleanup script from GitHub
329
+ const scriptUrl =
330
+ "https://raw.githubusercontent.com/chikhamx/voidx/master/scripts/clean_v2_data.py";
331
+ const tmpScript = path.join(os.tmpdir(), "voidx-clean-v2-data.py");
332
+
333
+ try {
334
+ const curlResult = spawnSync(
335
+ process.platform === "win32" ? "curl.exe" : "curl",
336
+ ["-fsSL", scriptUrl, "-o", tmpScript],
337
+ { encoding: "utf8", windowsHide: true, timeout: 15000 }
338
+ );
339
+ if (curlResult.error || curlResult.status !== 0) {
340
+ console.error(" ⚠️ Could not download v2 cleanup script, skipping.");
341
+ return;
342
+ }
343
+
344
+ const pyResult = spawnSync(
345
+ venvPython,
346
+ [tmpScript],
347
+ { encoding: "utf8", stdio: "inherit", windowsHide: true, timeout: 30000 }
348
+ );
349
+ if (pyResult.error || pyResult.status !== 0) {
350
+ console.error(" ⚠️ v2 cleanup script failed, continuing anyway.");
351
+ }
352
+
353
+ try { fs.unlinkSync(tmpScript); } catch {}
354
+ } catch (err) {
355
+ console.error(` ⚠️ v2 cleanup error: ${err.message}`);
356
+ }
357
+ }
358
+
359
+ // ── Helpers ────────────────────────────────────────────────────────────────
360
+
361
+ function readFile(filePath) {
362
+ try {
363
+ return fs.readFileSync(filePath, "utf8");
364
+ } catch {
365
+ return "";
366
+ }
367
+ }
368
+
369
+ function debug(env, message) {
370
+ if (env.VOIDX_NPM_DEBUG === "1") {
371
+ console.error(`[voidx npm] ${message}`);
372
+ }
373
+ }
374
+
375
+ function fail(message) {
376
+ console.error(message);
377
+ process.exit(1);
378
+ }
379
+
380
+ if (require.main === module) {
381
+ main();
382
+ }
383
+
384
+ module.exports = {
385
+ isCompatible,
386
+ parseVersion,
387
+ resolveDataHome,
388
+ resolveVenvDir,
389
+ resolveBundledPythonBin,
390
+ selectPython,
391
+ };