@xberg-io/liter-llm-cli 1.9.3 → 1.10.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/liter-llm.js +10 -10
- package/install.js +3 -47
- package/package.json +1 -1
package/bin/liter-llm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Launcher: exec the downloaded native liter-llm binary, forwarding argv and
|
|
3
|
-
// inheriting stdio. If the binary is missing (postinstall failed), download it
|
|
4
|
-
// on demand before exec.
|
|
2
|
+
// ~keep Launcher: exec the downloaded native liter-llm binary, forwarding argv and
|
|
3
|
+
// ~keep inheriting stdio. If the binary is missing (postinstall failed), download it
|
|
4
|
+
// ~keep on demand before exec.
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
@@ -15,11 +15,11 @@ function binaryName() {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
-
// install.js extracts the binary into this same bin/ directory.
|
|
18
|
+
// ~keep install.js extracts the binary into this same bin/ directory.
|
|
19
19
|
const binPath = path.join(__dirname, binaryName());
|
|
20
20
|
|
|
21
|
-
// A cached binary is only usable if it is non-empty and (on non-Windows) has an
|
|
22
|
-
// exec bit. A truncated or non-executable file means a corrupt cache: re-download.
|
|
21
|
+
// ~keep A cached binary is only usable if it is non-empty and (on non-Windows) has an
|
|
22
|
+
// ~keep exec bit. A truncated or non-executable file means a corrupt cache: re-download.
|
|
23
23
|
function isHealthy(file) {
|
|
24
24
|
try {
|
|
25
25
|
const stat = fs.statSync(file);
|
|
@@ -34,9 +34,9 @@ function isHealthy(file) {
|
|
|
34
34
|
async function ensureBinary() {
|
|
35
35
|
if (fs.existsSync(binPath) && isHealthy(binPath)) return;
|
|
36
36
|
process.stderr.write(`${BIN_NAME}: binary missing or corrupt, attempting download...\n`);
|
|
37
|
-
// Call main() explicitly rather than relying on import side-effects: ESM
|
|
38
|
-
// caches modules, so the installer's top-level run is gated to direct
|
|
39
|
-
// invocation only and would not fire on import.
|
|
37
|
+
// ~keep Call main() explicitly rather than relying on import side-effects: ESM
|
|
38
|
+
// ~keep caches modules, so the installer's top-level run is gated to direct
|
|
39
|
+
// ~keep invocation only and would not fire on import.
|
|
40
40
|
const { main } = await import("../install.js");
|
|
41
41
|
await main();
|
|
42
42
|
}
|
|
@@ -64,7 +64,7 @@ async function main() {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
main().catch((err) => {
|
|
67
|
-
// No standalone CLI for this platform: print the graceful install hint, not a stack.
|
|
67
|
+
// ~keep No standalone CLI for this platform: print the graceful install hint, not a stack.
|
|
68
68
|
if (err && err.name === "CliUnavailableError") {
|
|
69
69
|
printUnavailable();
|
|
70
70
|
process.exit(1);
|
package/install.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// postinstall: download, verify, and extract the native liter-llm binary
|
|
2
|
-
// into ./bin so the launcher can exec it. All diagnostics go to stderr.
|
|
3
1
|
import fs from "node:fs";
|
|
4
2
|
import os from "node:os";
|
|
5
3
|
import path from "node:path";
|
|
@@ -14,7 +12,6 @@ const PKG_NAME = "liter-llm-cli";
|
|
|
14
12
|
const VERSION_ENV = "LITER_LLM_CLI_VERSION";
|
|
15
13
|
const USER_AGENT = "liter-llm-cli-npm-proxy";
|
|
16
14
|
|
|
17
|
-
// Map Node's platform/arch to the Rust target triple embedded in asset names.
|
|
18
15
|
function targetTriple() {
|
|
19
16
|
const type = os.type();
|
|
20
17
|
const arch = os.arch();
|
|
@@ -40,9 +37,6 @@ function binaryName() {
|
|
|
40
37
|
return os.type() === "Windows_NT" ? `${BIN_NAME}.exe` : BIN_NAME;
|
|
41
38
|
}
|
|
42
39
|
|
|
43
|
-
// GET a URL following redirects, returning the response body as a Buffer.
|
|
44
|
-
// Every hop (initial request and every redirect target) MUST be https; any
|
|
45
|
-
// other scheme is rejected to prevent downgrade/SSRF via a malicious Location.
|
|
46
40
|
function httpGetBuffer(url, { headers = {} } = {}, maxRedirects = 5) {
|
|
47
41
|
return new Promise((resolve, reject) => {
|
|
48
42
|
if (maxRedirects < 0) return reject(new Error("too many redirects"));
|
|
@@ -80,9 +74,6 @@ async function httpGetJson(url) {
|
|
|
80
74
|
return JSON.parse(buf.toString("utf8"));
|
|
81
75
|
}
|
|
82
76
|
|
|
83
|
-
// Signals that the release carries no standalone CLI for this platform (only
|
|
84
|
-
// bindings/native-lib/brew-bottle artifacts). The launcher catches this by name
|
|
85
|
-
// and prints a graceful install hint instead of a raw stack trace.
|
|
86
77
|
export class CliUnavailableError extends Error {
|
|
87
78
|
constructor(message) {
|
|
88
79
|
super(message);
|
|
@@ -90,8 +81,6 @@ export class CliUnavailableError extends Error {
|
|
|
90
81
|
}
|
|
91
82
|
}
|
|
92
83
|
|
|
93
|
-
// Substrings that mark an asset as a binding/native-lib/brew-bottle artifact —
|
|
94
|
-
// never the standalone CLI. Matched case-insensitively anywhere in the name.
|
|
95
84
|
const NON_CLI_PATTERNS = [
|
|
96
85
|
"-ffi",
|
|
97
86
|
"_ffi",
|
|
@@ -109,7 +98,6 @@ const NON_CLI_PATTERNS = [
|
|
|
109
98
|
"napi",
|
|
110
99
|
];
|
|
111
100
|
|
|
112
|
-
// True if the asset name matches any non-CLI artifact pattern.
|
|
113
101
|
export function isNonCliArtifact(name) {
|
|
114
102
|
const n = (name || "").toLowerCase();
|
|
115
103
|
return NON_CLI_PATTERNS.some((pat) => n.includes(pat));
|
|
@@ -123,9 +111,6 @@ export function assetScore(name) {
|
|
|
123
111
|
return score;
|
|
124
112
|
}
|
|
125
113
|
|
|
126
|
-
// Pure asset-selection core: from a list of asset names, keep only triple-matched
|
|
127
|
-
// .tar.gz/.zip archives that are NOT binding/native-lib/bottle artifacts, then
|
|
128
|
-
// return the best (cli/bin-name preferred). Returns null when none qualify.
|
|
129
114
|
export function selectArchiveName(names, triple) {
|
|
130
115
|
const survivors = (names || []).filter((name) => {
|
|
131
116
|
const n = (name || "").toLowerCase();
|
|
@@ -138,13 +123,6 @@ export function selectArchiveName(names, triple) {
|
|
|
138
123
|
return survivors[0];
|
|
139
124
|
}
|
|
140
125
|
|
|
141
|
-
// Resolve the release (honoring LITER_LLM_CLI_VERSION to pin a tag) and pick the
|
|
142
|
-
// archive asset for this platform plus an optional SHA256SUMS asset.
|
|
143
|
-
//
|
|
144
|
-
// Selection: among assets whose name contains the target triple, ends in
|
|
145
|
-
// .tar.gz/.zip, and is NOT a binding/native-lib/bottle artifact, prefer one
|
|
146
|
-
// whose name contains "cli" or the bin name. If none survive, the release has
|
|
147
|
-
// no standalone CLI for this platform (CliUnavailableError).
|
|
148
126
|
async function resolveRelease() {
|
|
149
127
|
const triple = targetTriple();
|
|
150
128
|
const pinned = process.env[VERSION_ENV];
|
|
@@ -180,7 +158,6 @@ async function resolveRelease() {
|
|
|
180
158
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
181
159
|
const BIN_DIR = path.join(__dirname, "bin");
|
|
182
160
|
|
|
183
|
-
// Parse a `sha256<space>filename` checksums file; return the digest for name.
|
|
184
161
|
function expectedDigest(text, assetName) {
|
|
185
162
|
for (const raw of text.split(/\r?\n/)) {
|
|
186
163
|
const line = raw.trim();
|
|
@@ -215,18 +192,15 @@ async function verifyOrWarn(archiveBuf, archiveName, checksums) {
|
|
|
215
192
|
process.stderr.write(`Checksum verified for ${archiveName}.\n`);
|
|
216
193
|
}
|
|
217
194
|
|
|
218
|
-
// Reject archive entry names that could escape the extraction directory:
|
|
219
|
-
// absolute paths (POSIX or Windows drive/UNC) or any component equal to "..".
|
|
220
195
|
function isUnsafeEntry(name) {
|
|
221
196
|
const entry = String(name).replace(/\\/g, "/").trim();
|
|
222
197
|
if (!entry) return false;
|
|
223
198
|
if (entry.startsWith("/")) return true;
|
|
224
|
-
if (/^[a-zA-Z]:/.test(entry)) return true;
|
|
225
|
-
if (entry.startsWith("//")) return true;
|
|
199
|
+
if (/^[a-zA-Z]:/.test(entry)) return true;
|
|
200
|
+
if (entry.startsWith("//")) return true;
|
|
226
201
|
return entry.split("/").some((part) => part === "..");
|
|
227
202
|
}
|
|
228
203
|
|
|
229
|
-
// List the entries of a gzipped tar without extracting (`tar -tzf`).
|
|
230
204
|
function listTarEntries(archivePath) {
|
|
231
205
|
const result = spawnSync("tar", ["-tzf", archivePath]);
|
|
232
206
|
if (result.status !== 0) {
|
|
@@ -248,7 +222,6 @@ function extractTarGz(archivePath, destDir) {
|
|
|
248
222
|
}
|
|
249
223
|
}
|
|
250
224
|
|
|
251
|
-
// List the entries of a zip without extracting (`unzip -Z1`, or PowerShell on Windows).
|
|
252
225
|
function listZipEntries(archivePath) {
|
|
253
226
|
if (os.type() === "Windows_NT") {
|
|
254
227
|
const script =
|
|
@@ -278,7 +251,6 @@ function listZipEntries(archivePath) {
|
|
|
278
251
|
|
|
279
252
|
function extractZip(archivePath, destDir) {
|
|
280
253
|
if (os.type() === "Windows_NT") {
|
|
281
|
-
// No string interpolation into a -Command: all path data passed as literal args.
|
|
282
254
|
const result = spawnSync("powershell", [
|
|
283
255
|
"-NoProfile",
|
|
284
256
|
"-NonInteractive",
|
|
@@ -303,7 +275,6 @@ function extractZip(archivePath, destDir) {
|
|
|
303
275
|
}
|
|
304
276
|
}
|
|
305
277
|
|
|
306
|
-
// Locate the binary anywhere under dir (archives may nest it in a subdir).
|
|
307
278
|
function findBinary(dir, name) {
|
|
308
279
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
309
280
|
const full = path.join(dir, entry.name);
|
|
@@ -317,7 +288,6 @@ function findBinary(dir, name) {
|
|
|
317
288
|
return null;
|
|
318
289
|
}
|
|
319
290
|
|
|
320
|
-
// Find a directory named `name` anywhere under dir.
|
|
321
291
|
function findDir(dir, name) {
|
|
322
292
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
323
293
|
if (!entry.isDirectory()) continue;
|
|
@@ -328,10 +298,6 @@ function findDir(dir, name) {
|
|
|
328
298
|
return null;
|
|
329
299
|
}
|
|
330
300
|
|
|
331
|
-
// Validate every archive entry, extract into an isolated temp dir, then copy
|
|
332
|
-
// out ONLY the expected binary (by basename) plus an optional sibling lib/ dir.
|
|
333
|
-
// Nothing else from the archive is honored, so a malicious member can never
|
|
334
|
-
// land outside dest even if extraction tooling mishandled it.
|
|
335
301
|
function safeExtract(archivePath, archiveName, dest) {
|
|
336
302
|
const isZip = archiveName.toLowerCase().endsWith(".zip");
|
|
337
303
|
const entries = isZip ? listZipEntries(archivePath) : listTarEntries(archivePath);
|
|
@@ -352,15 +318,11 @@ function safeExtract(archivePath, archiveName, dest) {
|
|
|
352
318
|
const binName = binaryName();
|
|
353
319
|
const extractedBin = findBinary(tmpDir, binName);
|
|
354
320
|
if (!extractedBin) {
|
|
355
|
-
// The chosen asset did not actually contain the CLI binary — treat the
|
|
356
|
-
// release as having no valid CLI for this platform rather than leaving a
|
|
357
|
-
// bad/partial install behind.
|
|
358
321
|
throw new CliUnavailableError(`archive ${archiveName} did not contain expected CLI binary ${binName}`);
|
|
359
322
|
}
|
|
360
323
|
const finalBin = path.join(dest, binName);
|
|
361
324
|
fs.copyFileSync(extractedBin, finalBin);
|
|
362
325
|
|
|
363
|
-
// Copy a sibling lib/ directory if present (some platforms ship shared libs).
|
|
364
326
|
const libDir = findDir(tmpDir, "lib");
|
|
365
327
|
if (libDir) {
|
|
366
328
|
fs.cpSync(libDir, path.join(dest, "lib"), { recursive: true });
|
|
@@ -380,9 +342,7 @@ export async function main() {
|
|
|
380
342
|
const sizeOk = stat.size > 0;
|
|
381
343
|
const execOk = os.type() === "Windows_NT" || (stat.mode & 0o111) !== 0;
|
|
382
344
|
if (sizeOk && execOk) return;
|
|
383
|
-
} catch {
|
|
384
|
-
// fall through and re-download
|
|
385
|
-
}
|
|
345
|
+
} catch {}
|
|
386
346
|
}
|
|
387
347
|
|
|
388
348
|
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
@@ -393,7 +353,6 @@ export async function main() {
|
|
|
393
353
|
const archiveBuf = await httpGetBuffer(archive.browser_download_url);
|
|
394
354
|
await verifyOrWarn(archiveBuf, archive.name, checksums);
|
|
395
355
|
|
|
396
|
-
// Stage the archive in an isolated temp dir; never extract straight into BIN_DIR.
|
|
397
356
|
const stageDir = fs.mkdtempSync(path.join(os.tmpdir(), `${PKG_NAME}-dl-`));
|
|
398
357
|
try {
|
|
399
358
|
const archivePath = path.join(stageDir, path.basename(archive.name));
|
|
@@ -409,9 +368,6 @@ export async function main() {
|
|
|
409
368
|
process.stderr.write(`${BIN_NAME} installed.\n`);
|
|
410
369
|
}
|
|
411
370
|
|
|
412
|
-
// Run automatically only when invoked directly (npm postinstall: `node install.js`).
|
|
413
|
-
// When imported by the launcher, the launcher calls main() explicitly instead of
|
|
414
|
-
// relying on import side-effects (ESM caches modules, so a second import is a no-op).
|
|
415
371
|
if (import.meta.url === pathToFileURL(process.argv[1] || "").href) {
|
|
416
372
|
main().catch((err) => {
|
|
417
373
|
process.stderr.write(`Error installing ${BIN_NAME}: ${err.message}\n`);
|
package/package.json
CHANGED