@longleash/cli 0.1.0-rc.5 → 0.1.0-rc.6

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/longleash.mjs CHANGED
@@ -1401,27 +1401,42 @@ function prepareManagedInstall(version, env = process.env) {
1401
1401
  const hadWrapper = pathExists2(paths.wrapper);
1402
1402
  const hadMarker = pathExists2(marker);
1403
1403
  let previousWrapper;
1404
+ let previousWrapperMode = 448;
1404
1405
  let previousMarker;
1405
1406
  let wrapperWritten = false;
1406
1407
  let markerWritten = false;
1408
+ let migratedLegacyWrapper = false;
1409
+ let legacyWrapper;
1407
1410
  const tempLink = join2(paths.home, `.current-${process.pid}-${randomUUID2()}`);
1408
1411
  try {
1409
1412
  mkdirSync2(paths.bin, { recursive: true, mode: 448 });
1410
1413
  if (hadWrapper) {
1411
- if (lstatSync2(paths.wrapper).isSymbolicLink()) throw new Error(`Refusing symlinked executable: ${paths.wrapper}`);
1414
+ const wrapperStat = lstatSync2(paths.wrapper);
1415
+ if (wrapperStat.isSymbolicLink()) throw new Error(`Refusing symlinked executable: ${paths.wrapper}`);
1416
+ if (!wrapperStat.isFile()) throw new Error(`Refusing non-file executable: ${paths.wrapper}`);
1417
+ if (process.getuid !== void 0 && wrapperStat.uid !== process.getuid()) {
1418
+ throw new Error(`Refusing executable owned by another user: ${paths.wrapper}`);
1419
+ }
1412
1420
  previousWrapper = readFileSync2(paths.wrapper, "utf8");
1421
+ previousWrapperMode = wrapperStat.mode & 511;
1413
1422
  if (!previousWrapper.includes(WRAPPER_MARKER)) {
1414
- throw new Error(`Refusing to replace an unmanaged executable: ${paths.wrapper}`);
1423
+ legacyWrapper = recognizeLegacyWrapper(previousWrapper, wrapperStat.mode & 511, paths.wrapper);
1424
+ migratedLegacyWrapper = true;
1415
1425
  }
1416
1426
  }
1417
1427
  if (hadMarker) {
1418
1428
  if (lstatSync2(marker).isSymbolicLink()) throw new Error(`Refusing symlinked install marker: ${marker}`);
1419
1429
  previousMarker = readFileSync2(marker, "utf8");
1430
+ const parsed = parseInstallMarker(previousMarker, marker);
1431
+ if (parsed.package !== PACKAGE_NAME) throw new Error(`Refusing unrecognized install marker: ${marker}`);
1432
+ legacyWrapper = parsed.legacyWrapper ?? legacyWrapper;
1420
1433
  }
1421
1434
  const activeCli = join2(paths.current, "node_modules", "@longleash", "cli", "bin", "longleash.mjs");
1422
1435
  writeExecutableAtomically(paths.wrapper, wrapper(activeCli, process.execPath));
1423
1436
  wrapperWritten = true;
1424
- writeFileAtomically(marker, `${JSON.stringify({ schema: 1, package: PACKAGE_NAME }, null, 2)}
1437
+ const nextMarker = { schema: 1, package: PACKAGE_NAME };
1438
+ if (legacyWrapper) nextMarker.legacyWrapper = legacyWrapper;
1439
+ writeFileAtomically(marker, `${JSON.stringify(nextMarker, null, 2)}
1425
1440
  `, 384);
1426
1441
  markerWritten = true;
1427
1442
  symlinkSync(release, tempLink, "dir");
@@ -1430,6 +1445,7 @@ function prepareManagedInstall(version, env = process.env) {
1430
1445
  }
1431
1446
  renameSync2(tempLink, paths.current);
1432
1447
  activated = true;
1448
+ return { migratedLegacyWrapper };
1433
1449
  } catch (error) {
1434
1450
  try {
1435
1451
  rmSync2(tempLink, { force: true });
@@ -1438,7 +1454,7 @@ function prepareManagedInstall(version, env = process.env) {
1438
1454
  const restorationFailures = [];
1439
1455
  if (wrapperWritten) {
1440
1456
  try {
1441
- restoreManagedFile(paths.wrapper, hadWrapper ? previousWrapper : void 0, 448);
1457
+ restoreManagedFile(paths.wrapper, hadWrapper ? previousWrapper : void 0, previousWrapperMode);
1442
1458
  } catch (restoreError) {
1443
1459
  restorationFailures.push(`executable: ${String(restoreError)}`);
1444
1460
  }
@@ -1478,18 +1494,78 @@ function prepareManagedInstall(version, env = process.env) {
1478
1494
  function uninstallManagedRuntime(env = process.env) {
1479
1495
  const paths = installPaths(env);
1480
1496
  const marker = join2(paths.home, MARKER);
1481
- if (!pathExists2(marker)) return { removed: false, configPreserved: resolve2(env.LONGLEASH_DATA ?? join2(homedir2(), ".longleash")) };
1497
+ if (!pathExists2(marker)) {
1498
+ return {
1499
+ removed: false,
1500
+ configPreserved: resolve2(env.LONGLEASH_DATA ?? join2(homedir2(), ".longleash")),
1501
+ legacyWrapperRestored: false
1502
+ };
1503
+ }
1482
1504
  if (lstatSync2(marker).isSymbolicLink()) throw new Error(`Refusing symlinked install marker: ${marker}`);
1483
- const parsed = JSON.parse(readFileSync2(marker, "utf8"));
1505
+ const parsed = parseInstallMarker(readFileSync2(marker, "utf8"), marker);
1484
1506
  if (parsed.package !== PACKAGE_NAME) throw new Error(`Refusing to remove unrecognized install directory: ${paths.home}`);
1507
+ let legacyWrapperRestored = false;
1485
1508
  if (pathExists2(paths.wrapper)) {
1486
1509
  if (lstatSync2(paths.wrapper).isSymbolicLink()) throw new Error(`Refusing symlinked executable: ${paths.wrapper}`);
1487
1510
  const wrapperText = readFileSync2(paths.wrapper, "utf8");
1488
1511
  if (!wrapperText.includes(WRAPPER_MARKER)) throw new Error(`Refusing to remove an unmanaged executable: ${paths.wrapper}`);
1489
- rmSync2(paths.wrapper);
1512
+ if (parsed.legacyWrapper) {
1513
+ const backup = validateLegacyBackup(parsed.legacyWrapper, paths.wrapper);
1514
+ writeFileAtomically(paths.wrapper, backup.content, backup.mode);
1515
+ legacyWrapperRestored = true;
1516
+ } else {
1517
+ rmSync2(paths.wrapper);
1518
+ }
1490
1519
  }
1491
1520
  rmSync2(paths.home, { recursive: true });
1492
- return { removed: true, configPreserved: resolve2(env.LONGLEASH_DATA ?? join2(homedir2(), ".longleash")) };
1521
+ return {
1522
+ removed: true,
1523
+ configPreserved: resolve2(env.LONGLEASH_DATA ?? join2(homedir2(), ".longleash")),
1524
+ legacyWrapperRestored
1525
+ };
1526
+ }
1527
+ function recognizeLegacyWrapper(content, mode, wrapperPath) {
1528
+ if (Buffer.byteLength(content, "utf8") > 16 * 1024) {
1529
+ throw new Error(`Refusing to replace an unmanaged executable: ${wrapperPath}`);
1530
+ }
1531
+ const match = content.match(
1532
+ /^#!\/usr\/bin\/env bash\n# Created by the LongLeash installer\. The three values below are the settings; the behaviour\n# lives in \$LONGLEASH_DIR\/scripts\/longleash\.sh and updates with the code\.\nset -euo pipefail\nexport LONGLEASH_DIR="([^"\n]+)"\nexport LONGLEASH_DEFAULT_ROOTS="([^"\n]*)"\nexport LONGLEASH_DEFAULT_RELAY="([^"\n]*)"\nexec bash "\$LONGLEASH_DIR\/scripts\/longleash\.sh" "\$@"\n?$/
1533
+ );
1534
+ if (!match) throw new Error(`Refusing to replace an unmanaged executable: ${wrapperPath}`);
1535
+ const legacyRoot = match[1];
1536
+ if (!legacyRoot || resolve2(legacyRoot) !== legacyRoot) {
1537
+ throw new Error(`Refusing legacy executable with an unsafe checkout path: ${wrapperPath}`);
1538
+ }
1539
+ const legacyEntrypoint = join2(legacyRoot, "scripts", "longleash.sh");
1540
+ if (!pathExists2(legacyEntrypoint) || lstatSync2(legacyEntrypoint).isSymbolicLink() || !lstatSync2(legacyEntrypoint).isFile()) {
1541
+ throw new Error(`The verified legacy executable points to a missing checkout: ${legacyRoot}`);
1542
+ }
1543
+ return { content, mode: normalizeExecutableMode(mode) };
1544
+ }
1545
+ function validateLegacyBackup(value, wrapperPath) {
1546
+ if (!value || typeof value.content !== "string" || !Number.isSafeInteger(value.mode)) {
1547
+ throw new Error(`Refusing invalid legacy executable backup in the install marker: ${wrapperPath}`);
1548
+ }
1549
+ return recognizeLegacyWrapper(value.content, value.mode, wrapperPath);
1550
+ }
1551
+ function parseInstallMarker(content, markerPath) {
1552
+ let parsed;
1553
+ try {
1554
+ parsed = JSON.parse(content);
1555
+ } catch {
1556
+ throw new Error(`Refusing invalid install marker: ${markerPath}`);
1557
+ }
1558
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1559
+ throw new Error(`Refusing invalid install marker: ${markerPath}`);
1560
+ }
1561
+ const marker = parsed;
1562
+ if (marker.legacyWrapper !== void 0) validateLegacyBackup(marker.legacyWrapper, markerPath);
1563
+ return marker;
1564
+ }
1565
+ function normalizeExecutableMode(mode) {
1566
+ const normalized = mode & 511;
1567
+ if ((normalized & 64) === 0) throw new Error("The legacy LongLeash executable is not owner-executable.");
1568
+ return normalized;
1493
1569
  }
1494
1570
  function wrapper(cli, node) {
1495
1571
  return `#!/bin/sh
@@ -2292,7 +2368,10 @@ ${resolved.message}
2292
2368
  throw new Error("The staged package could not be configured. The active version was not changed.");
2293
2369
  }
2294
2370
  try {
2295
- prepared.activate();
2371
+ const activation = prepared.activate();
2372
+ if (activation.migratedLegacyWrapper) {
2373
+ console.log("Migrated the verified legacy LongLeash command. Its checkout was preserved and will be restored if you uninstall this runtime.");
2374
+ }
2296
2375
  } catch (error) {
2297
2376
  prepared.rollback();
2298
2377
  restoreConfigSnapshot(configSnapshot);
@@ -2520,6 +2599,7 @@ async function uninstall() {
2520
2599
  if (hooksRemoved !== 0) throw new Error("Provider hooks could not be removed safely. The managed runtime was preserved.");
2521
2600
  const result = uninstallManagedRuntime();
2522
2601
  console.log(result.removed ? "Removed the managed LongLeash runtime and executable." : "No managed LongLeash runtime was installed.");
2602
+ if (result.legacyWrapperRestored) console.log("Restored the verified legacy LongLeash command that existed before the npm migration.");
2523
2603
  console.log(`Preserved settings, paired devices, and audit data in ${result.configPreserved}.`);
2524
2604
  return 0;
2525
2605
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@longleash/cli",
3
- "version": "0.1.0-rc.5",
3
+ "version": "0.1.0-rc.6",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@longleash/cli",
9
- "version": "0.1.0-rc.5",
9
+ "version": "0.1.0-rc.6",
10
10
  "license": "MIT",
11
11
  "os": [
12
12
  "darwin",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longleash/cli",
3
- "version": "0.1.0-rc.5",
3
+ "version": "0.1.0-rc.6",
4
4
  "description": "Securely control Claude Code and Codex sessions on your laptop from your phone.",
5
5
  "license": "MIT",
6
6
  "type": "module",