@longleash/cli 0.1.0-rc.4 → 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
@@ -1616,6 +1692,27 @@ function processAlive(pid) {
1616
1692
  }
1617
1693
  }
1618
1694
 
1695
+ // src/setup-input.ts
1696
+ function resolveAllowedRootAnswer(answer, defaultRoot) {
1697
+ const trimmed = answer.trim();
1698
+ if (/^(?:y|yes|n|no)$/i.test(trimmed)) {
1699
+ return {
1700
+ ok: false,
1701
+ message: `This question needs a folder path. Press Enter to use ${defaultRoot}, or type another existing folder.`
1702
+ };
1703
+ }
1704
+ try {
1705
+ const [root] = normalizeRoots([trimmed || defaultRoot]);
1706
+ if (!root) throw new Error("At least one allowed project directory is required.");
1707
+ return { ok: true, root };
1708
+ } catch (error) {
1709
+ return {
1710
+ ok: false,
1711
+ message: `That folder cannot be used: ${error instanceof Error ? error.message : String(error)}`
1712
+ };
1713
+ }
1714
+ }
1715
+
1619
1716
  // src/service.ts
1620
1717
  import {
1621
1718
  chmodSync as chmodSync2,
@@ -2218,8 +2315,15 @@ async function setup(options) {
2218
2315
  try {
2219
2316
  console.log("\nLongLeash setup\n");
2220
2317
  console.log("Agents remain on this laptop. Choose only folders where they may work.");
2221
- const rootAnswer = await prompt.question(`Allowed project folder [${process.cwd()}]: `);
2222
- options.roots = [rootAnswer.trim() || process.cwd()];
2318
+ const defaultRoot = process.cwd();
2319
+ while (options.roots.length === 0) {
2320
+ const rootAnswer = await prompt.question(`Allowed project folder (press Enter for ${defaultRoot}): `);
2321
+ const resolved = resolveAllowedRootAnswer(rootAnswer, defaultRoot);
2322
+ if (resolved.ok) options.roots = [resolved.root];
2323
+ else console.log(`
2324
+ ${resolved.message}
2325
+ `);
2326
+ }
2223
2327
  console.log("\nConnectivity: hosted works away from home; LAN-only has no internet relay.");
2224
2328
  const relayAnswer = await prompt.question("Relay [hosted / lan / wss://your-relay/ws] (hosted): ");
2225
2329
  options.relay = relayAnswer.trim() || "hosted";
@@ -2264,7 +2368,10 @@ async function setup(options) {
2264
2368
  throw new Error("The staged package could not be configured. The active version was not changed.");
2265
2369
  }
2266
2370
  try {
2267
- 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
+ }
2268
2375
  } catch (error) {
2269
2376
  prepared.rollback();
2270
2377
  restoreConfigSnapshot(configSnapshot);
@@ -2492,6 +2599,7 @@ async function uninstall() {
2492
2599
  if (hooksRemoved !== 0) throw new Error("Provider hooks could not be removed safely. The managed runtime was preserved.");
2493
2600
  const result = uninstallManagedRuntime();
2494
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.");
2495
2603
  console.log(`Preserved settings, paired devices, and audit data in ${result.configPreserved}.`);
2496
2604
  return 0;
2497
2605
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@longleash/cli",
3
- "version": "0.1.0-rc.4",
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.4",
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.4",
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",