impel-cli 0.18.15-beta.4 → 0.18.15-beta.5
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/package.json +1 -1
- package/src/apps.js +120 -19
package/package.json
CHANGED
package/src/apps.js
CHANGED
|
@@ -37,6 +37,17 @@ const APP_DEFINITIONS = {
|
|
|
37
37
|
executableNames: ["ChatGPT", "Codex"],
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
|
+
const BUNDLE_ARTIFACT_REMOVE_OPTIONS = Object.freeze({
|
|
41
|
+
recursive: true,
|
|
42
|
+
force: true,
|
|
43
|
+
maxRetries: 5,
|
|
44
|
+
retryDelay: 25,
|
|
45
|
+
});
|
|
46
|
+
const BUNDLE_LOCK_WAIT_MS = 120_000;
|
|
47
|
+
const BUNDLE_LOCK_POLL_MS = 100;
|
|
48
|
+
const BUNDLE_LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(4));
|
|
49
|
+
const MACOS_O_EXLOCK = 0x00000020;
|
|
50
|
+
const portableBundleMutationLocks = new Set();
|
|
40
51
|
|
|
41
52
|
// Vendor updates can change minified renderer contracts without notice. Each
|
|
42
53
|
// CLI release therefore installs and accepts only the exact app builds tested
|
|
@@ -972,7 +983,6 @@ function installPinnedVendorApp(target, homeDir) {
|
|
|
972
983
|
homeDir, ".config", RUNTIME_BRAND.cli.configNamespace, "vendor", target, pin.version, pin.bundleName,
|
|
973
984
|
);
|
|
974
985
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
975
|
-
sweepStaleBundleArtifacts(path.dirname(destination), path.basename(destination));
|
|
976
986
|
replaceDirectory(destination, extractedApp);
|
|
977
987
|
return destination;
|
|
978
988
|
} finally {
|
|
@@ -1529,6 +1539,12 @@ exec "$NODE" "$CLI" token${tenantArgs}
|
|
|
1529
1539
|
}
|
|
1530
1540
|
|
|
1531
1541
|
function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageName) {
|
|
1542
|
+
return withBundleMutationLock(paths.claude.launcher, () => (
|
|
1543
|
+
writeVendoredClaudeBundleLocked(paths, vendorPath, gatewayUrl, safeStorageName)
|
|
1544
|
+
));
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
function writeVendoredClaudeBundleLocked(paths, vendorPath, gatewayUrl, safeStorageName) {
|
|
1532
1548
|
if (!vendorPath) throw new Error("Claude vendor app is unavailable");
|
|
1533
1549
|
if (!isVendableVendorApp("claude", vendorPath)) {
|
|
1534
1550
|
const bundleId = readBundleIdentifier(vendorPath);
|
|
@@ -1542,9 +1558,8 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
|
|
|
1542
1558
|
if (!executableName) throw new Error(`Claude executable is missing from ${vendorPath}`);
|
|
1543
1559
|
|
|
1544
1560
|
const bundle = paths.claude.launcher;
|
|
1545
|
-
const staging = `${bundle}.tmp-${process.pid}`;
|
|
1546
1561
|
sweepStaleBundleArtifacts(path.dirname(bundle), path.basename(bundle));
|
|
1547
|
-
|
|
1562
|
+
const staging = uniqueBundleArtifactPath(bundle, "tmp");
|
|
1548
1563
|
cloneAppBundle(vendorPath, staging);
|
|
1549
1564
|
try {
|
|
1550
1565
|
const plistPath = path.join(staging, "Contents", "Info.plist");
|
|
@@ -1626,9 +1641,9 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
|
|
|
1626
1641
|
}, null, 2) + "\n", 0o644);
|
|
1627
1642
|
|
|
1628
1643
|
signVendoredApp(staging, vendorExecutable, "Claude", signingIdentity);
|
|
1629
|
-
|
|
1644
|
+
replaceDirectoryUnlocked(bundle, staging);
|
|
1630
1645
|
} catch (error) {
|
|
1631
|
-
|
|
1646
|
+
removeBundleArtifact(staging);
|
|
1632
1647
|
throw error;
|
|
1633
1648
|
}
|
|
1634
1649
|
}
|
|
@@ -1642,6 +1657,12 @@ function claudeUsageCompatibilityEnabled(gatewayUrl) {
|
|
|
1642
1657
|
}
|
|
1643
1658
|
|
|
1644
1659
|
function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
|
|
1660
|
+
return withBundleMutationLock(paths.chatgpt.launcher, () => (
|
|
1661
|
+
writeVendoredChatGPTBundleLocked(paths, vendorPath, gatewayUrl, homeDir)
|
|
1662
|
+
));
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function writeVendoredChatGPTBundleLocked(paths, vendorPath, gatewayUrl, homeDir) {
|
|
1645
1666
|
if (!vendorPath) throw new Error("ChatGPT/Codex vendor app is unavailable");
|
|
1646
1667
|
const definition = APP_DEFINITIONS.chatgpt;
|
|
1647
1668
|
const executableName = definition.executableNames.find((name) => (
|
|
@@ -1650,11 +1671,10 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
|
|
|
1650
1671
|
if (!executableName) throw new Error(`ChatGPT/Codex executable is missing from ${vendorPath}`);
|
|
1651
1672
|
|
|
1652
1673
|
const bundle = paths.chatgpt.launcher;
|
|
1653
|
-
|
|
1674
|
+
sweepStaleBundleArtifacts(path.dirname(bundle), path.basename(bundle));
|
|
1675
|
+
const staging = uniqueBundleArtifactPath(bundle, "tmp");
|
|
1654
1676
|
const vendorBundleName = path.basename(vendorPath);
|
|
1655
1677
|
const vendorBundle = path.join(staging, "Contents", "Resources", vendorBundleName);
|
|
1656
|
-
sweepStaleBundleArtifacts(path.dirname(bundle), path.basename(bundle));
|
|
1657
|
-
fs.rmSync(staging, { recursive: true, force: true });
|
|
1658
1678
|
cloneAppBundle(vendorPath, vendorBundle);
|
|
1659
1679
|
try {
|
|
1660
1680
|
const asarPath = path.join(vendorBundle, "Contents", "Resources", "app.asar");
|
|
@@ -1730,9 +1750,9 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
|
|
|
1730
1750
|
}, null, 2) + "\n", 0o644);
|
|
1731
1751
|
|
|
1732
1752
|
signVendoredApp(staging, vendorExecutable, "ChatGPT", signingIdentity);
|
|
1733
|
-
|
|
1753
|
+
replaceDirectoryUnlocked(bundle, staging);
|
|
1734
1754
|
} catch (error) {
|
|
1735
|
-
|
|
1755
|
+
removeBundleArtifact(staging);
|
|
1736
1756
|
throw error;
|
|
1737
1757
|
}
|
|
1738
1758
|
}
|
|
@@ -2536,24 +2556,105 @@ export function sweepStaleBundleArtifacts(dir, baseName) {
|
|
|
2536
2556
|
throw error;
|
|
2537
2557
|
}
|
|
2538
2558
|
const prefixes = [`${baseName}.tmp-`, `${baseName}.previous-`];
|
|
2539
|
-
const removed =
|
|
2540
|
-
for (const entry of
|
|
2541
|
-
|
|
2559
|
+
const removed = [];
|
|
2560
|
+
for (const entry of entries.filter((candidate) => prefixes.some((prefix) => candidate.startsWith(prefix)))) {
|
|
2561
|
+
if (removeBundleArtifact(path.join(dir, entry))) removed.push(entry);
|
|
2542
2562
|
}
|
|
2543
2563
|
return removed;
|
|
2544
2564
|
}
|
|
2545
2565
|
|
|
2546
|
-
function
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2566
|
+
function uniqueBundleArtifactPath(destination, kind) {
|
|
2567
|
+
return `${destination}.${kind}-${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
function removeBundleArtifact(candidate) {
|
|
2571
|
+
try {
|
|
2572
|
+
fs.rmSync(candidate, BUNDLE_ARTIFACT_REMOVE_OPTIONS);
|
|
2573
|
+
return !fs.existsSync(candidate);
|
|
2574
|
+
} catch {
|
|
2575
|
+
return false;
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
export function withBundleMutationLock(destination, run) {
|
|
2580
|
+
const lockPath = path.join(
|
|
2581
|
+
path.dirname(destination),
|
|
2582
|
+
`.${path.basename(destination)}.impel-install.lock`,
|
|
2583
|
+
);
|
|
2584
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
2585
|
+
|
|
2586
|
+
if (process.platform !== "darwin") {
|
|
2587
|
+
if (portableBundleMutationLocks.has(lockPath)) {
|
|
2588
|
+
throw new Error(`another Impel app update is already replacing ${path.basename(destination)}`);
|
|
2589
|
+
}
|
|
2590
|
+
const descriptor = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_RDWR, 0o600);
|
|
2591
|
+
portableBundleMutationLocks.add(lockPath);
|
|
2592
|
+
try {
|
|
2593
|
+
return run();
|
|
2594
|
+
} finally {
|
|
2595
|
+
portableBundleMutationLocks.delete(lockPath);
|
|
2596
|
+
fs.closeSync(descriptor);
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
const deadline = Date.now() + BUNDLE_LOCK_WAIT_MS;
|
|
2601
|
+
let descriptor;
|
|
2602
|
+
while (descriptor === undefined) {
|
|
2603
|
+
try {
|
|
2604
|
+
descriptor = fs.openSync(
|
|
2605
|
+
lockPath,
|
|
2606
|
+
fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_NONBLOCK | MACOS_O_EXLOCK,
|
|
2607
|
+
0o600,
|
|
2608
|
+
);
|
|
2609
|
+
} catch (error) {
|
|
2610
|
+
if (!["EAGAIN", "EWOULDBLOCK"].includes(error?.code)) throw error;
|
|
2611
|
+
if (Date.now() >= deadline) {
|
|
2612
|
+
throw new Error(`timed out waiting for another Impel app update to replace ${path.basename(destination)}`);
|
|
2613
|
+
}
|
|
2614
|
+
Atomics.wait(BUNDLE_LOCK_SLEEP, 0, 0, BUNDLE_LOCK_POLL_MS);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
try {
|
|
2618
|
+
return run();
|
|
2619
|
+
} finally {
|
|
2620
|
+
fs.closeSync(descriptor);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
function replaceDirectoryUnlocked(destination, staging, {
|
|
2625
|
+
removeArtifact = removeBundleArtifact,
|
|
2626
|
+
} = {}) {
|
|
2627
|
+
const backup = fs.existsSync(destination)
|
|
2628
|
+
? uniqueBundleArtifactPath(destination, "previous")
|
|
2629
|
+
: null;
|
|
2630
|
+
if (backup) fs.renameSync(destination, backup);
|
|
2550
2631
|
try {
|
|
2551
2632
|
fs.renameSync(staging, destination);
|
|
2552
|
-
fs.rmSync(backup, { recursive: true, force: true });
|
|
2553
2633
|
} catch (error) {
|
|
2554
|
-
if (!fs.existsSync(destination) && fs.existsSync(backup))
|
|
2634
|
+
if (backup && !fs.existsSync(destination) && fs.existsSync(backup)) {
|
|
2635
|
+
fs.renameSync(backup, destination);
|
|
2636
|
+
}
|
|
2555
2637
|
throw error;
|
|
2556
2638
|
}
|
|
2639
|
+
|
|
2640
|
+
// The new bundle is committed once staging has been renamed into place.
|
|
2641
|
+
// Finder, LaunchServices, or a just-closed helper can briefly keep changing
|
|
2642
|
+
// the old tree and make recursive removal end with ENOTEMPTY. Do not turn
|
|
2643
|
+
// that post-commit housekeeping race into a failed tenant update: the next
|
|
2644
|
+
// sweep will retry any backup that remains.
|
|
2645
|
+
if (backup) {
|
|
2646
|
+
try {
|
|
2647
|
+
removeArtifact(backup);
|
|
2648
|
+
} catch {
|
|
2649
|
+
// Cleanup is best-effort after the committed rename.
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
export function replaceDirectory(destination, staging, options = {}) {
|
|
2655
|
+
return withBundleMutationLock(destination, () => (
|
|
2656
|
+
replaceDirectoryUnlocked(destination, staging, options)
|
|
2657
|
+
));
|
|
2557
2658
|
}
|
|
2558
2659
|
|
|
2559
2660
|
function isGatewayModel(model) {
|