tessl 0.92.0 → 0.94.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 +91 -48
  2. package/package.json +1 -1
package/bin/tessl.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // installer/src/main.ts
4
- import { existsSync as existsSync3, rmSync } from "node:fs";
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";
5
7
  import { createInterface } from "node:readline";
6
8
  import { Readable as Readable2 } from "node:stream";
7
9
 
@@ -148,6 +150,40 @@ import {
148
150
  } from "node:fs/promises";
149
151
  import { homedir } from "node:os";
150
152
  import { basename, dirname, join, resolve } from "node:path";
153
+
154
+ // installer/src/validate.ts
155
+ import { spawnSync } from "node:child_process";
156
+ function validateBinary(binaryPath) {
157
+ try {
158
+ const result = spawnSync(binaryPath, ["--version"], {
159
+ stdio: "pipe",
160
+ timeout: 1e4,
161
+ env: {
162
+ ...process.env,
163
+ TESSL_AUTO_UPDATE_INTERVAL_MINUTES: "0"
164
+ }
165
+ });
166
+ if (result.error) {
167
+ return { success: false, error: result.error.message };
168
+ }
169
+ if (result.status !== 0) {
170
+ return {
171
+ success: false,
172
+ error: `--version exited with code ${result.status}`
173
+ };
174
+ }
175
+ return { success: true };
176
+ } catch (err) {
177
+ return {
178
+ success: false,
179
+ error: err instanceof Error ? err.message : String(err)
180
+ };
181
+ }
182
+ }
183
+
184
+ // installer/src/path.ts
185
+ var pathSeq = 0;
186
+ var nextPathSeq = () => pathSeq += 1;
151
187
  function getAppDataDir() {
152
188
  if (process.platform === "win32") {
153
189
  return process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local");
@@ -163,11 +199,29 @@ function getTesslBinPath() {
163
199
  function getVersionsDirectory() {
164
200
  return join(getAppDataDir(), "tessl", "versions");
165
201
  }
202
+ function getStagedVersionsDirectory() {
203
+ return join(getAppDataDir(), "tessl", "staged-versions");
204
+ }
205
+ function getStagingDirForRun() {
206
+ return join(getStagedVersionsDirectory(), `${Date.now()}-${process.pid}-${nextPathSeq()}`);
207
+ }
166
208
  function getVersionedBinPath(version, platform) {
167
209
  const base = `tessl-${version}-${platform}`;
168
210
  const name = platform.startsWith("win32") ? `${base}.exe` : base;
169
211
  return join(getVersionsDirectory(), name);
170
212
  }
213
+ async function promoteStagedBinary({
214
+ stagedBinPath,
215
+ versionedBinPath,
216
+ versionsDir
217
+ }) {
218
+ const validation = validateBinary(stagedBinPath);
219
+ if (!validation.success) {
220
+ throw new Error(`${validation.error}. Please try again.`);
221
+ }
222
+ await mkdir2(versionsDir, { recursive: true });
223
+ await rename(stagedBinPath, versionedBinPath);
224
+ }
171
225
  async function getExistingBinaryPath(tesslBinPath) {
172
226
  if (!existsSync(tesslBinPath)) {
173
227
  return null;
@@ -186,22 +240,28 @@ async function getExistingBinaryPath(tesslBinPath) {
186
240
  return null;
187
241
  }
188
242
  }
189
- async function updateBinaryAtomic(versionedBinPath, tesslBinPath) {
243
+ async function updateBinaryAtomic({
244
+ versionedBinPath,
245
+ tesslBinPath
246
+ }) {
190
247
  await mkdir2(dirname(tesslBinPath), { recursive: true });
191
- const tempPath = `${tesslBinPath}.tmp`;
248
+ const tag = `${Date.now()}-${process.pid}-${nextPathSeq()}`;
249
+ const tempPath = `${tesslBinPath}.tmp.${tag}`;
192
250
  if (process.platform === "win32") {
193
- const oldPath = `${tesslBinPath}.old.${Date.now()}`;
251
+ const oldPath = `${tesslBinPath}.old.${tag}`;
194
252
  await copyFile(versionedBinPath, tempPath);
195
253
  try {
196
254
  await rename(tesslBinPath, oldPath);
197
255
  } catch (e) {
198
256
  if (e.code !== "ENOENT") {
257
+ await unlink(tempPath).catch(() => {});
199
258
  throw e;
200
259
  }
201
260
  }
202
261
  try {
203
262
  await rename(tempPath, tesslBinPath);
204
263
  } catch (e) {
264
+ await unlink(tempPath).catch(() => {});
205
265
  try {
206
266
  await rename(oldPath, tesslBinPath);
207
267
  } catch {}
@@ -214,7 +274,12 @@ async function updateBinaryAtomic(versionedBinPath, tesslBinPath) {
214
274
  await unlink(tempPath);
215
275
  } catch {}
216
276
  await symlink(versionedBinPath, tempPath);
217
- await rename(tempPath, tesslBinPath);
277
+ try {
278
+ await rename(tempPath, tesslBinPath);
279
+ } catch (e) {
280
+ await unlink(tempPath).catch(() => {});
281
+ throw e;
282
+ }
218
283
  }
219
284
  async function pruneOldBackups(binDir, tesslBinPath) {
220
285
  try {
@@ -328,36 +393,6 @@ function spawnBinary(binaryPath, args) {
328
393
  });
329
394
  }
330
395
 
331
- // installer/src/validate.ts
332
- import { spawnSync } from "node:child_process";
333
- function validateBinary(binaryPath) {
334
- try {
335
- const result = spawnSync(binaryPath, ["--version"], {
336
- stdio: "pipe",
337
- timeout: 1e4,
338
- env: {
339
- ...process.env,
340
- TESSL_AUTO_UPDATE_INTERVAL_MINUTES: "0"
341
- }
342
- });
343
- if (result.error) {
344
- return { success: false, error: result.error.message };
345
- }
346
- if (result.status !== 0) {
347
- return {
348
- success: false,
349
- error: `--version exited with code ${result.status}`
350
- };
351
- }
352
- return { success: true };
353
- } catch (err) {
354
- return {
355
- success: false,
356
- error: err instanceof Error ? err.message : String(err)
357
- };
358
- }
359
- }
360
-
361
396
  // installer/src/verify.ts
362
397
  import { createHash, verify } from "node:crypto";
363
398
  function parseManifest(manifest) {
@@ -399,7 +434,12 @@ function sha256Hex(data) {
399
434
  function isInteractive() {
400
435
  return process.stdin.isTTY === true && process.stdout.isTTY === true;
401
436
  }
402
- async function downloadVerifyAndExtract(version, platform, tarballUrl, versionsDir) {
437
+ async function downloadVerifyAndExtract({
438
+ version,
439
+ platform,
440
+ tarballUrl,
441
+ destination
442
+ }) {
403
443
  const fileName = getBinaryFileName(version, platform);
404
444
  const [tarballBytes, manifestBytes, manifestSig] = await Promise.all([
405
445
  downloadBytes(tarballUrl),
@@ -415,7 +455,7 @@ async function downloadVerifyAndExtract(version, platform, tarballUrl, versionsD
415
455
  throw new Error(`Checksum mismatch for ${fileName} (expected ${expected}, got ${actual}) — refusing to install.`);
416
456
  }
417
457
  const verifiedStream = Readable2.toWeb(Readable2.from(tarballBytes));
418
- await extractTarball(verifiedStream, versionsDir);
458
+ await extractTarball(verifiedStream, destination);
419
459
  }
420
460
  async function promptConfirmation(message) {
421
461
  return new Promise((resolve2) => {
@@ -441,7 +481,7 @@ function printUnsupportedPlatformMessage() {
441
481
  async function prepareBinary() {
442
482
  const tesslBinPath = getTesslBinPath();
443
483
  const existingBinary = await getExistingBinaryPath(tesslBinPath);
444
- const installerVersion = "0.92.0";
484
+ const installerVersion = "0.94.0";
445
485
  const isTestOrDevBuild = installerVersion.includes("test") || installerVersion.includes("dev");
446
486
  let versionMismatch = false;
447
487
  if (isTestOrDevBuild && existingBinary) {
@@ -471,28 +511,31 @@ async function prepareBinary() {
471
511
  console.log(`Downloading ${version} for ${platform}`);
472
512
  const url = getBinaryUrl(version, platform);
473
513
  const versionsDir = getVersionsDirectory();
514
+ const stagingDir = getStagingDirForRun();
515
+ const stagedBinPath = join2(stagingDir, basename2(versionedBinPath));
474
516
  const progressInterval = setInterval(() => process.stdout.write("."), 500).unref();
475
517
  try {
518
+ await mkdir3(stagingDir, { recursive: true });
476
519
  if (RELEASE_VERIFICATION_ENABLED) {
477
- await downloadVerifyAndExtract(version, platform, url, versionsDir);
520
+ await downloadVerifyAndExtract({
521
+ version,
522
+ platform,
523
+ tarballUrl: url,
524
+ destination: stagingDir
525
+ });
478
526
  } else {
479
527
  const tarballStream = await downloadTarball(url);
480
- await extractTarball(tarballStream, versionsDir);
528
+ await extractTarball(tarballStream, stagingDir);
481
529
  }
530
+ await promoteStagedBinary({ stagedBinPath, versionedBinPath, versionsDir });
482
531
  } finally {
483
532
  clearInterval(progressInterval);
484
533
  console.log("");
485
- }
486
- const validation = validateBinary(versionedBinPath);
487
- if (!validation.success) {
488
- try {
489
- rmSync(versionedBinPath, { force: true });
490
- } catch {}
491
- throw new Error(`${validation.error}. Removed the corrupt file — please try again.`);
534
+ await rm2(stagingDir, { recursive: true, force: true }).catch(() => {});
492
535
  }
493
536
  const shouldInstall = !isInteractive() || await promptConfirmation(`Install Tessl CLI to ${tesslBinPath}?`);
494
537
  if (shouldInstall) {
495
- await updateBinaryAtomic(versionedBinPath, tesslBinPath);
538
+ await updateBinaryAtomic({ versionedBinPath, tesslBinPath });
496
539
  if (platform.includes("musl")) {
497
540
  console.log(`
498
541
  This system requires libgcc and libstdc++. Install these using your distribution's package manager.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tessl",
3
- "version": "0.92.0",
3
+ "version": "0.94.0",
4
4
  "description": "Tessl CLI",
5
5
  "author": "Tessl",
6
6
  "license": "SEE LICENSE.md",