tessl 0.102.0 → 0.103.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.
- package/bin/tessl.js +240 -210
- package/package.json +1 -1
package/bin/tessl.js
CHANGED
|
@@ -1,146 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// installer/src/main.ts
|
|
4
|
-
import { existsSync as existsSync3 } from "node:fs";
|
|
5
|
-
import { mkdir as mkdir3, rm as rm2 } from "node:fs/promises";
|
|
6
|
-
import { basename as basename2, join as join2 } from "node:path";
|
|
7
|
-
import { createInterface } from "node:readline";
|
|
8
|
-
import { Readable as Readable2 } from "node:stream";
|
|
9
|
-
|
|
10
|
-
// installer/src/download.ts
|
|
11
|
-
async function downloadTarball(url) {
|
|
12
|
-
const response = await fetch(url);
|
|
13
|
-
if (!response.ok) {
|
|
14
|
-
throw new Error(`Failed to download binary: ${response.status} ${response.statusText}`);
|
|
15
|
-
}
|
|
16
|
-
if (!response.body) {
|
|
17
|
-
throw new Error("Response body is null");
|
|
18
|
-
}
|
|
19
|
-
return response.body;
|
|
20
|
-
}
|
|
21
|
-
async function downloadBytes(url, maxBytes = 512 * 1024 * 1024) {
|
|
22
|
-
const response = await fetch(url);
|
|
23
|
-
if (!response.ok) {
|
|
24
|
-
throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
|
|
25
|
-
}
|
|
26
|
-
const advertised = Number(response.headers.get("content-length"));
|
|
27
|
-
if (Number.isFinite(advertised) && advertised > maxBytes) {
|
|
28
|
-
throw new Error(`Response from ${url} exceeds size limit (${advertised} > ${maxBytes} bytes)`);
|
|
29
|
-
}
|
|
30
|
-
if (!response.body) {
|
|
31
|
-
throw new Error(`Response from ${url} has no body`);
|
|
32
|
-
}
|
|
33
|
-
const chunks = [];
|
|
34
|
-
let total = 0;
|
|
35
|
-
const reader = response.body.getReader();
|
|
36
|
-
for (;; ) {
|
|
37
|
-
const chunk = await reader.read();
|
|
38
|
-
if (chunk.done)
|
|
39
|
-
break;
|
|
40
|
-
total += chunk.value.byteLength;
|
|
41
|
-
if (total > maxBytes) {
|
|
42
|
-
await reader.cancel();
|
|
43
|
-
throw new Error(`Response from ${url} exceeds size limit (> ${maxBytes} bytes)`);
|
|
44
|
-
}
|
|
45
|
-
chunks.push(chunk.value);
|
|
46
|
-
}
|
|
47
|
-
return Buffer.concat(chunks);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// installer/src/channels.ts
|
|
51
|
-
var MAX_CHANNEL_RESPONSE_BYTES = 8 * 1024;
|
|
52
|
-
var VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9.+-]{0,127}$/;
|
|
53
|
-
function assertPathSafeVersion(version, source) {
|
|
54
|
-
if (!VERSION_PATTERN.test(version)) {
|
|
55
|
-
const shown = version.length > 40 ? `${JSON.stringify(version.slice(0, 40))}… (${version.length} chars)` : JSON.stringify(version);
|
|
56
|
-
const where = source ? ` from ${source}` : "";
|
|
57
|
-
throw new Error(`Refusing to install: release channel returned a malformed version${where}: ${shown}.`);
|
|
58
|
-
}
|
|
59
|
-
return version;
|
|
60
|
-
}
|
|
61
|
-
async function fetchLatestVersion(channel) {
|
|
62
|
-
const url = getChannelUrl(channel);
|
|
63
|
-
const bytes = await downloadBytes(url, MAX_CHANNEL_RESPONSE_BYTES);
|
|
64
|
-
const version = bytes.toString("utf8").trim();
|
|
65
|
-
if (!version) {
|
|
66
|
-
throw new Error(`Empty version returned from ${url}`);
|
|
67
|
-
}
|
|
68
|
-
return assertPathSafeVersion(version, url);
|
|
69
|
-
}
|
|
70
|
-
function getChannelFromVersion(version) {
|
|
71
|
-
if (version.includes("head"))
|
|
72
|
-
return "head";
|
|
73
|
-
if (version.includes("nightly"))
|
|
74
|
-
return "nightly";
|
|
75
|
-
if (version.includes("beta"))
|
|
76
|
-
return "beta";
|
|
77
|
-
return "latest";
|
|
78
|
-
}
|
|
79
|
-
function getBaseUrl() {
|
|
80
|
-
return process.env.TESSL_INSTALL_BASE_URL || "https://install.tessl.io";
|
|
81
|
-
}
|
|
82
|
-
function getChannelUrl(channel) {
|
|
83
|
-
return `${getBaseUrl()}/.well-known/tessl/${channel}.txt`;
|
|
84
|
-
}
|
|
85
|
-
function getManifestUrl(version) {
|
|
86
|
-
return `${getBaseUrl()}/binaries/${encodeURIComponent(version)}/SHA256SUMS`;
|
|
87
|
-
}
|
|
88
|
-
function getManifestSigUrl(version) {
|
|
89
|
-
return `${getBaseUrl()}/binaries/${encodeURIComponent(version)}/SHA256SUMS.sig`;
|
|
90
|
-
}
|
|
91
|
-
function getBinaryFileName(version, platform) {
|
|
92
|
-
return `tessl-${version}-${platform}.tar.gz`;
|
|
93
|
-
}
|
|
94
|
-
function getBinaryUrl(version, platform) {
|
|
95
|
-
const encodedVersion = encodeURIComponent(version);
|
|
96
|
-
return `${getBaseUrl()}/binaries/${encodedVersion}/${getBinaryFileName(encodedVersion, platform)}`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// installer/src/extract.ts
|
|
100
|
-
import { spawn } from "node:child_process";
|
|
101
|
-
import { mkdir } from "node:fs/promises";
|
|
102
|
-
import { Readable } from "node:stream";
|
|
103
|
-
import { pipeline } from "node:stream/promises";
|
|
104
|
-
async function extractTarball(stream, destination) {
|
|
105
|
-
await mkdir(destination, { recursive: true });
|
|
106
|
-
const tar = spawn("tar", ["-xzf", "-", "-C", destination], {
|
|
107
|
-
stdio: ["pipe", "inherit", "inherit"]
|
|
108
|
-
});
|
|
109
|
-
if (!tar.stdin) {
|
|
110
|
-
throw new Error("Failed to open tar stdin");
|
|
111
|
-
}
|
|
112
|
-
const tarExit = new Promise((resolve, reject) => {
|
|
113
|
-
tar.on("exit", (code) => {
|
|
114
|
-
if (code === 0) {
|
|
115
|
-
resolve();
|
|
116
|
-
} else {
|
|
117
|
-
reject(new Error(`tar exited with code ${code}`));
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
tar.on("error", reject);
|
|
121
|
-
});
|
|
122
|
-
await pipeline(Readable.fromWeb(stream), tar.stdin);
|
|
123
|
-
await tarExit;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// installer/src/keys.ts
|
|
127
|
-
var RELEASE_SIGNING_PUBLIC_KEYS = [
|
|
128
|
-
{
|
|
129
|
-
id: "kms-2026-06",
|
|
130
|
-
publicKeyPem: `-----BEGIN PUBLIC KEY-----
|
|
131
|
-
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE81DxbddrC8wywZa2W0R+dq5q7xsB
|
|
132
|
-
TaNvYdQeUrdR5LGZIaYN4XziIK27qwZEl4xJIEB+sqgsx2limHUjf/OKwA==
|
|
133
|
-
-----END PUBLIC KEY-----
|
|
134
|
-
`
|
|
135
|
-
}
|
|
136
|
-
];
|
|
137
|
-
var RELEASE_VERIFICATION_ENABLED = true;
|
|
138
|
-
|
|
139
3
|
// installer/src/path.ts
|
|
140
4
|
import { existsSync } from "node:fs";
|
|
141
5
|
import {
|
|
142
6
|
copyFile,
|
|
143
|
-
mkdir
|
|
7
|
+
mkdir,
|
|
144
8
|
readdir,
|
|
145
9
|
readlink,
|
|
146
10
|
rename,
|
|
@@ -219,7 +83,7 @@ async function promoteStagedBinary({
|
|
|
219
83
|
if (!validation.success) {
|
|
220
84
|
throw new Error(`${validation.error}. Please try again.`);
|
|
221
85
|
}
|
|
222
|
-
await
|
|
86
|
+
await mkdir(versionsDir, { recursive: true });
|
|
223
87
|
await rename(stagedBinPath, versionedBinPath);
|
|
224
88
|
}
|
|
225
89
|
async function getExistingBinaryPath(tesslBinPath) {
|
|
@@ -244,7 +108,7 @@ async function updateBinaryAtomic({
|
|
|
244
108
|
versionedBinPath,
|
|
245
109
|
tesslBinPath
|
|
246
110
|
}) {
|
|
247
|
-
await
|
|
111
|
+
await mkdir(dirname(tesslBinPath), { recursive: true });
|
|
248
112
|
const tag = `${Date.now()}-${process.pid}-${nextPathSeq()}`;
|
|
249
113
|
const tempPath = `${tesslBinPath}.tmp.${tag}`;
|
|
250
114
|
if (process.platform === "win32") {
|
|
@@ -358,41 +222,142 @@ function getPlatformDescription() {
|
|
|
358
222
|
return `${platformName} ${archName}`;
|
|
359
223
|
}
|
|
360
224
|
|
|
361
|
-
// installer/src/
|
|
362
|
-
import {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
225
|
+
// installer/src/prepare.ts
|
|
226
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
227
|
+
import { mkdir as mkdir3, rm as rm2 } from "node:fs/promises";
|
|
228
|
+
import { basename as basename2, join as join2 } from "node:path";
|
|
229
|
+
import { createInterface } from "node:readline";
|
|
230
|
+
import { Readable as Readable2 } from "node:stream";
|
|
231
|
+
|
|
232
|
+
// installer/src/download.ts
|
|
233
|
+
async function downloadTarball(url) {
|
|
234
|
+
const response = await fetch(url);
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
throw new Error(`Failed to download binary: ${response.status} ${response.statusText}`);
|
|
237
|
+
}
|
|
238
|
+
if (!response.body) {
|
|
239
|
+
throw new Error("Response body is null");
|
|
240
|
+
}
|
|
241
|
+
return response.body;
|
|
242
|
+
}
|
|
243
|
+
async function downloadBytes(url, maxBytes = 512 * 1024 * 1024) {
|
|
244
|
+
const response = await fetch(url);
|
|
245
|
+
if (!response.ok) {
|
|
246
|
+
throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
|
|
247
|
+
}
|
|
248
|
+
const advertised = Number(response.headers.get("content-length"));
|
|
249
|
+
if (Number.isFinite(advertised) && advertised > maxBytes) {
|
|
250
|
+
throw new Error(`Response from ${url} exceeds size limit (${advertised} > ${maxBytes} bytes)`);
|
|
251
|
+
}
|
|
252
|
+
if (!response.body) {
|
|
253
|
+
throw new Error(`Response from ${url} has no body`);
|
|
254
|
+
}
|
|
255
|
+
const chunks = [];
|
|
256
|
+
let total = 0;
|
|
257
|
+
const reader = response.body.getReader();
|
|
258
|
+
for (;; ) {
|
|
259
|
+
const chunk = await reader.read();
|
|
260
|
+
if (chunk.done)
|
|
261
|
+
break;
|
|
262
|
+
total += chunk.value.byteLength;
|
|
263
|
+
if (total > maxBytes) {
|
|
264
|
+
await reader.cancel();
|
|
265
|
+
throw new Error(`Response from ${url} exceeds size limit (> ${maxBytes} bytes)`);
|
|
369
266
|
}
|
|
267
|
+
chunks.push(chunk.value);
|
|
268
|
+
}
|
|
269
|
+
return Buffer.concat(chunks);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// installer/src/channels.ts
|
|
273
|
+
var MAX_CHANNEL_RESPONSE_BYTES = 8 * 1024;
|
|
274
|
+
var VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9.+-]{0,127}$/;
|
|
275
|
+
function assertPathSafeVersion(version, source) {
|
|
276
|
+
if (!VERSION_PATTERN.test(version)) {
|
|
277
|
+
const shown = version.length > 40 ? `${JSON.stringify(version.slice(0, 40))}… (${version.length} chars)` : JSON.stringify(version);
|
|
278
|
+
const where = source ? ` from ${source}` : "";
|
|
279
|
+
throw new Error(`Refusing to install: release channel returned a malformed version${where}: ${shown}.`);
|
|
280
|
+
}
|
|
281
|
+
return version;
|
|
282
|
+
}
|
|
283
|
+
async function fetchLatestVersion(channel) {
|
|
284
|
+
const url = getChannelUrl(channel);
|
|
285
|
+
const bytes = await downloadBytes(url, MAX_CHANNEL_RESPONSE_BYTES);
|
|
286
|
+
const version = bytes.toString("utf8").trim();
|
|
287
|
+
if (!version) {
|
|
288
|
+
throw new Error(`Empty version returned from ${url}`);
|
|
289
|
+
}
|
|
290
|
+
return assertPathSafeVersion(version, url);
|
|
291
|
+
}
|
|
292
|
+
function getChannelFromVersion(version) {
|
|
293
|
+
if (version.includes("head"))
|
|
294
|
+
return "head";
|
|
295
|
+
if (version.includes("nightly"))
|
|
296
|
+
return "nightly";
|
|
297
|
+
if (version.includes("beta"))
|
|
298
|
+
return "beta";
|
|
299
|
+
return "latest";
|
|
300
|
+
}
|
|
301
|
+
function getBaseUrl() {
|
|
302
|
+
return process.env.TESSL_INSTALL_BASE_URL || "https://install.tessl.io";
|
|
303
|
+
}
|
|
304
|
+
function getChannelUrl(channel) {
|
|
305
|
+
return `${getBaseUrl()}/.well-known/tessl/${channel}.txt`;
|
|
306
|
+
}
|
|
307
|
+
function getManifestUrl(version) {
|
|
308
|
+
return `${getBaseUrl()}/binaries/${encodeURIComponent(version)}/SHA256SUMS`;
|
|
309
|
+
}
|
|
310
|
+
function getManifestSigUrl(version) {
|
|
311
|
+
return `${getBaseUrl()}/binaries/${encodeURIComponent(version)}/SHA256SUMS.sig`;
|
|
312
|
+
}
|
|
313
|
+
function getBinaryFileName(version, platform) {
|
|
314
|
+
return `tessl-${version}-${platform}.tar.gz`;
|
|
315
|
+
}
|
|
316
|
+
function getBinaryUrl(version, platform) {
|
|
317
|
+
const encodedVersion = encodeURIComponent(version);
|
|
318
|
+
return `${getBaseUrl()}/binaries/${encodedVersion}/${getBinaryFileName(encodedVersion, platform)}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// installer/src/extract.ts
|
|
322
|
+
import { spawn } from "node:child_process";
|
|
323
|
+
import { mkdir as mkdir2 } from "node:fs/promises";
|
|
324
|
+
import { Readable } from "node:stream";
|
|
325
|
+
import { pipeline } from "node:stream/promises";
|
|
326
|
+
async function extractTarball(stream, destination) {
|
|
327
|
+
await mkdir2(destination, { recursive: true });
|
|
328
|
+
const tar = spawn("tar", ["-xzf", "-", "-C", destination], {
|
|
329
|
+
stdio: ["pipe", "inherit", "inherit"]
|
|
370
330
|
});
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
for (const signal of signals) {
|
|
374
|
-
const handler = () => {
|
|
375
|
-
child.kill(signal);
|
|
376
|
-
};
|
|
377
|
-
handlers.set(signal, handler);
|
|
378
|
-
process.on(signal, handler);
|
|
331
|
+
if (!tar.stdin) {
|
|
332
|
+
throw new Error("Failed to open tar stdin");
|
|
379
333
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
334
|
+
const tarExit = new Promise((resolve2, reject) => {
|
|
335
|
+
tar.on("exit", (code) => {
|
|
336
|
+
if (code === 0) {
|
|
337
|
+
resolve2();
|
|
338
|
+
} else {
|
|
339
|
+
reject(new Error(`tar exited with code ${code}`));
|
|
384
340
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
process.exit(code ?? 1);
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
child.on("error", (err) => {
|
|
391
|
-
console.error(`Failed to spawn binary: ${err.message}`);
|
|
392
|
-
process.exit(1);
|
|
341
|
+
});
|
|
342
|
+
tar.on("error", reject);
|
|
393
343
|
});
|
|
344
|
+
await pipeline(Readable.fromWeb(stream), tar.stdin);
|
|
345
|
+
await tarExit;
|
|
394
346
|
}
|
|
395
347
|
|
|
348
|
+
// installer/src/keys.ts
|
|
349
|
+
var RELEASE_SIGNING_PUBLIC_KEYS = [
|
|
350
|
+
{
|
|
351
|
+
id: "kms-2026-06",
|
|
352
|
+
publicKeyPem: `-----BEGIN PUBLIC KEY-----
|
|
353
|
+
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE81DxbddrC8wywZa2W0R+dq5q7xsB
|
|
354
|
+
TaNvYdQeUrdR5LGZIaYN4XziIK27qwZEl4xJIEB+sqgsx2limHUjf/OKwA==
|
|
355
|
+
-----END PUBLIC KEY-----
|
|
356
|
+
`
|
|
357
|
+
}
|
|
358
|
+
];
|
|
359
|
+
var RELEASE_VERIFICATION_ENABLED = true;
|
|
360
|
+
|
|
396
361
|
// installer/src/verify.ts
|
|
397
362
|
import { createHash, verify } from "node:crypto";
|
|
398
363
|
function parseManifest(manifest) {
|
|
@@ -430,7 +395,7 @@ function sha256Hex(data) {
|
|
|
430
395
|
return createHash("sha256").update(data).digest("hex");
|
|
431
396
|
}
|
|
432
397
|
|
|
433
|
-
// installer/src/
|
|
398
|
+
// installer/src/prepare.ts
|
|
434
399
|
function isInteractive() {
|
|
435
400
|
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
436
401
|
}
|
|
@@ -438,7 +403,8 @@ async function downloadVerifyAndExtract({
|
|
|
438
403
|
version,
|
|
439
404
|
platform,
|
|
440
405
|
tarballUrl,
|
|
441
|
-
destination
|
|
406
|
+
destination,
|
|
407
|
+
trustedKeys
|
|
442
408
|
}) {
|
|
443
409
|
const fileName = getBinaryFileName(version, platform);
|
|
444
410
|
const [tarballBytes, manifestBytes, manifestSig] = await Promise.all([
|
|
@@ -446,7 +412,7 @@ async function downloadVerifyAndExtract({
|
|
|
446
412
|
downloadBytes(getManifestUrl(version), 2 * 1024 * 1024),
|
|
447
413
|
downloadBytes(getManifestSigUrl(version), 64 * 1024)
|
|
448
414
|
]);
|
|
449
|
-
if (!verifyManifestSignatureWithKeys(manifestBytes, manifestSig,
|
|
415
|
+
if (!verifyManifestSignatureWithKeys(manifestBytes, manifestSig, trustedKeys)) {
|
|
450
416
|
throw new Error("SHA256SUMS signature verification failed — refusing to install. " + "The download may be corrupt or tampered with.");
|
|
451
417
|
}
|
|
452
418
|
const expected = expectedHashFor(manifestBytes.toString("utf8"), fileName);
|
|
@@ -470,44 +436,27 @@ async function promptConfirmation(message) {
|
|
|
470
436
|
});
|
|
471
437
|
});
|
|
472
438
|
}
|
|
473
|
-
function
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const existingBinary = await getExistingBinaryPath(tesslBinPath);
|
|
484
|
-
const installerVersion = "0.102.0";
|
|
485
|
-
const isTestOrDevBuild = installerVersion.includes("test") || installerVersion.includes("dev");
|
|
486
|
-
let versionMismatch = false;
|
|
487
|
-
if (isTestOrDevBuild && existingBinary) {
|
|
488
|
-
const platform2 = getPlatformString();
|
|
489
|
-
if (!platform2) {
|
|
490
|
-
printUnsupportedPlatformMessage();
|
|
491
|
-
return process.exit(1);
|
|
492
|
-
}
|
|
493
|
-
if (existingBinary !== getVersionedBinPath(installerVersion, platform2)) {
|
|
494
|
-
console.log(`Test/dev build - switching to version: ${installerVersion}`);
|
|
495
|
-
versionMismatch = true;
|
|
439
|
+
async function describeInstallFailure(version, cause) {
|
|
440
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
441
|
+
let available = "";
|
|
442
|
+
try {
|
|
443
|
+
const latest = await Promise.race([
|
|
444
|
+
fetchLatestVersion(getChannelFromVersion(version)),
|
|
445
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 5000).unref())
|
|
446
|
+
]);
|
|
447
|
+
if (latest !== version) {
|
|
448
|
+
available = ` The latest available version is ${latest}.`;
|
|
496
449
|
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const versionedBinPath = getVersionedBinPath(version, platform);
|
|
508
|
-
if (existsSync3(versionedBinPath)) {
|
|
509
|
-
return versionedBinPath;
|
|
510
|
-
}
|
|
450
|
+
} catch {}
|
|
451
|
+
return new Error(`Failed to install Tessl CLI version ${version}: ${reason}.${available}`);
|
|
452
|
+
}
|
|
453
|
+
async function downloadRequestedVersion({
|
|
454
|
+
version,
|
|
455
|
+
platform,
|
|
456
|
+
versionedBinPath,
|
|
457
|
+
trustedKeys,
|
|
458
|
+
verificationEnabled
|
|
459
|
+
}) {
|
|
511
460
|
console.log(`Downloading ${version} for ${platform}`);
|
|
512
461
|
const url = getBinaryUrl(version, platform);
|
|
513
462
|
const versionsDir = getVersionsDirectory();
|
|
@@ -516,23 +465,49 @@ async function prepareBinary() {
|
|
|
516
465
|
const progressInterval = setInterval(() => process.stdout.write("."), 500).unref();
|
|
517
466
|
try {
|
|
518
467
|
await mkdir3(stagingDir, { recursive: true });
|
|
519
|
-
if (
|
|
468
|
+
if (verificationEnabled) {
|
|
520
469
|
await downloadVerifyAndExtract({
|
|
521
470
|
version,
|
|
522
471
|
platform,
|
|
523
472
|
tarballUrl: url,
|
|
524
|
-
destination: stagingDir
|
|
473
|
+
destination: stagingDir,
|
|
474
|
+
trustedKeys
|
|
525
475
|
});
|
|
526
476
|
} else {
|
|
527
477
|
const tarballStream = await downloadTarball(url);
|
|
528
478
|
await extractTarball(tarballStream, stagingDir);
|
|
529
479
|
}
|
|
530
480
|
await promoteStagedBinary({ stagedBinPath, versionedBinPath, versionsDir });
|
|
481
|
+
} catch (err) {
|
|
482
|
+
throw await describeInstallFailure(version, err);
|
|
531
483
|
} finally {
|
|
532
484
|
clearInterval(progressInterval);
|
|
533
485
|
console.log("");
|
|
534
486
|
await rm2(stagingDir, { recursive: true, force: true }).catch(() => {});
|
|
535
487
|
}
|
|
488
|
+
}
|
|
489
|
+
async function prepareBinary({
|
|
490
|
+
installerVersion,
|
|
491
|
+
platform,
|
|
492
|
+
tesslBinPath,
|
|
493
|
+
trustedKeys = RELEASE_SIGNING_PUBLIC_KEYS,
|
|
494
|
+
verificationEnabled = RELEASE_VERIFICATION_ENABLED
|
|
495
|
+
}) {
|
|
496
|
+
const version = installerVersion;
|
|
497
|
+
const versionedBinPath = getVersionedBinPath(version, platform);
|
|
498
|
+
const existingBinary = await getExistingBinaryPath(tesslBinPath);
|
|
499
|
+
if (existingBinary !== null) {
|
|
500
|
+
return tesslBinPath;
|
|
501
|
+
}
|
|
502
|
+
if (!existsSync3(versionedBinPath)) {
|
|
503
|
+
await downloadRequestedVersion({
|
|
504
|
+
version,
|
|
505
|
+
platform,
|
|
506
|
+
versionedBinPath,
|
|
507
|
+
trustedKeys,
|
|
508
|
+
verificationEnabled
|
|
509
|
+
});
|
|
510
|
+
}
|
|
536
511
|
const shouldInstall = !isInteractive() || await promptConfirmation(`Install Tessl CLI to ${tesslBinPath}?`);
|
|
537
512
|
if (shouldInstall) {
|
|
538
513
|
await updateBinaryAtomic({ versionedBinPath, tesslBinPath });
|
|
@@ -546,8 +521,63 @@ On Alpine: apk add libgcc libstdc++
|
|
|
546
521
|
}
|
|
547
522
|
return versionedBinPath;
|
|
548
523
|
}
|
|
524
|
+
|
|
525
|
+
// installer/src/spawn.ts
|
|
526
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
527
|
+
function spawnBinary(binaryPath, args) {
|
|
528
|
+
const child = spawn2(binaryPath, args, {
|
|
529
|
+
stdio: "inherit",
|
|
530
|
+
env: {
|
|
531
|
+
...process.env,
|
|
532
|
+
TESSL_MANAGED_BY_NPM: "1"
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
const signals = process.platform === "win32" ? ["SIGINT", "SIGTERM", "SIGBREAK"] : ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
536
|
+
const handlers = new Map;
|
|
537
|
+
for (const signal of signals) {
|
|
538
|
+
const handler = () => {
|
|
539
|
+
child.kill(signal);
|
|
540
|
+
};
|
|
541
|
+
handlers.set(signal, handler);
|
|
542
|
+
process.on(signal, handler);
|
|
543
|
+
}
|
|
544
|
+
child.on("exit", (code, signal) => {
|
|
545
|
+
if (signal) {
|
|
546
|
+
for (const [sig, handler] of handlers) {
|
|
547
|
+
process.removeListener(sig, handler);
|
|
548
|
+
}
|
|
549
|
+
process.kill(process.pid, signal);
|
|
550
|
+
} else {
|
|
551
|
+
process.exit(code ?? 1);
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
child.on("error", (err) => {
|
|
555
|
+
console.error(`Failed to spawn binary: ${err.message}`);
|
|
556
|
+
process.exit(1);
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// installer/src/main.ts
|
|
561
|
+
function printUnsupportedPlatformMessage() {
|
|
562
|
+
console.error("Error: Unsupported platform", getPlatformDescription());
|
|
563
|
+
console.error(`Tessl CLI binaries are available for:
|
|
564
|
+
- macOS (ARM64, x64)
|
|
565
|
+
- Linux (x64, ARM64)
|
|
566
|
+
- Windows (x64, ARM64)
|
|
567
|
+
`);
|
|
568
|
+
}
|
|
549
569
|
async function main() {
|
|
550
|
-
const
|
|
570
|
+
const platform = getPlatformString();
|
|
571
|
+
if (!platform) {
|
|
572
|
+
printUnsupportedPlatformMessage();
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
const installerVersion = "0.103.0";
|
|
576
|
+
const path = await prepareBinary({
|
|
577
|
+
installerVersion,
|
|
578
|
+
platform,
|
|
579
|
+
tesslBinPath: getTesslBinPath()
|
|
580
|
+
});
|
|
551
581
|
spawnBinary(path, process.argv.slice(2));
|
|
552
582
|
}
|
|
553
583
|
main().catch((err) => {
|