@chikhamx/voidx 3.6.0 → 3.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.
@@ -4,14 +4,19 @@
4
4
  // Post-install: download a standalone Python, create venv, pip install voidx.
5
5
  // Uses only the bundled Python — never falls back to the system Python.
6
6
 
7
- const { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, rmSync } = require("fs");
7
+ const { createWriteStream, existsSync, mkdirSync, readFileSync, unlinkSync, rmSync } = require("fs");
8
8
  const { spawnSync } = require("child_process");
9
- const { join, dirname } = require("path");
9
+ const { join } = require("path");
10
10
  const os = require("os");
11
- const fs = require("fs");
12
11
  const path = require("path");
13
12
 
14
13
  const pkg = require("../package.json");
14
+ const {
15
+ installVerifyAndRepair,
16
+ resolveBundledCliWheel,
17
+ verifyPair,
18
+ writeMarkerAtomic,
19
+ } = require("./runtime-install");
15
20
 
16
21
  // ── Python build metadata ──────────────────────────────────────────────────
17
22
  // Pin to a specific python-build-standalone release tag and CPython version.
@@ -248,13 +253,8 @@ function readMarker(markerPath) {
248
253
  try { return readFileSync(markerPath, "utf8"); } catch { return ""; }
249
254
  }
250
255
 
