beltsh 1.18.6 → 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.
- package/install.js +160 -45
- package/package.json +2 -2
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
127
|
-
if (!
|
|
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
|
-
//
|
|
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
|
-
|
|
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}
|
|
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 = {
|
|
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.
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
child
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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();
|