@xberg-io/liter-llm-cli 1.16.0 → 1.18.0

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/bin/liter-llm.js +12 -17
  2. package/install.js +95 -133
  3. package/package.json +1 -1
package/bin/liter-llm.js CHANGED
@@ -5,8 +5,8 @@
5
5
  import fs from "node:fs";
6
6
  import os from "node:os";
7
7
  import path from "node:path";
8
- import {fileURLToPath} from "node:url";
9
- import {spawnSync} from "node:child_process";
8
+ import { fileURLToPath } from "node:url";
9
+ import { spawnSync } from "node:child_process";
10
10
 
11
11
  const BIN_NAME = "liter-llm";
12
12
 
@@ -24,10 +24,8 @@ const binPath = path.join(__dirname, binaryName());
24
24
  function isHealthy(file) {
25
25
  try {
26
26
  const stat = fs.statSync(file);
27
- if (stat.size <= 0)
28
- return false;
29
- if (os.type() !== "Windows_NT" && (stat.mode & 0o111) === 0)
30
- return false;
27
+ if (stat.size <= 0) return false;
28
+ if (os.type() !== "Windows_NT" && (stat.mode & 0o111) === 0) return false;
31
29
  return true;
32
30
  } catch {
33
31
  return false;
@@ -35,22 +33,20 @@ function isHealthy(file) {
35
33
  }
36
34
 
37
35
  async function ensureBinary() {
38
- if (fs.existsSync(binPath) && isHealthy(binPath))
39
- return;
40
- process.stderr.write(
41
- `${BIN_NAME}: binary missing or corrupt, attempting download...\n`);
36
+ if (fs.existsSync(binPath) && isHealthy(binPath)) return;
37
+ process.stderr.write(`${BIN_NAME}: binary missing or corrupt, attempting download...\n`);
42
38
  // ~keep Call main() explicitly rather than relying on import side-effects:
43
39
  // ESM ~keep caches modules, so the installer's top-level run is gated to
44
40
  // direct ~keep invocation only and would not fire on import.
45
- const {main} = await import("../install.js");
41
+ const { main } = await import("../install.js");
46
42
  await main();
47
43
  }
48
44
 
49
45
  function printUnavailable() {
50
46
  process.stderr.write(
51
- `${BIN_NAME} is not available for your platform yet. Install it with:\n` +
52
- ` brew install xberg-io/tap/liter-llm\n` +
53
- ` or use the Xberg plugin: /plugin marketplace add xberg-io/plugins\n`,
47
+ `${BIN_NAME} is not available for your platform yet. Install it with:\n` +
48
+ ` brew install xberg-io/tap/liter-llm\n` +
49
+ ` or use the Xberg plugin: /plugin marketplace add xberg-io/plugins\n`,
54
50
  );
55
51
  }
56
52
 
@@ -60,10 +56,9 @@ async function main() {
60
56
  printUnavailable();
61
57
  process.exit(1);
62
58
  }
63
- const result = spawnSync(binPath, process.argv.slice(2), {stdio : "inherit"});
59
+ const result = spawnSync(binPath, process.argv.slice(2), { stdio: "inherit" });
64
60
  if (result.error) {
65
- process.stderr.write(
66
- `${BIN_NAME}: failed to spawn binary: ${result.error.message}\n`);
61
+ process.stderr.write(`${BIN_NAME}: failed to spawn binary: ${result.error.message}\n`);
67
62
  process.exit(1);
68
63
  }
69
64
  process.exit(result.status ?? 0);
package/install.js CHANGED
@@ -1,10 +1,10 @@
1
- import {execFileSync, spawnSync} from "node:child_process";
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
2
  import crypto from "node:crypto";
3
3
  import fs from "node:fs";
4
4
  import https from "node:https";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
- import {fileURLToPath, pathToFileURL} from "node:url";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
8
 
9
9
  const REPO = "xberg-io/liter-llm";
10
10
  const BIN_NAME = "liter-llm";
@@ -17,22 +17,17 @@ function targetTriple() {
17
17
  const arch = os.arch();
18
18
 
19
19
  if (type === "Windows_NT") {
20
- if (arch === "x64")
21
- return "x86_64-pc-windows-msvc";
20
+ if (arch === "x64") return "x86_64-pc-windows-msvc";
22
21
  throw new Error(`unsupported Windows arch: ${arch}`);
23
22
  }
24
23
  if (type === "Linux") {
25
- if (arch === "x64")
26
- return "x86_64-unknown-linux-gnu";
27
- if (arch === "arm64")
28
- return "aarch64-unknown-linux-gnu";
24
+ if (arch === "x64") return "x86_64-unknown-linux-gnu";
25
+ if (arch === "arm64") return "aarch64-unknown-linux-gnu";
29
26
  throw new Error(`unsupported Linux arch: ${arch}`);
30
27
  }
31
28
  if (type === "Darwin") {
32
- if (arch === "arm64")
33
- return "aarch64-apple-darwin";
34
- if (arch === "x64")
35
- return "x86_64-apple-darwin";
29
+ if (arch === "arm64") return "aarch64-apple-darwin";
30
+ if (arch === "x64") return "x86_64-apple-darwin";
36
31
  throw new Error(`unsupported macOS arch: ${arch}`);
37
32
  }
38
33
  throw new Error(`unsupported platform: ${type} ${arch}`);
@@ -42,35 +37,30 @@ function binaryName() {
42
37
  return os.type() === "Windows_NT" ? `${BIN_NAME}.exe` : BIN_NAME;
43
38
  }
44
39
 
45
- function httpGetBuffer(url, {headers = {}} = {}, maxRedirects = 5) {
40
+ function httpGetBuffer(url, { headers = {} } = {}, maxRedirects = 5) {
46
41
  return new Promise((resolve, reject) => {
47
- if (maxRedirects < 0)
48
- return reject(new Error("too many redirects"));
42
+ if (maxRedirects < 0) return reject(new Error("too many redirects"));
49
43
  if (!/^https:\/\//i.test(url)) {
50
44
  return reject(new Error(`refusing non-https URL: ${url}`));
51
45
  }
52
- const req = https.get(
53
- url, {headers : {"User-Agent" : USER_AGENT, ...headers}}, (res) => {
54
- if (res.statusCode >= 300 && res.statusCode < 400 &&
55
- res.headers.location) {
56
- res.resume();
57
- const next = res.headers.location;
58
- if (!/^https:\/\//i.test(next)) {
59
- return reject(
60
- new Error(`refusing non-https redirect to: ${next}`));
61
- }
62
- return httpGetBuffer(next, {headers}, maxRedirects - 1)
63
- .then(resolve, reject);
64
- }
65
- if (res.statusCode !== 200) {
66
- res.resume();
67
- return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
68
- }
69
- const chunks = [];
70
- res.on("data", (c) => chunks.push(c));
71
- res.on("end", () => resolve(Buffer.concat(chunks)));
72
- res.on("error", reject);
73
- });
46
+ const req = https.get(url, { headers: { "User-Agent": USER_AGENT, ...headers } }, (res) => {
47
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
48
+ res.resume();
49
+ const next = res.headers.location;
50
+ if (!/^https:\/\//i.test(next)) {
51
+ return reject(new Error(`refusing non-https redirect to: ${next}`));
52
+ }
53
+ return httpGetBuffer(next, { headers }, maxRedirects - 1).then(resolve, reject);
54
+ }
55
+ if (res.statusCode !== 200) {
56
+ res.resume();
57
+ return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
58
+ }
59
+ const chunks = [];
60
+ res.on("data", (c) => chunks.push(c));
61
+ res.on("end", () => resolve(Buffer.concat(chunks)));
62
+ res.on("error", reject);
63
+ });
74
64
  req.on("error", reject);
75
65
  req.setTimeout(60000, () => {
76
66
  req.destroy();
@@ -80,8 +70,7 @@ function httpGetBuffer(url, {headers = {}} = {}, maxRedirects = 5) {
80
70
  }
81
71
 
82
72
  async function httpGetJson(url) {
83
- const buf = await httpGetBuffer(
84
- url, {headers : {Accept : "application/vnd.github+json"}});
73
+ const buf = await httpGetBuffer(url, { headers: { Accept: "application/vnd.github+json" } });
85
74
  return JSON.parse(buf.toString("utf8"));
86
75
  }
87
76
 
@@ -117,24 +106,19 @@ export function isNonCliArtifact(name) {
117
106
  export function assetScore(name) {
118
107
  const n = (name || "").toLowerCase();
119
108
  let score = 0;
120
- if (n.includes("cli"))
121
- score += 2;
122
- if (n.includes(BIN_NAME.toLowerCase()))
123
- score += 1;
109
+ if (n.includes("cli")) score += 2;
110
+ if (n.includes(BIN_NAME.toLowerCase())) score += 1;
124
111
  return score;
125
112
  }
126
113
 
127
114
  export function selectArchiveName(names, triple) {
128
115
  const survivors = (names || []).filter((name) => {
129
116
  const n = (name || "").toLowerCase();
130
- if (!n.includes(triple))
131
- return false;
132
- if (!(n.endsWith(".tar.gz") || n.endsWith(".zip")))
133
- return false;
117
+ if (!n.includes(triple)) return false;
118
+ if (!(n.endsWith(".tar.gz") || n.endsWith(".zip"))) return false;
134
119
  return !isNonCliArtifact(n);
135
120
  });
136
- if (survivors.length === 0)
137
- return null;
121
+ if (survivors.length === 0) return null;
138
122
  survivors.sort((a, b) => assetScore(b) - assetScore(a));
139
123
  return survivors[0];
140
124
  }
@@ -143,16 +127,15 @@ async function resolveRelease() {
143
127
  const triple = targetTriple();
144
128
  const pinned = process.env[VERSION_ENV];
145
129
  const apiUrl = pinned
146
- ? `https://api.github.com/repos/${REPO}/releases/tags/${
147
- encodeURIComponent(pinned)}`
148
- : `https://api.github.com/repos/${REPO}/releases/latest`;
130
+ ? `https://api.github.com/repos/${REPO}/releases/tags/${encodeURIComponent(pinned)}`
131
+ : `https://api.github.com/repos/${REPO}/releases/latest`;
149
132
 
150
133
  let release;
151
134
  try {
152
135
  release = await httpGetJson(apiUrl);
153
136
  } catch (err) {
154
137
  if (pinned && /HTTP 404/.test(err.message)) {
155
- throw new Error(`release tag '${pinned}' not found`, {cause : err});
138
+ throw new Error(`release tag '${pinned}' not found`, { cause: err });
156
139
  }
157
140
  throw err;
158
141
  }
@@ -160,18 +143,16 @@ async function resolveRelease() {
160
143
  const tag = release.tag_name || pinned || "latest";
161
144
 
162
145
  const chosenName = selectArchiveName(
163
- assets.map((a) => a.name),
164
- triple,
146
+ assets.map((a) => a.name),
147
+ triple,
165
148
  );
166
149
  if (!chosenName) {
167
- throw new CliUnavailableError(`no standalone CLI asset for target triple "${
168
- triple}" in ${REPO} release ${tag}`);
150
+ throw new CliUnavailableError(`no standalone CLI asset for target triple "${triple}" in ${REPO} release ${tag}`);
169
151
  }
170
152
  const archive = assets.find((a) => a.name === chosenName);
171
- const checksums =
172
- assets.find((a) => (a.name || "").toUpperCase().includes("SHA256SUMS"));
153
+ const checksums = assets.find((a) => (a.name || "").toUpperCase().includes("SHA256SUMS"));
173
154
 
174
- return {tag, triple, archive, checksums};
155
+ return { tag, triple, archive, checksums };
175
156
  }
176
157
 
177
158
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -180,14 +161,11 @@ const BIN_DIR = path.join(__dirname, "bin");
180
161
  function expectedDigest(text, assetName) {
181
162
  for (const raw of text.split(/\r?\n/)) {
182
163
  const line = raw.trim();
183
- if (!line)
184
- continue;
164
+ if (!line) continue;
185
165
  const parts = line.split(/\s+/);
186
- if (parts.length < 2)
187
- continue;
166
+ if (parts.length < 2) continue;
188
167
  const name = parts[parts.length - 1].replace(/^\*/, "");
189
- if (name === assetName)
190
- return parts[0].toLowerCase();
168
+ if (name === assetName) return parts[0].toLowerCase();
191
169
  }
192
170
  return null;
193
171
  }
@@ -195,58 +173,49 @@ function expectedDigest(text, assetName) {
195
173
  async function verifyOrWarn(archiveBuf, archiveName, checksums) {
196
174
  if (!checksums) {
197
175
  process.stderr.write(
198
- `WARNING: no SHA256SUMS asset found for ${archiveName}; ` +
199
- `installing over HTTPS without checksum verification.\n`,
176
+ `WARNING: no SHA256SUMS asset found for ${archiveName}; ` +
177
+ `installing over HTTPS without checksum verification.\n`,
200
178
  );
201
179
  return;
202
180
  }
203
- const sumsText =
204
- (await httpGetBuffer(checksums.browser_download_url)).toString("utf8");
181
+ const sumsText = (await httpGetBuffer(checksums.browser_download_url)).toString("utf8");
205
182
  const expected = expectedDigest(sumsText, archiveName);
206
183
  if (!expected) {
207
184
  throw new Error(
208
- `no checksum entry for ${archiveName} in ${
209
- checksums.name} — refusing to install unverified binary`,
185
+ `no checksum entry for ${archiveName} in ${checksums.name} — refusing to install unverified binary`,
210
186
  );
211
187
  }
212
- const actual = crypto.createHash("sha256")
213
- .update(archiveBuf)
214
- .digest("hex")
215
- .toLowerCase();
188
+ const actual = crypto.createHash("sha256").update(archiveBuf).digest("hex").toLowerCase();
216
189
  if (actual !== expected) {
217
- throw new Error(`checksum mismatch for ${archiveName} (expected ${
218
- expected}, got ${actual})`);
190
+ throw new Error(`checksum mismatch for ${archiveName} (expected ${expected}, got ${actual})`);
219
191
  }
220
192
  process.stderr.write(`Checksum verified for ${archiveName}.\n`);
221
193
  }
222
194
 
223
195
  function isUnsafeEntry(name) {
224
196
  const entry = String(name).replace(/\\/g, "/").trim();
225
- if (!entry)
226
- return false;
227
- if (entry.startsWith("/"))
228
- return true;
229
- if (/^[a-zA-Z]:/.test(entry))
230
- return true;
231
- if (entry.startsWith("//"))
232
- return true;
197
+ if (!entry) return false;
198
+ if (entry.startsWith("/")) return true;
199
+ if (/^[a-zA-Z]:/.test(entry)) return true;
200
+ if (entry.startsWith("//")) return true;
233
201
  return entry.split("/").some((part) => part === "..");
234
202
  }
235
203
 
236
204
  function listTarEntries(archivePath) {
237
- const result = spawnSync("tar", [ "-tzf", archivePath ]);
205
+ const result = spawnSync("tar", ["-tzf", archivePath]);
238
206
  if (result.status !== 0) {
239
207
  const stderr = result.stderr ? result.stderr.toString() : "";
240
208
  throw new Error(`tar listing failed: ${stderr || result.error}`);
241
209
  }
242
- return result.stdout.toString()
243
- .split(/\r?\n/)
244
- .map((s) => s.trim())
245
- .filter(Boolean);
210
+ return result.stdout
211
+ .toString()
212
+ .split(/\r?\n/)
213
+ .map((s) => s.trim())
214
+ .filter(Boolean);
246
215
  }
247
216
 
248
217
  function extractTarGz(archivePath, destDir) {
249
- const result = spawnSync("tar", [ "-xzf", archivePath, "-C", destDir ]);
218
+ const result = spawnSync("tar", ["-xzf", archivePath, "-C", destDir]);
250
219
  if (result.status !== 0) {
251
220
  const stderr = result.stderr ? result.stderr.toString() : "";
252
221
  throw new Error(`tar extraction failed: ${stderr || result.error}`);
@@ -256,26 +225,28 @@ function extractTarGz(archivePath, destDir) {
256
225
  function listZipEntries(archivePath) {
257
226
  if (os.type() === "Windows_NT") {
258
227
  const script =
259
- "$ErrorActionPreference='Stop';" +
260
- "Add-Type -AssemblyName System.IO.Compression.FileSystem;" +
261
- "[System.IO.Compression.ZipFile]::OpenRead($args[0]).Entries |" +
262
- " ForEach-Object { $_.FullName }";
263
- const out = execFileSync(
264
- "powershell",
265
- [ "-NoProfile", "-NonInteractive", "-Command", script, archivePath ], {
266
- encoding : "utf8",
267
- });
268
- return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
228
+ "$ErrorActionPreference='Stop';" +
229
+ "Add-Type -AssemblyName System.IO.Compression.FileSystem;" +
230
+ "[System.IO.Compression.ZipFile]::OpenRead($args[0]).Entries |" +
231
+ " ForEach-Object { $_.FullName }";
232
+ const out = execFileSync("powershell", ["-NoProfile", "-NonInteractive", "-Command", script, archivePath], {
233
+ encoding: "utf8",
234
+ });
235
+ return out
236
+ .split(/\r?\n/)
237
+ .map((s) => s.trim())
238
+ .filter(Boolean);
269
239
  }
270
- const result = spawnSync("unzip", [ "-Z1", archivePath ]);
240
+ const result = spawnSync("unzip", ["-Z1", archivePath]);
271
241
  if (result.status !== 0) {
272
242
  const stderr = result.stderr ? result.stderr.toString() : "";
273
243
  throw new Error(`zip listing failed: ${stderr || result.error}`);
274
244
  }
275
- return result.stdout.toString()
276
- .split(/\r?\n/)
277
- .map((s) => s.trim())
278
- .filter(Boolean);
245
+ return result.stdout
246
+ .toString()
247
+ .split(/\r?\n/)
248
+ .map((s) => s.trim())
249
+ .filter(Boolean);
279
250
  }
280
251
 
281
252
  function extractZip(archivePath, destDir) {
@@ -297,7 +268,7 @@ function extractZip(archivePath, destDir) {
297
268
  }
298
269
  return;
299
270
  }
300
- const result = spawnSync("unzip", [ "-o", archivePath, "-d", destDir ]);
271
+ const result = spawnSync("unzip", ["-o", archivePath, "-d", destDir]);
301
272
  if (result.status !== 0) {
302
273
  const stderr = result.stderr ? result.stderr.toString() : "";
303
274
  throw new Error(`zip extraction failed: ${stderr || result.error}`);
@@ -305,12 +276,11 @@ function extractZip(archivePath, destDir) {
305
276
  }
306
277
 
307
278
  function findBinary(dir, name) {
308
- for (const entry of fs.readdirSync(dir, {withFileTypes : true})) {
279
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
309
280
  const full = path.join(dir, entry.name);
310
281
  if (entry.isDirectory()) {
311
282
  const found = findBinary(full, name);
312
- if (found)
313
- return found;
283
+ if (found) return found;
314
284
  } else if (entry.name === name) {
315
285
  return full;
316
286
  }
@@ -319,22 +289,18 @@ function findBinary(dir, name) {
319
289
  }
320
290
 
321
291
  function findDir(dir, name) {
322
- for (const entry of fs.readdirSync(dir, {withFileTypes : true})) {
323
- if (!entry.isDirectory())
324
- continue;
325
- if (entry.name === name)
326
- return path.join(dir, entry.name);
292
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
293
+ if (!entry.isDirectory()) continue;
294
+ if (entry.name === name) return path.join(dir, entry.name);
327
295
  const found = findDir(path.join(dir, entry.name), name);
328
- if (found)
329
- return found;
296
+ if (found) return found;
330
297
  }
331
298
  return null;
332
299
  }
333
300
 
334
301
  function safeExtract(archivePath, archiveName, dest) {
335
302
  const isZip = archiveName.toLowerCase().endsWith(".zip");
336
- const entries =
337
- isZip ? listZipEntries(archivePath) : listTarEntries(archivePath);
303
+ const entries = isZip ? listZipEntries(archivePath) : listTarEntries(archivePath);
338
304
  for (const entry of entries) {
339
305
  if (isUnsafeEntry(entry)) {
340
306
  throw new Error(`refusing unsafe archive entry: ${entry}`);
@@ -352,19 +318,18 @@ function safeExtract(archivePath, archiveName, dest) {
352
318
  const binName = binaryName();
353
319
  const extractedBin = findBinary(tmpDir, binName);
354
320
  if (!extractedBin) {
355
- throw new CliUnavailableError(`archive ${
356
- archiveName} did not contain expected CLI binary ${binName}`);
321
+ throw new CliUnavailableError(`archive ${archiveName} did not contain expected CLI binary ${binName}`);
357
322
  }
358
323
  const finalBin = path.join(dest, binName);
359
324
  fs.copyFileSync(extractedBin, finalBin);
360
325
 
361
326
  const libDir = findDir(tmpDir, "lib");
362
327
  if (libDir) {
363
- fs.cpSync(libDir, path.join(dest, "lib"), {recursive : true});
328
+ fs.cpSync(libDir, path.join(dest, "lib"), { recursive: true });
364
329
  }
365
330
  return finalBin;
366
331
  } finally {
367
- fs.rmSync(tmpDir, {recursive : true, force : true});
332
+ fs.rmSync(tmpDir, { recursive: true, force: true });
368
333
  }
369
334
  }
370
335
 
@@ -376,17 +341,14 @@ export async function main() {
376
341
  const stat = fs.statSync(finalPath);
377
342
  const sizeOk = stat.size > 0;
378
343
  const execOk = os.type() === "Windows_NT" || (stat.mode & 0o111) !== 0;
379
- if (sizeOk && execOk)
380
- return;
381
- } catch {
382
- }
344
+ if (sizeOk && execOk) return;
345
+ } catch {}
383
346
  }
384
347
 
385
- fs.mkdirSync(BIN_DIR, {recursive : true});
348
+ fs.mkdirSync(BIN_DIR, { recursive: true });
386
349
 
387
- const {tag, archive, checksums} = await resolveRelease();
388
- process.stderr.write(
389
- `Downloading ${BIN_NAME} ${tag} asset ${archive.name}...\n`);
350
+ const { tag, archive, checksums } = await resolveRelease();
351
+ process.stderr.write(`Downloading ${BIN_NAME} ${tag} asset ${archive.name}...\n`);
390
352
 
391
353
  const archiveBuf = await httpGetBuffer(archive.browser_download_url);
392
354
  await verifyOrWarn(archiveBuf, archive.name, checksums);
@@ -397,7 +359,7 @@ export async function main() {
397
359
  fs.writeFileSync(archivePath, archiveBuf);
398
360
  safeExtract(archivePath, archive.name, BIN_DIR);
399
361
  } finally {
400
- fs.rmSync(stageDir, {recursive : true, force : true});
362
+ fs.rmSync(stageDir, { recursive: true, force: true });
401
363
  }
402
364
 
403
365
  if (os.type() !== "Windows_NT") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xberg-io/liter-llm-cli",
3
- "version": "1.16.0",
3
+ "version": "1.18.0",
4
4
  "description": "CLI proxy for liter-llm — downloads and runs the native liter-llm binary from GitHub releases.",
5
5
  "license": "MIT",
6
6
  "author": "Na'aman Hirschfeld",