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