251
- function writeMarker(markerPath, content) {
252
- mkdirSync(dirname(markerPath), { recursive: true });
253
- writeFileSync(markerPath, content);
254
- }
255
-
256
- // ── pip install ────────────────────────────────────────────────────────────
257
- function pipInstall(venvPython, packageSpec, env) {
256
+ // ── pip setup ──────────────────────────────────────────────────────────────
257
+ function upgradePip(venvPython, env) {
258
258
  // Upgrade pip first to avoid resolver bugs in old versions
259
259
  const pipUpgradeEnv = Object.assign({}, env, {
260
260
  PIP_NO_INPUT: "1",
@@ -269,39 +269,6 @@ function pipInstall(venvPython, packageSpec, env) {
269
269
  if (pipUpgradeResult.error || pipUpgradeResult.status !== 0) {
270
270
  console.error(" ⚠️ Failed to upgrade pip, continuing with current version…");
271
271
  }
272
-
273
- const pipEnv = Object.assign({}, env, {
274
- PIP_NO_INPUT: "1",
275
- PIP_DISABLE_PIP_VERSION_CHECK: "1",
276
- PYTHON_KEYRING_BACKEND: "keyring.backends.null.Keyring",
277
- });
278
-
279
- const pipArgs = ["-m", "pip", "install", "--upgrade", "--no-cache-dir", "--progress-bar", "on"];
280
-
281
- // Support custom PyPI index for users behind firewalls or in regions with slow PyPI access
282
- const pipIndex = env.VOIDX_NPM_PIP_INDEX;
283
- if (pipIndex) {
284
- pipArgs.push("-i", pipIndex);
285
- // Extract host for --trusted-host when using a custom index
286
- try {
287
- const indexUrl = new URL(pipIndex);
288
- pipArgs.push("--trusted-host", indexUrl.hostname);
289
- } catch {}
290
- }
291
-
292
- pipArgs.push(packageSpec);
293
-
294
- const result = spawnSync(
295
- venvPython,
296
- pipArgs,
297
- { encoding: "utf8", stdio: "inherit", windowsHide: true, env: pipEnv }
298
- );
299
- if (result.error) {
300
- throw new Error(`pip install failed: ${result.error.message}`);
301
- }
302
- if (result.status !== 0) {
303
- throw new Error("pip install failed. See errors above.");
304
- }
305
272
  }
306
273
 
307
274
  // ── Main ───────────────────────────────────────────────────────────────────
@@ -322,8 +289,17 @@ async function main() {
322
289
 
323
290
  // Already set up and up-to-date?
324
291
  if (existsSync(executable) && readMarker(markerPath) === marker) {
325
- console.error(`\n✅ voidx ${pkg.version} ready (cached)\n`);
326
- return;
292
+ const verification = verifyPair({
293
+ venvPython,
294
+ executable,
295
+ expectedVersion: pkg.version,
296
+ env,
297
+ });
298
+ if (verification.ok) {
299
+ console.error(`\n✅ voidx ${pkg.version} ready (cached)\n`);
300
+ return;
301
+ }
302
+ console.error(` ⚠️ Cached environment is invalid: ${verification.message}`);
327
303
  }
328
304
 
329
305
  // Step 1: Download bundled Python (required — no system Python fallback)
@@ -359,7 +335,7 @@ async function main() {
359
335
  console.error(" Extraction complete.");
360
336
 
361
337
  // Clean up archive to save disk
362
- try { fs.unlinkSync(archivePath); } catch {}
338
+ try { unlinkSync(archivePath); } catch {}
363
339
  } catch (err) {
364
340
  // Clean up partial archive on failure
365
341
  try { unlinkSync(archivePath); } catch {}
@@ -403,23 +379,22 @@ async function main() {
403
379
  }
404
380
  }
405
381
 
406
- // Step 3: pip install voidx (core runtime)
407
- console.error(" [3/4] Installing voidx core…");
408
- pipInstall(venvPython, packageSpec, env);
409
-
410
- // Step 4: pip install bundled voidx_cli wheel (terminal frontend)
411
- console.error(" [4/4] Installing voidx_cli frontend…");
382
+ // Step 3: install and verify the exact core/CLI pair
383
+ console.error(" [3/3] Installing and verifying voidx…");
384
+ upgradePip(venvPython, env);
412
385
  const npmDir = path.resolve(__dirname, "..");
413
- const wheelPattern = `voidx_cli-${pkg.version}-py3-none-any.whl`;
414
- const wheelPath = path.join(npmDir, wheelPattern);
415
- if (existsSync(wheelPath)) {
416
- pipInstall(venvPython, wheelPath, env);
417
- } else {
418
- console.error(` ⚠️ Bundled ${wheelPattern} not found — TUI frontend unavailable`);
419
- }
386
+ const wheelPath = resolveBundledCliWheel(npmDir, pkg.version);
387
+ installVerifyAndRepair({
388
+ venvPython,
389
+ executable,
390
+ coreSpec: packageSpec,
391
+ cliSpec: wheelPath,
392
+ expectedVersion: pkg.version,
393
+ env,
394
+ });
420
395
 
421
396
  // Done
422
- writeMarker(markerPath, marker);
397
+ writeMarkerAtomic(markerPath, marker);
423
398
  console.error(`\n✅ voidx ${pkg.version} installed! Run: voidx\n`);
424
399
  }
425
400
 
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const { spawnSync } = require("child_process");
7
+
8
+ const VERIFY_PROBE = String.raw`
9
+ import importlib
10
+ import json
11
+ import os
12
+ import re
13
+ import subprocess
14
+ import sys
15
+ from importlib.metadata import PackageNotFoundError, version
16
+
17
+ current_directory = os.path.normcase(os.path.realpath(os.getcwd()))
18
+ sys.path = [
19
+ entry
20
+ for entry in sys.path
21
+ if entry and os.path.normcase(os.path.realpath(entry)) != current_directory
22
+ ]
23
+
24
+ def installed_version(name):
25
+ try:
26
+ return version(name)
27
+ except PackageNotFoundError:
28
+ return None
29
+ except Exception:
30
+ return None
31
+
32
+ payload = {
33
+ "core_version": installed_version("voidx"),
34
+ "cli_version": installed_version("voidx-cli"),
35
+ "core_import": False,
36
+ "cli_import": False,
37
+ "entrypoint_ok": False,
38
+ "entrypoint_version": None,
39
+ }
40
+
41
+ try:
42
+ importlib.import_module("voidx")
43
+ payload["core_import"] = True
44
+ except Exception as exc:
45
+ payload["core_error"] = str(exc)
46
+
47
+ try:
48
+ importlib.import_module("voidx_cli")
49
+ payload["cli_import"] = True
50
+ except Exception as exc:
51
+ payload["cli_error"] = str(exc)
52
+
53
+ try:
54
+ completed = subprocess.run(
55
+ [sys.argv[1], "--version"],
56
+ capture_output=True,
57
+ text=True,
58
+ timeout=30,
59
+ )
60
+ output = (completed.stdout or completed.stderr or "").strip()
61
+ payload["entrypoint_ok"] = completed.returncode == 0
62
+ match = re.search(r"\d+\.\d+\.\d+(?:[A-Za-z0-9.+-]*)?", output)
63
+ payload["entrypoint_version"] = match.group(0) if match else None
64
+ except Exception as exc:
65
+ payload["entrypoint_error"] = str(exc)
66
+
67
+ print(json.dumps(payload))
68
+ `;
69
+
70
+ function resolveBundledCliWheel(npmDir, version) {
71
+ const filename = `voidx_cli-${version}-py3-none-any.whl`;
72
+ const wheelPath = path.join(npmDir, filename);
73
+ if (!fs.existsSync(wheelPath)) {
74
+ throw new Error(`Bundled ${filename} not found.`);
75
+ }
76
+ return wheelPath;
77
+ }
78
+
79
+ function pipEnvironment(env) {
80
+ return {
81
+ ...env,
82
+ PIP_NO_INPUT: "1",
83
+ PIP_DISABLE_PIP_VERSION_CHECK: "1",
84
+ PYTHON_KEYRING_BACKEND: "keyring.backends.null.Keyring",
85
+ };
86
+ }
87
+
88
+ function buildPipArgs({ coreSpec, cliSpec, forceReinstall, env }) {
89
+ const args = [
90
+ "-m",
91
+ "pip",
92
+ "install",
93
+ "--upgrade",
94
+ "--no-cache-dir",
95
+ "--progress-bar",
96
+ "on",
97
+ ];
98
+ if (forceReinstall) {
99
+ args.push("--force-reinstall");
100
+ }
101
+ const pipIndex = env.VOIDX_NPM_PIP_INDEX;
102
+ if (pipIndex) {
103
+ args.push("-i", pipIndex);
104
+ try {
105
+ args.push("--trusted-host", new URL(pipIndex).hostname);
106
+ } catch {}
107
+ }
108
+ args.push(coreSpec, cliSpec);
109
+ return args;
110
+ }
111
+
112
+ function installPair({
113
+ venvPython,
114
+ coreSpec,
115
+ cliSpec,
116
+ env,
117
+ forceReinstall = false,
118
+ runner = spawnSync,
119
+ }) {
120
+ const result = runner(
121
+ venvPython,
122
+ buildPipArgs({ coreSpec, cliSpec, forceReinstall, env }),
123
+ {
124
+ encoding: "utf8",
125
+ stdio: "inherit",
126
+ windowsHide: true,
127
+ env: pipEnvironment(env),
128
+ }
129
+ );
130
+ if (result.error) {
131
+ throw new Error(`pip install failed: ${result.error.message}`);
132
+ }
133
+ if (result.status !== 0) {
134
+ throw new Error("pip install failed. See errors above.");
135
+ }
136
+ return result;
137
+ }
138
+
139
+ function verifyPair({
140
+ venvPython,
141
+ executable,
142
+ expectedVersion,
143
+ env,
144
+ runner = spawnSync,
145
+ }) {
146
+ const result = runner(
147
+ venvPython,
148
+ ["-c", VERIFY_PROBE, executable],
149
+ {
150
+ encoding: "utf8",
151
+ windowsHide: true,
152
+ env,
153
+ }
154
+ );
155
+ if (result.error || result.status !== 0) {
156
+ const detail = result.error
157
+ ? result.error.message
158
+ : (result.stderr || result.stdout || `verification exited ${result.status}`).trim();
159
+ return {
160
+ ok: false,
161
+ coreVersion: null,
162
+ cliVersion: null,
163
+ message: `Installation verification failed: ${detail}`,
164
+ };
165
+ }
166
+
167
+ let payload;
168
+ try {
169
+ payload = JSON.parse((result.stdout || "").trim());
170
+ } catch (error) {
171
+ return {
172
+ ok: false,
173
+ coreVersion: null,
174
+ cliVersion: null,
175
+ message: `Installation verification returned invalid data: ${error.message}`,
176
+ };
177
+ }
178
+
179
+ const coreVersion = payload.core_version || null;
180
+ const cliVersion = payload.cli_version || null;
181
+ const failures = [];
182
+ if (!coreVersion) failures.push("voidx is not installed");
183
+ if (!cliVersion) failures.push("voidx-cli is not installed");
184
+ if (!payload.core_import) failures.push("voidx is not importable");
185
+ if (!payload.cli_import) failures.push("voidx-cli is not importable");
186
+ if (!payload.entrypoint_ok) failures.push("voidx entry point failed");
187
+ if (coreVersion && cliVersion && coreVersion !== cliVersion) {
188
+ failures.push(`package versions differ (${coreVersion} != ${cliVersion})`);
189
+ }
190
+ if (coreVersion && payload.entrypoint_version !== coreVersion) {
191
+ failures.push(
192
+ `entry point version differs (${payload.entrypoint_version || "missing"} != ${coreVersion})`
193
+ );
194
+ }
195
+ if (coreVersion !== expectedVersion) {
196
+ failures.push(`voidx version is ${coreVersion || "missing"}, expected ${expectedVersion}`);
197
+ }
198
+ if (cliVersion !== expectedVersion) {
199
+ failures.push(`voidx-cli version is ${cliVersion || "missing"}, expected ${expectedVersion}`);
200
+ }
201
+
202
+ return {
203
+ ok: failures.length === 0,
204
+ coreVersion,
205
+ cliVersion,
206
+ message: failures.length === 0
207
+ ? `Verified voidx and voidx-cli ${expectedVersion}`
208
+ : failures.join("; "),
209
+ };
210
+ }
211
+
212
+ function installVerifyAndRepair(options) {
213
+ const installFn = options.installFn || installPair;
214
+ const verifyFn = options.verifyFn || verifyPair;
215
+ let initialInstallError = null;
216
+ try {
217
+ installFn({ ...options, forceReinstall: false });
218
+ } catch (error) {
219
+ initialInstallError = error;
220
+ }
221
+ let verification = verifyFn(options);
222
+ if (verification.ok) {
223
+ return verification;
224
+ }
225
+ let repairInstallError = null;
226
+ try {
227
+ installFn({ ...options, forceReinstall: true });
228
+ } catch (error) {
229
+ repairInstallError = error;
230
+ }
231
+ verification = verifyFn(options);
232
+ if (!verification.ok) {
233
+ const installError = repairInstallError || initialInstallError;
234
+ const detail = installError
235
+ ? `${verification.message}; ${installError.message}`
236
+ : verification.message;
237
+ throw new Error(detail);
238
+ }
239
+ return verification;
240
+ }
241
+
242
+ function writeMarkerAtomic(markerPath, marker) {
243
+ fs.mkdirSync(path.dirname(markerPath), { recursive: true });
244
+ const temporaryPath = `${markerPath}.${process.pid}.tmp`;
245
+ try {
246
+ fs.writeFileSync(temporaryPath, marker);
247
+ fs.renameSync(temporaryPath, markerPath);
248
+ } finally {
249
+ try {
250
+ fs.unlinkSync(temporaryPath);
251
+ } catch {}
252
+ }
253
+ }
254
+
255
+ module.exports = {
256
+ installPair,
257
+ installVerifyAndRepair,
258
+ resolveBundledCliWheel,
259
+ verifyPair,
260
+ writeMarkerAtomic,
261
+ };
package/bin/voidx.js CHANGED
@@ -7,6 +7,12 @@ const os = require("os");
7
7
  const path = require("path");
8
8
 
9
9
  const pkg = require("../package.json");
10
+ const {
11
+ installVerifyAndRepair,
12
+ resolveBundledCliWheel,
13
+ verifyPair,
14
+ writeMarkerAtomic,
15
+ } = require("./runtime-install");
10
16
 
11
17
  // ── Configuration ──────────────────────────────────────────────────────────
12
18
 
@@ -190,6 +196,7 @@ function resolveVoidxExecutable(venvDir) {
190
196
 
191
197
  function ensureVenv(python, venvDir, env) {
192
198
  const executable = resolveVoidxExecutable(venvDir);
199
+ const venvPython = resolveVenvPython(venvDir);
193
200
  if (env.VOIDX_NPM_SKIP_BOOTSTRAP === "1") {
194
201
  if (!fs.existsSync(executable)) {
195
202
  throw new Error(`voidx executable not found in ${venvDir}.`);
@@ -199,16 +206,26 @@ function ensureVenv(python, venvDir, env) {
199
206
 
200
207
  const markerPath = path.join(venvDir, ".voidx-install-version");
201
208
  const marker = `${pkg.version}\n${PBS_TAG}\n${PBS_CPYTHON}\n`;
202
- if (fs.existsSync(executable) && readFile(markerPath) === marker) {
203
- debug(env, `Using cached environment at ${venvDir}`);
204
- return;
209
+ let needsInstall = !fs.existsSync(executable) || readMarker(markerPath) !== marker;
210
+ if (fs.existsSync(executable) && readMarker(markerPath) === marker) {
211
+ const verification = verifyPair({
212
+ venvPython,
213
+ executable,
214
+ expectedVersion: pkg.version,
215
+ env,
216
+ });
217
+ if (verification.ok) {
218
+ debug(env, `Using cached environment at ${venvDir}`);
219
+ return;
220
+ }
221
+ console.error(` Cached environment is invalid: ${verification.message}`);
222
+ needsInstall = true;
205
223
  }
206
224
 
207
225
  // v2 → v3 migration: clean up legacy data before first v3 run
208
226
  runV2CleanupIfNeeded(venvDir, env);
209
227
 
210
228
  fs.mkdirSync(path.dirname(venvDir), { recursive: true });
211
- const venvPython = resolveVenvPython(venvDir);
212
229
 
213
230
  // If venv exists but is corrupted (python binary missing), nuke and rebuild
214
231
  if (fs.existsSync(venvDir) && !fs.existsSync(venvPython)) {
@@ -242,7 +259,7 @@ function ensureVenv(python, venvDir, env) {
242
259
  }
243
260
  }
244
261
 
245
- if (!fs.existsSync(executable) || readFile(markerPath) !== marker) {
262
+ if (needsInstall) {
246
263
  const packageSpec = env.VOIDX_NPM_PACKAGE_SPEC || `voidx==${pkg.version}`;
247
264
  console.error(
248
265
  `\n📦 Downloading ${packageSpec} and dependencies… ` +
@@ -264,49 +281,26 @@ function ensureVenv(python, venvDir, env) {
264
281
  console.error(" ⚠️ Failed to upgrade pip, continuing with current version…");
265
282
  }
266
283
 
267
- const pipEnv = Object.assign({}, env, {
268
- PIP_NO_INPUT: "1",
269
- PIP_DISABLE_PIP_VERSION_CHECK: "1",
270
- PYTHON_KEYRING_BACKEND: "keyring.backends.null.Keyring",
271
- });
272
-
273
- const pipArgs = ["-m", "pip", "install", "--upgrade", "--no-cache-dir", "--progress-bar", "on"];
274
-
275
- // Support custom PyPI index
276
- const pipIndex = env.VOIDX_NPM_PIP_INDEX;
277
- if (pipIndex) {
278
- pipArgs.push("-i", pipIndex);
279
- try {
280
- const indexUrl = new URL(pipIndex);
281
- pipArgs.push("--trusted-host", indexUrl.hostname);
282
- } catch {}
283
- }
284
-
285
- pipArgs.push(packageSpec);
286
-
287
- const result = spawnSync(
284
+ const npmDir = path.resolve(__dirname, "..");
285
+ const wheelPath = resolveBundledCliWheel(npmDir, pkg.version);
286
+ installVerifyAndRepair({
288
287
  venvPython,
289
- pipArgs,
290
- { encoding: "utf8", stdio: "inherit", windowsHide: true, env: pipEnv }
291
- );
292
- if (result.error) {
293
- throw new Error(
294
- `Failed to install ${packageSpec}: ${result.error.message}`
295
- );
296
- }
297
- if (result.status !== 0) {
298
- throw new Error(`Failed to install ${packageSpec}. See errors above.`);
299
- }
288
+ executable,
289
+ coreSpec: packageSpec,
290
+ cliSpec: wheelPath,
291
+ expectedVersion: pkg.version,
292
+ env,
293
+ });
300
294
  }
301
295
 
302
- fs.writeFileSync(markerPath, marker);
296
+ writeMarkerAtomic(markerPath, marker);
303
297
  }
304
298
 
305
299
  // ── v2 → v3 migration ─────────────────────────────────────────────────────
306
300
 
307
301
  function runV2CleanupIfNeeded(venvDir, env) {
308
302
  const markerPath = path.join(venvDir, ".voidx-install-version");
309
- const oldMarker = readFile(markerPath).trim();
303
+ const oldMarker = readMarker(markerPath).trim();
310
304
  if (!oldMarker) return; // fresh install, nothing to migrate
311
305
 
312
306
  const oldVersion = parseVersion(oldMarker.split("\n")[0]);
@@ -360,7 +354,7 @@ function runV2CleanupIfNeeded(venvDir, env) {
360
354
 
361
355
  // ── Helpers ────────────────────────────────────────────────────────────────
362
356
 
363
- function readFile(filePath) {
357
+ function readMarker(filePath) {
364
358
  try {
365
359
  return fs.readFileSync(filePath, "utf8");
366
360
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chikhamx/voidx",
3
- "version": "3.6.0",
3
+ "version": "3.7.1",
4
4
  "description": "npm launcher for voidx, a terminal AI coding agent.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -31,6 +31,6 @@
31
31
  ],
32
32
  "scripts": {
33
33
  "postinstall": "node bin/postinstall.js",
34
- "check": "node --check bin/voidx.js && node --check bin/postinstall.js"
34
+ "check": "node --check bin/voidx.js && node --check bin/postinstall.js && node --check bin/runtime-install.js"
35
35
  }
36
36
  }
Binary file
Binary file