pytxo 1.2.5 → 1.3.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.
package/bin/pytxo.js CHANGED
@@ -3,26 +3,43 @@
3
3
 
4
4
  const { spawnSync } = require("node:child_process");
5
5
  const fs = require("node:fs");
6
- const path = require("node:path");
7
6
 
8
- const binName = process.platform === "win32" ? "pytxo.exe" : "pytxo";
9
- const vendor = path.join(__dirname, "..", "vendor", binName);
10
- const envBin = process.env.PYTXO_BINARY;
7
+ const { dest, install } = require("../postinstall");
11
8
 
12
- const candidates = [envBin, vendor].filter(Boolean);
9
+ function run(bin) {
10
+ const result = spawnSync(bin, process.argv.slice(2), { stdio: "inherit" });
11
+ if (result.error) {
12
+ console.error(result.error.message);
13
+ process.exit(1);
14
+ }
15
+ process.exit(result.status ?? 1);
16
+ }
17
+
18
+ async function main() {
19
+ const envBin = process.env.PYTXO_BINARY;
20
+ if (envBin && fs.existsSync(envBin)) run(envBin);
21
+ if (fs.existsSync(dest)) run(dest);
22
+
23
+ if (process.env.PYTXO_SKIP_DOWNLOAD === "1") {
24
+ console.error("pytxo binary not found. Unset PYTXO_SKIP_DOWNLOAD or set PYTXO_BINARY to a local build.");
25
+ process.exit(1);
26
+ }
13
27
 
14
- for (const bin of candidates) {
15
- if (fs.existsSync(bin)) {
16
- const result = spawnSync(bin, process.argv.slice(2), { stdio: "inherit" });
17
- if (result.error) {
18
- console.error(result.error.message);
28
+ // Install scripts were skipped (pnpm 10 blocks them by default, and so does
29
+ // `npm i --ignore-scripts`): fetch and verify the binary now, once.
30
+ try {
31
+ await install();
32
+ } catch (error) {
33
+ // A concurrent first run may have installed it in the meantime.
34
+ if (!fs.existsSync(dest)) {
35
+ console.error(`pytxo: could not install the pytxo binary: ${error.message}`);
36
+ console.error(
37
+ "pytxo: nothing was installed. Check your network and run pytxo again, or set PYTXO_BINARY to a local build.",
38
+ );
19
39
  process.exit(1);
20
40
  }
21
- process.exit(result.status ?? 1);
22
41
  }
42
+ run(dest);
23
43
  }
24
44
 
25
- console.error(
26
- "pytxo binary not found. Re-run npm install or set PYTXO_BINARY to a local build.",
27
- );
28
- process.exit(1);
45
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pytxo",
3
- "version": "1.2.5",
3
+ "version": "1.3.1",
4
4
  "description": "Pytxo agent hypervisor CLI — local-first PTY orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
package/postinstall.js CHANGED
@@ -15,6 +15,7 @@ const TAG = VERSION.startsWith("v") ? VERSION : `v${VERSION}`;
15
15
  const NORMALIZED_VERSION = VERSION.replace(/^v/, "");
16
16
  const MAX_DOWNLOAD_BYTES = 200 * 1024 * 1024;
17
17
  const MAX_REDIRECTS = 8;
18
+ const DOWNLOAD_IDLE_TIMEOUT_MS = 30_000;
18
19
 
19
20
  const vendorDir = process.env.PYTXO_VENDOR_DIR || path.join(__dirname, "vendor");
20
21
  const destName = process.platform === "win32" ? "pytxo.exe" : "pytxo";
@@ -23,7 +24,7 @@ const dest = path.join(vendorDir, destName);
23
24
  function downloadBuffer(url, redirects = 0) {
24
25
  return new Promise((resolve, reject) => {
25
26
  const transport = new URL(url).protocol === "https:" ? https : http;
26
- transport
27
+ const request = transport
27
28
  .get(url, (res) => {
28
29
  if (res.statusCode === 302 || res.statusCode === 301) {
29
30
  const loc = res.headers.location;
@@ -52,7 +53,11 @@ function downloadBuffer(url, redirects = 0) {
52
53
  res.on("end", () => resolve(Buffer.concat(chunks)));
53
54
  res.on("error", reject);
54
55
  })
55
- .on("error", reject);
56
+ .on("error", (error) => reject(new Error(`could not download ${url}: ${error.message}`)));
57
+ // Without this, a dead network can leave the first `pytxo` run hanging.
58
+ request.setTimeout(DOWNLOAD_IDLE_TIMEOUT_MS, () => {
59
+ request.destroy(new Error(`no response for ${DOWNLOAD_IDLE_TIMEOUT_MS / 1000}s`));
60
+ });
56
61
  });
57
62
  }
58
63
 
@@ -109,11 +114,9 @@ function installAtomically(tempPath, destination) {
109
114
  }
110
115
  }
111
116
 
112
- async function main() {
113
- if (process.env.PYTXO_SKIP_DOWNLOAD === "1") {
114
- console.log("pytxo: PYTXO_SKIP_DOWNLOAD=1, skipping binary download (development only)");
115
- return;
116
- }
117
+ // Shared by postinstall and by bin/pytxo.js, which calls it on first run when
118
+ // install scripts were skipped (pnpm 10, `npm i --ignore-scripts`).
119
+ async function install() {
117
120
  const asset = getAssetName();
118
121
  fs.mkdirSync(vendorDir, { recursive: true });
119
122
  const releaseBase = process.env.PYTXO_RELEASE_BASE_URL
@@ -121,7 +124,7 @@ async function main() {
121
124
  : `https://github.com/${REPO}/releases/download/${TAG}`;
122
125
  const checksumUrl = `${releaseBase}/SHA256SUMS.txt`;
123
126
  const url = `${releaseBase}/${asset}`;
124
- console.log(`pytxo: downloading ${asset} from ${TAG}…`);
127
+ console.error(`pytxo: downloading ${asset} ${TAG} and checking it against SHA256SUMS.txt…`);
125
128
  const checksumText = (await downloadBuffer(checksumUrl)).toString("utf8");
126
129
  const checksum = expectedChecksum(checksumText, asset);
127
130
 
@@ -150,6 +153,15 @@ async function main() {
150
153
  } finally {
151
154
  fs.rmSync(temp, { force: true });
152
155
  }
156
+ console.error(`pytxo: installed ${dest}`);
157
+ }
158
+
159
+ async function main() {
160
+ if (process.env.PYTXO_SKIP_DOWNLOAD === "1") {
161
+ console.log("pytxo: PYTXO_SKIP_DOWNLOAD=1, skipping binary download (development only)");
162
+ return;
163
+ }
164
+ await install();
153
165
  }
154
166
 
155
167
  if (require.main === module) {
@@ -161,7 +173,9 @@ if (require.main === module) {
161
173
 
162
174
  module.exports = {
163
175
  assertChecksum,
176
+ dest,
164
177
  expectedChecksum,
178
+ install,
165
179
  installAtomically,
166
180
  sha256,
167
181
  };