@chikhamx/voidx 1.1.1 → 2.0.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.
@@ -2,19 +2,14 @@
2
2
  "use strict";
3
3
 
4
4
  // Post-install: download a standalone Python, create venv, pip install voidx.
5
- // Falls back to system Python if download fails.
5
+ // Uses only the bundled Python — never falls back to the system Python.
6
6
 
7
- const { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } = require("fs");
7
+ const { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, rmSync } = require("fs");
8
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
9
  const { join, dirname } = require("path");
14
10
  const os = require("os");
15
11
  const fs = require("fs");
16
12
  const path = require("path");
17
- const { execSync } = require("child_process");
18
13
 
19
14
  const pkg = require("../package.json");
20
15
 
@@ -92,7 +87,32 @@ function resolveBundledPython(pythonDir, platform) {
92
87
  return path.join(installDir, "bin", "python3");
93
88
  }
94
89
 
95
- // ── Download ───────────────────────────────────────────────────────────────
90
+ // ── Download with retry ────────────────────────────────────────────────────
91
+ const MAX_DOWNLOAD_RETRIES = 3;
92
+
93
+ function sleep(ms) {
94
+ return new Promise((resolve) => setTimeout(resolve, ms));
95
+ }
96
+
97
+ async function downloadFileWithRetry(url, dest, retries = MAX_DOWNLOAD_RETRIES) {
98
+ for (let attempt = 1; attempt <= retries; attempt++) {
99
+ try {
100
+ return await downloadFile(url, dest);
101
+ } catch (err) {
102
+ // Clean up partial download
103
+ try { unlinkSync(dest); } catch {}
104
+ if (attempt < retries) {
105
+ const delay = Math.pow(2, attempt) * 1000;
106
+ console.error(` Download attempt ${attempt}/${retries} failed: ${err.message}`);
107
+ console.error(` Retrying in ${delay / 1000}s…`);
108
+ await sleep(delay);
109
+ } else {
110
+ throw err;
111
+ }
112
+ }
113
+ }
114
+ }
115
+
96
116
  function downloadFile(url, dest) {
97
117
  return new Promise((resolve, reject) => {
98
118
  const doRequest = (currentUrl, redirects = 0) => {
@@ -100,7 +120,7 @@ function downloadFile(url, dest) {
100
120
  return reject(new Error(`Too many redirects downloading ${url}`));
101
121
  }
102
122
  const mod = currentUrl.startsWith("https") ? require("https") : require("http");
103
- mod.get(currentUrl, { timeout: 60000 }, (res) => {
123
+ mod.get(currentUrl, { timeout: 30000 }, (res) => {
104
124
  if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
105
125
  return doRequest(res.headers.location, redirects + 1);
106
126
  }
@@ -163,14 +183,45 @@ function writeMarker(markerPath, content) {
163
183
 
164
184
  // ── pip install ────────────────────────────────────────────────────────────
165
185
  function pipInstall(venvPython, packageSpec, env) {
186
+ // Upgrade pip first to avoid resolver bugs in old versions
187
+ const pipUpgradeEnv = Object.assign({}, env, {
188
+ PIP_NO_INPUT: "1",
189
+ PIP_DISABLE_PIP_VERSION_CHECK: "1",
190
+ PYTHON_KEYRING_BACKEND: "keyring.backends.null.Keyring",
191
+ });
192
+ const pipUpgradeResult = spawnSync(
193
+ venvPython,
194
+ ["-m", "pip", "install", "--upgrade", "pip", "--no-cache-dir"],
195
+ { encoding: "utf8", stdio: "inherit", windowsHide: true, env: pipUpgradeEnv }
196
+ );
197
+ if (pipUpgradeResult.error || pipUpgradeResult.status !== 0) {
198
+ console.error(" ⚠️ Failed to upgrade pip, continuing with current version…");
199
+ }
200
+
166
201
  const pipEnv = Object.assign({}, env, {
167
202
  PIP_NO_INPUT: "1",
168
203
  PIP_DISABLE_PIP_VERSION_CHECK: "1",
169
204
  PYTHON_KEYRING_BACKEND: "keyring.backends.null.Keyring",
170
205
  });
206
+
207
+ const pipArgs = ["-m", "pip", "install", "--upgrade", "--no-cache-dir", "--progress-bar", "on"];
208
+
209
+ // Support custom PyPI index for users behind firewalls or in regions with slow PyPI access
210
+ const pipIndex = env.VOIDX_NPM_PIP_INDEX;
211
+ if (pipIndex) {
212
+ pipArgs.push("-i", pipIndex);
213
+ // Extract host for --trusted-host when using a custom index
214
+ try {
215
+ const indexUrl = new URL(pipIndex);
216
+ pipArgs.push("--trusted-host", indexUrl.hostname);
217
+ } catch {}
218
+ }
219
+
220
+ pipArgs.push(packageSpec);
221
+
171
222
  const result = spawnSync(
172
223
  venvPython,
173
- ["-m", "pip", "install", "--upgrade", "--progress-bar", "on", packageSpec],
224
+ pipArgs,
174
225
  { encoding: "utf8", stdio: "inherit", windowsHide: true, env: pipEnv }
175
226
  );
176
227
  if (result.error) {
@@ -181,37 +232,6 @@ function pipInstall(venvPython, packageSpec, env) {
181
232
  }
182
233
  }
183
234
 
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
235
  // ── Main ───────────────────────────────────────────────────────────────────
216
236
  async function main() {
217
237
  const env = process.env;
@@ -228,70 +248,71 @@ async function main() {
228
248
  return;
229
249
  }
230
250
 
231
- // Step 1: Get a Python interpreter (bundled or system)
232
- let pythonForVenv;
251
+ // Step 1: Download bundled Python (required no system Python fallback)
233
252
  const platformInfo = getPlatformInfo();
253
+ if (!platformInfo) {
254
+ console.error(`\n ❌ Unsupported platform: ${os.platform()}-${os.arch()}\n`);
255
+ console.error(" voidx npm package supports: macOS (x64/arm64), Linux (x64/arm64), Windows (x64/arm64)\n");
256
+ process.exit(1);
257
+ }
234
258
 
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 });
259
+ console.error(`\n🐍 Setting up voidx ${pkg.version}…\n`);
260
+ console.error(" [1/3] Downloading Python runtime…");
248
261
 
249
- if (!existsSync(archivePath)) {
250
- console.error(` Downloading ${pbsFilename}…`);
251
- await downloadFile(pbsUrl, archivePath);
252
- console.error(" Download complete.");
253
- }
262
+ const pythonDir = resolvePythonDir(env);
263
+ const pbsFilename = getPbsFilename(platformInfo.target);
264
+ const pbsUrlBase = env.VOIDX_NPM_PYTHON_MIRROR || PBS_RELEASE_BASE;
265
+ const pbsUrl = `${pbsUrlBase}/${PBS_TAG}/${pbsFilename}`;
266
+ const archivePath = path.join(pythonDir, pbsFilename);
267
+ const bundledPython = resolveBundledPython(pythonDir, platformInfo.platform);
254
268
 
255
- console.error(" Extracting Python runtime…");
256
- extractTarGz(archivePath, pythonDir);
257
- console.error(" Extraction complete.");
269
+ if (!existsSync(bundledPython)) {
270
+ try {
271
+ mkdirSync(pythonDir, { recursive: true });
258
272
 
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");
273
+ if (!existsSync(archivePath)) {
274
+ console.error(` Downloading ${pbsFilename}…`);
275
+ await downloadFileWithRetry(pbsUrl, archivePath);
276
+ console.error(" Download complete.");
264
277
  }
265
- } else {
266
- console.error(" Using cached Python runtime.");
267
- }
268
278
 
269
- if (existsSync(bundledPython)) {
270
- pythonForVenv = { command: bundledPython, args: [], label: "bundled" };
279
+ console.error(" Extracting Python runtime…");
280
+ extractTarGz(archivePath, pythonDir);
281
+ console.error(" Extraction complete.");
282
+
283
+ // Clean up archive to save disk
284
+ try { fs.unlinkSync(archivePath); } catch {}
285
+ } catch (err) {
286
+ // Clean up partial archive on failure
287
+ try { unlinkSync(archivePath); } catch {}
288
+ console.error(`\n ❌ Failed to download Python runtime: ${err.message}\n`);
289
+ console.error(" This is usually a network issue. Try:");
290
+ console.error(" 1. Use a mirror: VOIDX_NPM_PYTHON_MIRROR=https://npmmirror.com/mirrors/python-standalone");
291
+ console.error(" 2. Retry: npm install -g @chikhamx/voidx");
292
+ console.error(" 3. Debug: VOIDX_NPM_DEBUG=1 npm install -g @chikhamx/voidx\n");
293
+ process.exit(1);
271
294
  }
295
+ } else {
296
+ console.error(" Using cached Python runtime.");
272
297
  }
273
298
 
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);
299
+ // Step 2: Create venv (rebuild if corrupted)
300
+ console.error(" [2/3] Creating virtual environment…");
301
+
302
+ // If venv exists but is corrupted (python binary missing), nuke and rebuild
303
+ if (existsSync(venvDir) && !existsSync(venvPython)) {
304
+ console.error(" Existing venv is corrupted, rebuilding…");
305
+ try {
306
+ rmSync(venvDir, { recursive: true, force: true });
307
+ } catch (err) {
308
+ console.error(` Failed to remove corrupted venv: ${err.message}`);
286
309
  }
287
310
  }
288
311
 
289
- // Step 2: Create venv
290
- console.error(" [2/3] Creating virtual environment…");
291
312
  if (!existsSync(venvPython)) {
292
313
  const venvResult = spawnSync(
293
- pythonForVenv.command,
294
- [...pythonForVenv.args, "-m", "venv", venvDir],
314
+ bundledPython,
315
+ ["-m", "venv", venvDir],
295
316
  { encoding: "utf8", stdio: "inherit", windowsHide: true }
296
317
  );
297
318
  if (venvResult.error) {
@@ -313,7 +334,18 @@ async function main() {
313
334
  console.error(`\n✅ voidx ${pkg.version} installed! Run: voidx\n`);
314
335
  }
315
336
 
337
+ // ── URL builder (for mirror support) ──────────────────────────────────────
338
+
339
+ function buildPythonDownloadUrl(mirrorBase, tag, filename) {
340
+ return `${mirrorBase}/${tag}/${filename}`;
341
+ }
342
+
316
343
  main().catch((err) => {
317
344
  console.error(`\n❌ Setup failed: ${err.message}\n`);
318
345
  process.exit(1);
319
346
  });
347
+
348
+ module.exports = {
349
+ buildPythonDownloadUrl,
350
+ downloadFileWithRetry,
351
+ };
package/bin/voidx.js CHANGED
@@ -40,7 +40,7 @@ function main(argv = process.argv.slice(2), env = process.env) {
40
40
  // ── Python selection ───────────────────────────────────────────────────────
41
41
 
42
42
  function selectPython(env) {
43
- // 1. Explicit override
43
+ // 1. Explicit override (for advanced users / debugging)
44
44
  const explicit = env.VOIDX_PYTHON;
45
45
  if (explicit) {
46
46
  const candidate = { command: explicit, args: [], label: explicit };
@@ -56,7 +56,7 @@ function selectPython(env) {
56
56
  return candidate;
57
57
  }
58
58
 
59
- // 2. Bundled Python (downloaded by postinstall)
59
+ // 2. Bundled Python (the only normal path)
60
60
  const bundledBin = resolveBundledPythonBin(env);
61
61
  if (bundledBin && fs.existsSync(bundledBin)) {
62
62
  const candidate = { command: bundledBin, args: [], label: "bundled" };
@@ -66,38 +66,11 @@ function selectPython(env) {
66
66
  }
67
67
  }
68
68
 
69
- // 3. System Python
70
- const candidates = [
71
- { command: "python3", args: [], label: "python3" },
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" },
76
- ];
77
- if (process.platform === "win32") {
78
- candidates.push({ command: "py", args: ["-3"], label: "py -3" });
79
- }
80
-
81
- const oldVersions = [];
82
- for (const candidate of candidates) {
83
- const probe = probePython(candidate);
84
- if (!probe.ok) {
85
- continue;
86
- }
87
- if (isCompatible(probe.version)) {
88
- return candidate;
89
- }
90
- oldVersions.push(`${probe.versionText} at ${candidate.label}`);
91
- }
92
-
93
- const hint = pythonHint();
94
- if (oldVersions.length > 0) {
95
- throw new Error(
96
- `voidx requires Python 3.11+. Found ${oldVersions.join(", ")}.\n${hint}`
97
- );
98
- }
69
+ // No system Python fallback — bundled Python is required
99
70
  throw new Error(
100
- `voidx requires Python 3.11+. No Python found.\n${hint}`
71
+ "voidx bundled Python not found.\n" +
72
+ "Reinstall the npm package to set it up:\n" +
73
+ " npm install -g @chikhamx/voidx"
101
74
  );
102
75
  }
103
76
 
@@ -136,22 +109,6 @@ function isCompatible(version) {
136
109
  return version[0] > 3 || (version[0] === 3 && version[1] >= 11);
137
110
  }
138
111
 
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.";
143
- }
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.";
147
- }
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.";
151
- }
152
- return "Install Python 3.11+ or reinstall voidx.";
153
- }
154
-
155
112
  // ── Paths ──────────────────────────────────────────────────────────────────
156
113
 
157
114
  function resolveDataHome(env) {
@@ -220,6 +177,16 @@ function ensureVenv(python, venvDir, env) {
220
177
  fs.mkdirSync(path.dirname(venvDir), { recursive: true });
221
178
  const venvPython = resolveVenvPython(venvDir);
222
179
 
180
+ // If venv exists but is corrupted (python binary missing), nuke and rebuild
181
+ if (fs.existsSync(venvDir) && !fs.existsSync(venvPython)) {
182
+ console.error(" Existing venv is corrupted, rebuilding…");
183
+ try {
184
+ fs.rmSync(venvDir, { recursive: true, force: true });
185
+ } catch (err) {
186
+ console.error(` Failed to remove corrupted venv: ${err.message}`);
187
+ }
188
+ }
189
+
223
190
  const isFresh = !fs.existsSync(venvPython);
224
191
  if (isFresh) {
225
192
  console.error(
@@ -247,22 +214,45 @@ function ensureVenv(python, venvDir, env) {
247
214
  `\n📦 Downloading ${packageSpec} and dependencies… ` +
248
215
  "(1–2 minutes on first run)\n"
249
216
  );
217
+
218
+ // Upgrade pip first to avoid resolver bugs
219
+ const pipUpgradeEnv = Object.assign({}, env, {
220
+ PIP_NO_INPUT: "1",
221
+ PIP_DISABLE_PIP_VERSION_CHECK: "1",
222
+ PYTHON_KEYRING_BACKEND: "keyring.backends.null.Keyring",
223
+ });
224
+ const pipUpgradeResult = spawnSync(
225
+ venvPython,
226
+ ["-m", "pip", "install", "--upgrade", "pip", "--no-cache-dir"],
227
+ { encoding: "utf8", stdio: "inherit", windowsHide: true, env: pipUpgradeEnv }
228
+ );
229
+ if (pipUpgradeResult.error || pipUpgradeResult.status !== 0) {
230
+ console.error(" ⚠️ Failed to upgrade pip, continuing with current version…");
231
+ }
232
+
250
233
  const pipEnv = Object.assign({}, env, {
251
234
  PIP_NO_INPUT: "1",
252
235
  PIP_DISABLE_PIP_VERSION_CHECK: "1",
253
236
  PYTHON_KEYRING_BACKEND: "keyring.backends.null.Keyring",
254
237
  });
238
+
239
+ const pipArgs = ["-m", "pip", "install", "--upgrade", "--no-cache-dir", "--progress-bar", "on"];
240
+
241
+ // Support custom PyPI index
242
+ const pipIndex = env.VOIDX_NPM_PIP_INDEX;
243
+ if (pipIndex) {
244
+ pipArgs.push("-i", pipIndex);
245
+ try {
246
+ const indexUrl = new URL(pipIndex);
247
+ pipArgs.push("--trusted-host", indexUrl.hostname);
248
+ } catch {}
249
+ }
250
+
251
+ pipArgs.push(packageSpec);
252
+
255
253
  const result = spawnSync(
256
254
  venvPython,
257
- [
258
- "-m",
259
- "pip",
260
- "install",
261
- "--upgrade",
262
- "--progress-bar",
263
- "on",
264
- packageSpec,
265
- ],
255
+ pipArgs,
266
256
  { encoding: "utf8", stdio: "inherit", windowsHide: true, env: pipEnv }
267
257
  );
268
258
  if (result.error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chikhamx/voidx",
3
- "version": "1.1.1",
3
+ "version": "2.0.0",
4
4
  "description": "npm launcher for voidx, a terminal AI coding agent.",
5
5
  "bin": {
6
6
  "voidx": "bin/voidx.js"