beltsh 1.18.17 → 1.19.4

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/install.js +160 -45
  2. package/package.json +2 -2
  3. package/run.js +43 -14
package/install.js CHANGED
@@ -1,13 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const http = require("http");
3
4
  const https = require("https");
5
+ const tls = require("tls");
4
6
  const fs = require("fs");
5
7
  const path = require("path");
6
8
  const os = require("os");
7
9
  const zlib = require("zlib");
8
10
  const crypto = require("crypto");
9
11
 
10
- const MANIFEST_URL = "https://dist.inference.sh/cli/manifest.json";
12
+ // BELT_MANIFEST_URL points the installer at a mirror of dist.inference.sh.
13
+ const MANIFEST_URL =
14
+ process.env.BELT_MANIFEST_URL || "https://dist.inference.sh/cli/manifest.json";
11
15
 
12
16
  const PLATFORM = { darwin: "darwin", linux: "linux", win32: "windows" }[
13
17
  process.platform
@@ -21,6 +25,19 @@ if (!PLATFORM || !ARCH) {
21
25
 
22
26
  const KEY = `${PLATFORM}-${ARCH}`;
23
27
 
28
+ // Pick the manifest build for `key`. Releases before the native windows-arm64
29
+ // build have no such key; the amd64 build runs under emulation there.
30
+ function selectBuild(builds, key = KEY, log = console.log) {
31
+ if (builds[key]) return { key, build: builds[key] };
32
+ if (key === "windows-arm64" && builds["windows-amd64"]) {
33
+ log("note: no windows-arm64 build in this release, using windows-amd64 under emulation");
34
+ return { key: "windows-amd64", build: builds["windows-amd64"] };
35
+ }
36
+ return null;
37
+ }
38
+
39
+ // Where the curl installer puts the binary; run.js falls back to it when the
40
+ // package's own copy is missing (e.g. --ignore-scripts).
24
41
  function getInstallDir() {
25
42
  if (process.platform === "win32") {
26
43
  return path.join(os.homedir(), ".local", "bin");
@@ -31,22 +48,102 @@ function getInstallDir() {
31
48
  return path.join(os.homedir(), ".local", "bin");
32
49
  }
33
50
 
34
- function fetch(url) {
51
+ // --- proxy support (HTTPS_PROXY / HTTP_PROXY / NO_PROXY), no dependencies ---
52
+
53
+ function defaultPort(u) {
54
+ return Number(u.port) || (u.protocol === "https:" ? 443 : 80);
55
+ }
56
+
57
+ // NO_PROXY: comma-separated hosts or domain suffixes (leading dot optional,
58
+ // port suffix ignored); "*" disables proxying entirely.
59
+ function noProxyMatches(hostname, env = process.env) {
60
+ const raw = env.NO_PROXY || env.no_proxy;
61
+ if (!raw) return false;
62
+ const host = hostname.toLowerCase();
63
+ return raw
64
+ .split(",")
65
+ .map((r) => r.trim().toLowerCase())
66
+ .filter(Boolean)
67
+ .some((rule) => {
68
+ if (rule === "*") return true;
69
+ const r = rule.replace(/:\d+$/, "").replace(/^\./, "");
70
+ return host === r || host.endsWith(`.${r}`);
71
+ });
72
+ }
73
+
74
+ // The proxy URL to use for `target`, or null for a direct connection.
75
+ function proxyFor(target, env = process.env) {
76
+ if (noProxyMatches(target.hostname, env)) return null;
77
+ const raw =
78
+ target.protocol === "https:"
79
+ ? env.HTTPS_PROXY || env.https_proxy
80
+ : env.HTTP_PROXY || env.http_proxy;
81
+ if (!raw) return null;
82
+ return new URL(raw.includes("://") ? raw : `http://${raw}`);
83
+ }
84
+
85
+ // Open a CONNECT tunnel to `target` through `proxy` and resolve the raw socket.
86
+ function connectThroughProxy(proxy, target) {
35
87
  return new Promise((resolve, reject) => {
36
- https
37
- .get(url, (res) => {
38
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
39
- return fetch(res.headers.location).then(resolve, reject);
40
- }
41
- if (res.statusCode !== 200) {
42
- return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
43
- }
44
- const chunks = [];
45
- res.on("data", (c) => chunks.push(c));
46
- res.on("end", () => resolve(Buffer.concat(chunks)));
47
- res.on("error", reject);
48
- })
49
- .on("error", reject);
88
+ const hostPort = `${target.hostname}:${defaultPort(target)}`;
89
+ const headers = { Host: hostPort };
90
+ if (proxy.username) {
91
+ const creds = `${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`;
92
+ headers["Proxy-Authorization"] = `Basic ${Buffer.from(creds).toString("base64")}`;
93
+ }
94
+ const mod = proxy.protocol === "https:" ? https : http;
95
+ const req = mod.request({
96
+ host: proxy.hostname,
97
+ port: defaultPort(proxy),
98
+ method: "CONNECT",
99
+ path: hostPort,
100
+ headers,
101
+ agent: false,
102
+ });
103
+ req.on("connect", (res, socket) => {
104
+ if (res.statusCode !== 200) {
105
+ socket.destroy();
106
+ return reject(new Error(`proxy ${proxy.host} refused CONNECT ${hostPort}: HTTP ${res.statusCode}`));
107
+ }
108
+ resolve(socket);
109
+ });
110
+ req.on("error", (err) => reject(new Error(`proxy ${proxy.host}: ${err.message}`)));
111
+ req.end();
112
+ });
113
+ }
114
+
115
+ // GET `url` following redirects. Honors HTTPS_PROXY / HTTP_PROXY (http or
116
+ // https proxy URL, optional basic auth) via a CONNECT tunnel, and NO_PROXY.
117
+ async function fetch(url, redirects = 0) {
118
+ if (redirects > 10) throw new Error(`too many redirects for ${url}`);
119
+ const target = new URL(url);
120
+ const mod = target.protocol === "https:" ? https : http;
121
+ const options = {};
122
+ const proxy = proxyFor(target);
123
+ if (proxy) {
124
+ const socket = await connectThroughProxy(proxy, target);
125
+ // A createConnection without an agent makes the request use this socket.
126
+ options.createConnection =
127
+ target.protocol === "https:"
128
+ ? () => tls.connect({ socket, servername: target.hostname })
129
+ : () => socket;
130
+ }
131
+ const res = await new Promise((resolve, reject) => {
132
+ mod.get(url, options, resolve).on("error", reject);
133
+ });
134
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
135
+ res.resume();
136
+ return fetch(new URL(res.headers.location, url).href, redirects + 1);
137
+ }
138
+ if (res.statusCode !== 200) {
139
+ res.resume();
140
+ throw new Error(`HTTP ${res.statusCode} for ${url}`);
141
+ }
142
+ return new Promise((resolve, reject) => {
143
+ const chunks = [];
144
+ res.on("data", (c) => chunks.push(c));
145
+ res.on("end", () => resolve(Buffer.concat(chunks)));
146
+ res.on("error", reject);
50
147
  });
51
148
  }
52
149
 
@@ -118,16 +215,43 @@ function extractFromTarGz(buf, prefix) {
118
215
  return null;
119
216
  }
120
217
 
218
+ // Write `data` to `dest` without ever leaving `dest` missing: stage next to it
219
+ // and rename over. Windows refuses to replace a running exe but allows renaming
220
+ // it, so the old file is moved aside first there.
221
+ function installFile(dest, data) {
222
+ const staging = `${dest}.new-${process.pid}`;
223
+ fs.writeFileSync(staging, data, { mode: 0o755 });
224
+ try {
225
+ if (process.platform === "win32" && fs.existsSync(dest)) {
226
+ const old = `${dest}.old`;
227
+ try { fs.unlinkSync(old); } catch {}
228
+ fs.renameSync(dest, old);
229
+ try {
230
+ fs.renameSync(staging, dest);
231
+ } catch (err) {
232
+ fs.renameSync(old, dest);
233
+ throw err;
234
+ }
235
+ try { fs.unlinkSync(old); } catch {}
236
+ } else {
237
+ // rename replaces a symlink itself rather than writing through it.
238
+ fs.renameSync(staging, dest);
239
+ }
240
+ } finally {
241
+ try { fs.unlinkSync(staging); } catch {}
242
+ }
243
+ }
244
+
121
245
  async function main() {
122
- const installDir = getInstallDir();
123
246
  console.log(`Downloading belt for ${PLATFORM}/${ARCH}...`);
124
247
 
125
248
  const manifest = JSON.parse(await fetch(MANIFEST_URL));
126
- const build = manifest.builds[KEY];
127
- if (!build) {
249
+ const selected = selectBuild(manifest.builds || {});
250
+ if (!selected) {
128
251
  console.error(`No build found for ${KEY}`);
129
252
  process.exit(1);
130
253
  }
254
+ const { build } = selected;
131
255
 
132
256
  const archive = await fetch(build.url);
133
257
 
@@ -148,35 +272,16 @@ async function main() {
148
272
  process.exit(1);
149
273
  }
150
274
 
151
- // Install to persistent location
275
+ // Only the package's own bin/ copy is written: npm's bin shims (belt,
276
+ // infsh, inferencesh -> run.js) put it on PATH for `npm i -g`, and npx runs
277
+ // it directly. Writing a second copy to ~/.local/bin left two binaries that
278
+ // self-updated independently and shadowed each other.
152
279
  const ext = PLATFORM === "windows" ? ".exe" : "";
153
- fs.mkdirSync(installDir, { recursive: true });
154
- const dest = path.join(installDir, `belt${ext}`);
155
-
156
- // Remove any existing file/symlink first — copyFileSync follows symlinks,
157
- // which corrupts installs where an older installer left belt as a symlink.
158
- try { fs.unlinkSync(dest); } catch {}
159
- fs.writeFileSync(dest, entry.data, { mode: PLATFORM === "windows" ? undefined : 0o755 });
160
-
161
- // Create aliases (same as curl installer). Symlinks require elevation on
162
- // Windows, so fall back to copying the binary there.
163
- for (const alias of ["infsh", "inferencesh"]) {
164
- const link = path.join(installDir, `${alias}${ext}`);
165
- try { fs.unlinkSync(link); } catch {}
166
- if (PLATFORM === "windows") {
167
- fs.copyFileSync(dest, link);
168
- } else {
169
- fs.symlinkSync(`belt${ext}`, link);
170
- }
171
- }
172
-
173
- // Also install into npm bin/ so run.js works for npx / npm install -g
174
280
  const npmBin = path.join(__dirname, "bin");
175
281
  fs.mkdirSync(npmBin, { recursive: true });
176
- const npmDest = path.join(npmBin, `belt${ext}`);
177
- fs.writeFileSync(npmDest, entry.data, { mode: PLATFORM === "windows" ? undefined : 0o755 });
282
+ installFile(path.join(npmBin, `belt${ext}`), entry.data);
178
283
 
179
- console.log(`Installed belt ${manifest.version} to ${installDir}`);
284
+ console.log(`Installed belt ${manifest.version}`);
180
285
  }
181
286
 
182
287
  if (require.main === module) {
@@ -186,4 +291,14 @@ if (require.main === module) {
186
291
  });
187
292
  }
188
293
 
189
- module.exports = { extractFromZip, extractFromTarGz, getInstallDir };
294
+ module.exports = {
295
+ extractFromZip,
296
+ extractFromTarGz,
297
+ fetch,
298
+ getInstallDir,
299
+ installFile,
300
+ main,
301
+ noProxyMatches,
302
+ proxyFor,
303
+ selectBuild,
304
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beltsh",
3
- "version": "1.18.17",
3
+ "version": "1.19.4",
4
4
  "description": "inference.sh CLI — run AI apps, manage skills, connect MCP servers",
5
5
  "homepage": "https://inference.sh",
6
6
  "repository": {
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "scripts": {
17
17
  "postinstall": "node install.js",
18
- "test": "node --test install.test.js run.test.js"
18
+ "test": "node --test install.test.js run.test.js package.test.js"
19
19
  },
20
20
  "os": [
21
21
  "darwin",
package/run.js CHANGED
@@ -3,22 +3,51 @@
3
3
  const { spawn } = require("child_process");
4
4
  const { existsSync } = require("fs");
5
5
  const path = require("path");
6
+ const { getInstallDir, main: install } = require("./install.js");
6
7
 
7
8
  const ext = process.platform === "win32" ? ".exe" : "";
8
- const bin = path.join(__dirname, "bin", `belt${ext}`);
9
9
 
10
- if (!existsSync(bin)) {
11
- console.error(
12
- "belt binary not found at " + bin + "\n" +
13
- "This can happen on Git Bash/MSYS — install directly instead:\n" +
14
- " curl -fsSL https://dist.inference.sh/install.sh | sh"
15
- );
16
- process.exit(1);
10
+ // The postinstall copy, then the persistent one install.js also writes.
11
+ function findBinary() {
12
+ const candidates = [
13
+ path.join(__dirname, "bin", `belt${ext}`),
14
+ path.join(getInstallDir(), `belt${ext}`),
15
+ ];
16
+ return candidates.find((p) => existsSync(p));
17
17
  }
18
18
 
19
- const child = spawn(bin, process.argv.slice(2), { stdio: "inherit" });
20
- child.on("error", (err) => {
21
- console.error(err.message);
22
- process.exit(1);
23
- });
24
- child.on("exit", (code) => process.exit(code ?? 1));
19
+ function run(bin) {
20
+ const child = spawn(bin, process.argv.slice(2), { stdio: "inherit" });
21
+ child.on("error", (err) => {
22
+ console.error(err.message);
23
+ process.exit(1);
24
+ });
25
+ child.on("exit", (code) => process.exit(code ?? 1));
26
+ }
27
+
28
+ async function start() {
29
+ let bin = findBinary();
30
+ if (!bin) {
31
+ // postinstall did not run (--ignore-scripts) or failed: install now.
32
+ // Its progress goes to stderr so stdout stays clean for the command.
33
+ const log = console.log;
34
+ console.log = console.error;
35
+ try {
36
+ await install();
37
+ } catch (err) {
38
+ console.error(
39
+ "belt binary not found and could not be downloaded: " + err.message + "\n" +
40
+ "install directly instead:\n" +
41
+ " macOS / Linux / Git Bash: curl -fsSL https://cli.inference.sh | sh\n" +
42
+ " Windows PowerShell: irm https://cli.inference.sh/install.ps1 | iex"
43
+ );
44
+ process.exit(1);
45
+ } finally {
46
+ console.log = log;
47
+ }
48
+ bin = findBinary();
49
+ }
50
+ run(bin);
51
+ }
52
+
53
+ start();