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

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
@@ -1740,8 +1816,8 @@ StartLimitBurst=5
1740
1816
  [Service]
1741
1817
  Type=simple
1742
1818
  ExecStart=${systemdQuote(paths.wrapper)} run
1743
- EnvironmentFile=${systemdQuote(paths.environment)}
1744
- WorkingDirectory=${systemdQuote(home)}
1819
+ EnvironmentFile=${systemdPath(paths.environment)}
1820
+ WorkingDirectory=${systemdPath(home)}
1745
1821
  UMask=0077
1746
1822
  Restart=on-failure
1747
1823
  RestartSec=5s
@@ -1895,16 +1971,28 @@ function installSystemdService(context) {
1895
1971
  if (!context.paths.environment) throw new Error("The Linux service environment path is missing.");
1896
1972
  const previousUnit = snapshot(context.paths.definition);
1897
1973
  const previousEnvironment = snapshot(context.paths.environment);
1974
+ const wasEnabled = run(context, "systemctl", ["--user", "is-enabled", "--quiet", SYSTEMD_UNIT]).status === 0;
1975
+ const wasActive = run(context, "systemctl", ["--user", "is-active", "--quiet", SYSTEMD_UNIT]).status === 0;
1898
1976
  try {
1899
1977
  writeManagedAtomically(context.paths.environment, renderSystemdEnvironment(context.paths, context.env), 384);
1900
1978
  writeManagedAtomically(context.paths.definition, renderSystemdUnit(context.paths, context.home), 384);
1901
1979
  requireSuccess(context, "systemctl", ["--user", "daemon-reload"]);
1902
- requireSuccess(context, "systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT]);
1980
+ if (wasActive) {
1981
+ if (!wasEnabled) requireSuccess(context, "systemctl", ["--user", "enable", SYSTEMD_UNIT]);
1982
+ requireSuccess(context, "systemctl", ["--user", "restart", SYSTEMD_UNIT]);
1983
+ } else {
1984
+ requireSuccess(context, "systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT]);
1985
+ }
1903
1986
  } catch (error) {
1904
1987
  restore(context.paths.environment, previousEnvironment);
1905
1988
  restore(context.paths.definition, previousUnit);
1906
1989
  run(context, "systemctl", ["--user", "daemon-reload"]);
1907
- if (previousUnit !== null) run(context, "systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT]);
1990
+ if (previousUnit !== null) {
1991
+ if (wasEnabled) run(context, "systemctl", ["--user", "enable", SYSTEMD_UNIT]);
1992
+ else run(context, "systemctl", ["--user", "disable", SYSTEMD_UNIT]);
1993
+ if (wasActive) run(context, "systemctl", ["--user", "restart", SYSTEMD_UNIT]);
1994
+ else run(context, "systemctl", ["--user", "stop", SYSTEMD_UNIT]);
1995
+ }
1908
1996
  throw error;
1909
1997
  }
1910
1998
  }
@@ -2026,6 +2114,12 @@ function systemdQuote(value) {
2026
2114
  if (/[\0\r\n]/.test(value)) throw new Error("Service paths may not contain control characters.");
2027
2115
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%")}"`;
2028
2116
  }
2117
+ function systemdPath(value) {
2118
+ if (/[\u0000-\u0008\u000a-\u001f\u007f-\u009f]/.test(value)) {
2119
+ throw new Error("Service paths may not contain control characters.");
2120
+ }
2121
+ return value.replace(/%/g, "%%").replace(/\\/g, "\\x5c").replace(/ /g, "\\x20").replace(/\t/g, "\\x09").replace(/"/g, "\\x22").replace(/'/g, "\\x27");
2122
+ }
2029
2123
  function environmentQuote(value) {
2030
2124
  if (/[\0\r\n]/.test(value)) throw new Error("Service environment values may not contain control characters.");
2031
2125
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
@@ -2292,7 +2386,10 @@ ${resolved.message}
2292
2386
  throw new Error("The staged package could not be configured. The active version was not changed.");
2293
2387
  }
2294
2388
  try {
2295
- prepared.activate();
2389
+ const activation = prepared.activate();
2390
+ if (activation.migratedLegacyWrapper) {
2391
+ console.log("Migrated the verified legacy LongLeash command. Its checkout was preserved and will be restored if you uninstall this runtime.");
2392
+ }
2296
2393
  } catch (error) {
2297
2394
  prepared.rollback();
2298
2395
  restoreConfigSnapshot(configSnapshot);
@@ -2520,6 +2617,7 @@ async function uninstall() {
2520
2617
  if (hooksRemoved !== 0) throw new Error("Provider hooks could not be removed safely. The managed runtime was preserved.");
2521
2618
  const result = uninstallManagedRuntime();
2522
2619
  console.log(result.removed ? "Removed the managed LongLeash runtime and executable." : "No managed LongLeash runtime was installed.");
2620
+ if (result.legacyWrapperRestored) console.log("Restored the verified legacy LongLeash command that existed before the npm migration.");
2523
2621
  console.log(`Preserved settings, paired devices, and audit data in ${result.configPreserved}.`);
2524
2622
  return 0;
2525
2623
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@longleash/cli",
3
- "version": "0.1.0-rc.5",
3
+ "version": "0.1.0-rc.7",
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.7",
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.7",
4
4
  "description": "Securely control Claude Code and Codex sessions on your laptop from your phone.",
5
5
  "license": "MIT",
6
6
  "type": "module",