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