infsh 0.0.2 → 1.19.5
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 +304 -0
- package/package.json +34 -1
- package/run.js +53 -0
- package/index.js +0 -3
package/install.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const http = require("http");
|
|
4
|
+
const https = require("https");
|
|
5
|
+
const tls = require("tls");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
const zlib = require("zlib");
|
|
10
|
+
const crypto = require("crypto");
|
|
11
|
+
|
|
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";
|
|
15
|
+
|
|
16
|
+
const PLATFORM = { darwin: "darwin", linux: "linux", win32: "windows" }[
|
|
17
|
+
process.platform
|
|
18
|
+
];
|
|
19
|
+
const ARCH = { x64: "amd64", arm64: "arm64" }[process.arch];
|
|
20
|
+
|
|
21
|
+
if (!PLATFORM || !ARCH) {
|
|
22
|
+
console.error(`Unsupported platform: ${process.platform}/${process.arch}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const KEY = `${PLATFORM}-${ARCH}`;
|
|
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).
|
|
41
|
+
function getInstallDir() {
|
|
42
|
+
if (process.platform === "win32") {
|
|
43
|
+
return path.join(os.homedir(), ".local", "bin");
|
|
44
|
+
}
|
|
45
|
+
if (process.getuid && process.getuid() === 0) {
|
|
46
|
+
return "/usr/local/bin";
|
|
47
|
+
}
|
|
48
|
+
return path.join(os.homedir(), ".local", "bin");
|
|
49
|
+
}
|
|
50
|
+
|
|
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) {
|
|
87
|
+
return new Promise((resolve, 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);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Extract a single file matching `prefix` from a .zip buffer.
|
|
151
|
+
// ZIP central directory is at the end of the file — parse it to find entries.
|
|
152
|
+
function extractFromZip(buf, prefix) {
|
|
153
|
+
// Find End of Central Directory record (signature 0x06054b50)
|
|
154
|
+
let eocdOffset = -1;
|
|
155
|
+
for (let i = buf.length - 22; i >= 0; i--) {
|
|
156
|
+
if (buf.readUInt32LE(i) === 0x06054b50) {
|
|
157
|
+
eocdOffset = i;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (eocdOffset === -1) throw new Error("invalid zip: EOCD not found");
|
|
162
|
+
|
|
163
|
+
const cdOffset = buf.readUInt32LE(eocdOffset + 16);
|
|
164
|
+
const cdEntries = buf.readUInt16LE(eocdOffset + 10);
|
|
165
|
+
|
|
166
|
+
let pos = cdOffset;
|
|
167
|
+
for (let i = 0; i < cdEntries; i++) {
|
|
168
|
+
if (buf.readUInt32LE(pos) !== 0x02014b50) throw new Error("invalid zip: bad central dir");
|
|
169
|
+
const nameLen = buf.readUInt16LE(pos + 28);
|
|
170
|
+
const extraLen = buf.readUInt16LE(pos + 30);
|
|
171
|
+
const commentLen = buf.readUInt16LE(pos + 32);
|
|
172
|
+
const localOffset = buf.readUInt32LE(pos + 42);
|
|
173
|
+
const name = buf.toString("utf8", pos + 46, pos + 46 + nameLen);
|
|
174
|
+
|
|
175
|
+
if (name.startsWith(prefix)) {
|
|
176
|
+
// Read local file header at localOffset (signature 0x04034b50)
|
|
177
|
+
const lh = localOffset;
|
|
178
|
+
const lhNameLen = buf.readUInt16LE(lh + 26);
|
|
179
|
+
const lhExtraLen = buf.readUInt16LE(lh + 28);
|
|
180
|
+
const compMethod = buf.readUInt16LE(lh + 8);
|
|
181
|
+
const compSize = buf.readUInt32LE(lh + 18);
|
|
182
|
+
const dataStart = lh + 30 + lhNameLen + lhExtraLen;
|
|
183
|
+
const compressed = buf.subarray(dataStart, dataStart + compSize);
|
|
184
|
+
|
|
185
|
+
if (compMethod === 0) return { name, data: compressed };
|
|
186
|
+
if (compMethod === 8) return { name, data: zlib.inflateRawSync(compressed) };
|
|
187
|
+
throw new Error(`unsupported zip compression method: ${compMethod}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
pos += 46 + nameLen + extraLen + commentLen;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Extract a single file matching `prefix` from a .tar.gz buffer.
|
|
196
|
+
function extractFromTarGz(buf, prefix) {
|
|
197
|
+
const tar = zlib.gunzipSync(buf);
|
|
198
|
+
let offset = 0;
|
|
199
|
+
while (offset < tar.length - 512) {
|
|
200
|
+
const header = tar.subarray(offset, offset + 512);
|
|
201
|
+
if (header[0] === 0) break; // end of archive
|
|
202
|
+
|
|
203
|
+
const name = header.toString("utf8", 0, 100).replace(/\0.*/, "");
|
|
204
|
+
const sizeStr = header.toString("utf8", 124, 136).replace(/\0.*/, "").trim();
|
|
205
|
+
const size = parseInt(sizeStr, 8) || 0;
|
|
206
|
+
const dataStart = offset + 512;
|
|
207
|
+
|
|
208
|
+
if (name.startsWith(prefix)) {
|
|
209
|
+
return { name, data: tar.subarray(dataStart, dataStart + size) };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Advance past header + data (rounded up to 512-byte blocks)
|
|
213
|
+
offset = dataStart + Math.ceil(size / 512) * 512;
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
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
|
+
|
|
245
|
+
async function main() {
|
|
246
|
+
console.log(`Downloading belt for ${PLATFORM}/${ARCH}...`);
|
|
247
|
+
|
|
248
|
+
const manifest = JSON.parse(await fetch(MANIFEST_URL));
|
|
249
|
+
const selected = selectBuild(manifest.builds || {});
|
|
250
|
+
if (!selected) {
|
|
251
|
+
console.error(`No build found for ${KEY}`);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
const { build } = selected;
|
|
255
|
+
|
|
256
|
+
const archive = await fetch(build.url);
|
|
257
|
+
|
|
258
|
+
// Verify checksum
|
|
259
|
+
const actual = crypto.createHash("sha256").update(archive).digest("hex");
|
|
260
|
+
if (build.sha256 && actual !== build.sha256) {
|
|
261
|
+
console.error(`Checksum mismatch!\n expected: ${build.sha256}\n got: ${actual}`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Extract binary — pure Node, no shell commands, no MSYS path mangling
|
|
266
|
+
const entry = build.url.endsWith(".zip")
|
|
267
|
+
? extractFromZip(archive, "inferencesh-cli")
|
|
268
|
+
: extractFromTarGz(archive, "inferencesh-cli");
|
|
269
|
+
|
|
270
|
+
if (!entry) {
|
|
271
|
+
console.error("Binary not found in archive");
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
|
|
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.
|
|
279
|
+
const ext = PLATFORM === "windows" ? ".exe" : "";
|
|
280
|
+
const npmBin = path.join(__dirname, "bin");
|
|
281
|
+
fs.mkdirSync(npmBin, { recursive: true });
|
|
282
|
+
installFile(path.join(npmBin, `belt${ext}`), entry.data);
|
|
283
|
+
|
|
284
|
+
console.log(`Installed belt ${manifest.version}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (require.main === module) {
|
|
288
|
+
main().catch((err) => {
|
|
289
|
+
console.error("belt installation failed:", err.message);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
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 +1,34 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "infsh",
|
|
3
|
+
"version": "1.19.5",
|
|
4
|
+
"description": "inference.sh CLI — run AI apps, manage skills, connect MCP servers",
|
|
5
|
+
"homepage": "https://inference.sh",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/inference-sh/cli.git"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"bin": {
|
|
12
|
+
"belt": "run.js",
|
|
13
|
+
"infsh": "run.js",
|
|
14
|
+
"inferencesh": "run.js"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"postinstall": "node install.js",
|
|
18
|
+
"test": "node --test install.test.js run.test.js package.test.js"
|
|
19
|
+
},
|
|
20
|
+
"os": [
|
|
21
|
+
"darwin",
|
|
22
|
+
"linux",
|
|
23
|
+
"win32"
|
|
24
|
+
],
|
|
25
|
+
"cpu": [
|
|
26
|
+
"x64",
|
|
27
|
+
"arm64"
|
|
28
|
+
],
|
|
29
|
+
"files": [
|
|
30
|
+
"install.js",
|
|
31
|
+
"run.js",
|
|
32
|
+
"bin/"
|
|
33
|
+
]
|
|
34
|
+
}
|
package/run.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require("child_process");
|
|
4
|
+
const { existsSync } = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { getInstallDir, main: install } = require("./install.js");
|
|
7
|
+
|
|
8
|
+
const ext = process.platform === "win32" ? ".exe" : "";
|
|
9
|
+
|
|
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
|
+
}
|
|
18
|
+
|
|
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();
|
package/index.js
DELETED