tessl 0.101.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.
Files changed (2) hide show
  1. package/bin/tessl.js +240 -210
  2. 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 as mkdir2,
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 mkdir2(versionsDir, { recursive: true });
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 mkdir2(dirname(tesslBinPath), { recursive: true });
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/spawn.ts
362
- import { spawn as spawn2 } from "node:child_process";
363
- function spawnBinary(binaryPath, args) {
364
- const child = spawn2(binaryPath, args, {
365
- stdio: "inherit",
366
- env: {
367
- ...process.env,
368
- TESSL_MANAGED_BY_NPM: "1"
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
- const signals = process.platform === "win32" ? ["SIGINT", "SIGTERM", "SIGBREAK"] : ["SIGINT", "SIGTERM", "SIGHUP"];
372
- const handlers = new Map;
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
- child.on("exit", (code, signal) => {
381
- if (signal) {
382
- for (const [sig, handler] of handlers) {
383
- process.removeListener(sig, handler);
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
- process.kill(process.pid, signal);
386
- } else {
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/main.ts
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, RELEASE_SIGNING_PUBLIC_KEYS)) {
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 printUnsupportedPlatformMessage() {
474
- console.error("Error: Unsupported platform", getPlatformDescription());
475
- console.error(`Tessl CLI binaries are available for:
476
- - macOS (ARM64, x64)
477
- - Linux (x64, ARM64)
478
- - Windows (x64, ARM64)
479
- `);
480
- }
481
- async function prepareBinary() {
482
- const tesslBinPath = getTesslBinPath();
483
- const existingBinary = await getExistingBinaryPath(tesslBinPath);
484
- const installerVersion = "0.101.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
- if (existingBinary && !versionMismatch) {
499
- return tesslBinPath;
500
- }
501
- const platform = getPlatformString();
502
- if (!platform) {
503
- printUnsupportedPlatformMessage();
504
- return process.exit(1);
505
- }
506
- const version = isTestOrDevBuild ? installerVersion : await fetchLatestVersion(getChannelFromVersion(installerVersion));
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 (RELEASE_VERIFICATION_ENABLED) {
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 path = await prepareBinary();
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tessl",
3
- "version": "0.101.0",
3
+ "version": "0.103.0",
4
4
  "description": "Tessl CLI",
5
5
  "author": "Tessl",
6
6
  "license": "SEE LICENSE.md",