impel-cli 0.18.15-beta.4 → 0.18.15-beta.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/package.json +1 -1
- package/src/apps.js +154 -23
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
|
|
@@ -246,7 +257,10 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
|
246
257
|
// and the tenant manifest so unsupported hosts keep Chat and Code usable.
|
|
247
258
|
// 25: remove the unproven Windows Cowork capability gate from beta.0 profiles
|
|
248
259
|
// and restore the reviewed always-enabled managed policy.
|
|
249
|
-
|
|
260
|
+
// 26: remember the managed provider defaults and move legacy GPT-5.5 profiles
|
|
261
|
+
// onto GPT-5.6 Sol so ChatGPT's compact Work picker does not fall back to its
|
|
262
|
+
// unsupported-selection "Reset to default" treatment.
|
|
263
|
+
export const CURRENT_CONFIG_VERSION = 26;
|
|
250
264
|
|
|
251
265
|
// Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
|
|
252
266
|
// helper rebranding, and signing. A vendored bundle is rebuilt only when this
|
|
@@ -745,6 +759,7 @@ export function installManagedAppFiles({
|
|
|
745
759
|
target,
|
|
746
760
|
vendorPaths[target] || findVendorApp(target, homeDir),
|
|
747
761
|
]));
|
|
762
|
+
const previousManifest = readTenantManifest(homeDir, config.tenantId);
|
|
748
763
|
const vendorCodexModels = targets.includes("chatgpt")
|
|
749
764
|
? readVendorCodexModels(resolvedVendorPaths.chatgpt)
|
|
750
765
|
: new Map();
|
|
@@ -795,6 +810,7 @@ export function installManagedAppFiles({
|
|
|
795
810
|
vendorCodexModels,
|
|
796
811
|
chatgptInvocations,
|
|
797
812
|
generatedAt,
|
|
813
|
+
previousManifest,
|
|
798
814
|
);
|
|
799
815
|
if (RUNTIME_BRAND.features.sessions) ensureCodexSessionHooks(paths.chatgpt.codexHome, config.tenantId, "codex_desktop");
|
|
800
816
|
}
|
|
@@ -806,7 +822,7 @@ export function installManagedAppFiles({
|
|
|
806
822
|
}
|
|
807
823
|
|
|
808
824
|
writeAtomic(path.join(paths.tenantRoot, "manifest.json"), JSON.stringify({
|
|
809
|
-
schemaVersion:
|
|
825
|
+
schemaVersion: 7,
|
|
810
826
|
configVersion: CURRENT_CONFIG_VERSION,
|
|
811
827
|
modelCatalogVersion: 3,
|
|
812
828
|
gatewayUrl: config.gatewayUrl,
|
|
@@ -819,6 +835,10 @@ export function installManagedAppFiles({
|
|
|
819
835
|
models,
|
|
820
836
|
config,
|
|
821
837
|
),
|
|
838
|
+
modelDefaults: {
|
|
839
|
+
claude: models.find((model) => model.provider === "claude" && model.default)?.id ?? null,
|
|
840
|
+
chatgpt: models.find((model) => model.provider === "codex" && model.default)?.id ?? null,
|
|
841
|
+
},
|
|
822
842
|
models: models.map((model) => model.id),
|
|
823
843
|
updatedAt: generatedAt,
|
|
824
844
|
}, null, 2) + "\n", 0o600);
|
|
@@ -972,7 +992,6 @@ function installPinnedVendorApp(target, homeDir) {
|
|
|
972
992
|
homeDir, ".config", RUNTIME_BRAND.cli.configNamespace, "vendor", target, pin.version, pin.bundleName,
|
|
973
993
|
);
|
|
974
994
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
975
|
-
sweepStaleBundleArtifacts(path.dirname(destination), path.basename(destination));
|
|
976
995
|
replaceDirectory(destination, extractedApp);
|
|
977
996
|
return destination;
|
|
978
997
|
} finally {
|
|
@@ -1153,6 +1172,7 @@ function writeChatGPTConfig(
|
|
|
1153
1172
|
vendorCodexModels,
|
|
1154
1173
|
invocations = null,
|
|
1155
1174
|
generatedAt = new Date().toISOString(),
|
|
1175
|
+
previousManifest = null,
|
|
1156
1176
|
) {
|
|
1157
1177
|
const experimental = crossAppModelsEnabled(config);
|
|
1158
1178
|
const orderedModels = experimental
|
|
@@ -1168,8 +1188,28 @@ function writeChatGPTConfig(
|
|
|
1168
1188
|
const currentToml = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
|
|
1169
1189
|
const configuredModel = readTopLevelTomlString(currentToml, "model");
|
|
1170
1190
|
const defaultModelID = orderedModels.find((model) => model.provider === "codex" && model.default)?.id;
|
|
1171
|
-
const
|
|
1172
|
-
|
|
1191
|
+
const defaultModel = codexModels.find((model) => model.slug === defaultModelID);
|
|
1192
|
+
const previousDefaultModelID = previousManifest?.modelDefaults?.chatgpt;
|
|
1193
|
+
const followsPreviousManagedDefault = (
|
|
1194
|
+
typeof previousDefaultModelID === "string"
|
|
1195
|
+
&& configuredModel === previousDefaultModelID
|
|
1196
|
+
&& configuredModel !== defaultModelID
|
|
1197
|
+
);
|
|
1198
|
+
// Profiles written before the default-model field existed cannot distinguish
|
|
1199
|
+
// an old managed default from an explicit user choice. GPT-5.5 is the one
|
|
1200
|
+
// observed legacy default that ChatGPT 26.727 renders as an unsupported
|
|
1201
|
+
// compact-picker selection; migrate it once, while preserving every other
|
|
1202
|
+
// explicit model choice.
|
|
1203
|
+
const hasLegacyCompactPickerDefault = (
|
|
1204
|
+
previousDefaultModelID == null
|
|
1205
|
+
&& Number(previousManifest?.configVersion ?? 0) < CURRENT_CONFIG_VERSION
|
|
1206
|
+
&& configuredModel === "gpt-5.5"
|
|
1207
|
+
&& defaultModelID === "gpt-5.6-sol"
|
|
1208
|
+
);
|
|
1209
|
+
const selectedModel = (followsPreviousManagedDefault || hasLegacyCompactPickerDefault
|
|
1210
|
+
? defaultModel
|
|
1211
|
+
: codexModels.find((model) => model.slug === configuredModel))
|
|
1212
|
+
|| defaultModel
|
|
1173
1213
|
|| codexModels[0];
|
|
1174
1214
|
if (!selectedModel) throw new Error("no Codex models are available for Impel ChatGPT");
|
|
1175
1215
|
const configuredEffort = readTopLevelTomlString(currentToml, "model_reasoning_effort");
|
|
@@ -1529,6 +1569,12 @@ exec "$NODE" "$CLI" token${tenantArgs}
|
|
|
1529
1569
|
}
|
|
1530
1570
|
|
|
1531
1571
|
function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageName) {
|
|
1572
|
+
return withBundleMutationLock(paths.claude.launcher, () => (
|
|
1573
|
+
writeVendoredClaudeBundleLocked(paths, vendorPath, gatewayUrl, safeStorageName)
|
|
1574
|
+
));
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function writeVendoredClaudeBundleLocked(paths, vendorPath, gatewayUrl, safeStorageName) {
|
|
1532
1578
|
if (!vendorPath) throw new Error("Claude vendor app is unavailable");
|
|
1533
1579
|
if (!isVendableVendorApp("claude", vendorPath)) {
|
|
1534
1580
|
const bundleId = readBundleIdentifier(vendorPath);
|
|
@@ -1542,9 +1588,8 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
|
|
|
1542
1588
|
if (!executableName) throw new Error(`Claude executable is missing from ${vendorPath}`);
|
|
1543
1589
|
|
|
1544
1590
|
const bundle = paths.claude.launcher;
|
|
1545
|
-
const staging = `${bundle}.tmp-${process.pid}`;
|
|
1546
1591
|
sweepStaleBundleArtifacts(path.dirname(bundle), path.basename(bundle));
|
|
1547
|
-
|
|
1592
|
+
const staging = uniqueBundleArtifactPath(bundle, "tmp");
|
|
1548
1593
|
cloneAppBundle(vendorPath, staging);
|
|
1549
1594
|
try {
|
|
1550
1595
|
const plistPath = path.join(staging, "Contents", "Info.plist");
|
|
@@ -1626,9 +1671,9 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
|
|
|
1626
1671
|
}, null, 2) + "\n", 0o644);
|
|
1627
1672
|
|
|
1628
1673
|
signVendoredApp(staging, vendorExecutable, "Claude", signingIdentity);
|
|
1629
|
-
|
|
1674
|
+
replaceDirectoryUnlocked(bundle, staging);
|
|
1630
1675
|
} catch (error) {
|
|
1631
|
-
|
|
1676
|
+
removeBundleArtifact(staging);
|
|
1632
1677
|
throw error;
|
|
1633
1678
|
}
|
|
1634
1679
|
}
|
|
@@ -1642,6 +1687,12 @@ function claudeUsageCompatibilityEnabled(gatewayUrl) {
|
|
|
1642
1687
|
}
|
|
1643
1688
|
|
|
1644
1689
|
function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
|
|
1690
|
+
return withBundleMutationLock(paths.chatgpt.launcher, () => (
|
|
1691
|
+
writeVendoredChatGPTBundleLocked(paths, vendorPath, gatewayUrl, homeDir)
|
|
1692
|
+
));
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function writeVendoredChatGPTBundleLocked(paths, vendorPath, gatewayUrl, homeDir) {
|
|
1645
1696
|
if (!vendorPath) throw new Error("ChatGPT/Codex vendor app is unavailable");
|
|
1646
1697
|
const definition = APP_DEFINITIONS.chatgpt;
|
|
1647
1698
|
const executableName = definition.executableNames.find((name) => (
|
|
@@ -1650,11 +1701,10 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
|
|
|
1650
1701
|
if (!executableName) throw new Error(`ChatGPT/Codex executable is missing from ${vendorPath}`);
|
|
1651
1702
|
|
|
1652
1703
|
const bundle = paths.chatgpt.launcher;
|
|
1653
|
-
|
|
1704
|
+
sweepStaleBundleArtifacts(path.dirname(bundle), path.basename(bundle));
|
|
1705
|
+
const staging = uniqueBundleArtifactPath(bundle, "tmp");
|
|
1654
1706
|
const vendorBundleName = path.basename(vendorPath);
|
|
1655
1707
|
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
1708
|
cloneAppBundle(vendorPath, vendorBundle);
|
|
1659
1709
|
try {
|
|
1660
1710
|
const asarPath = path.join(vendorBundle, "Contents", "Resources", "app.asar");
|
|
@@ -1730,9 +1780,9 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
|
|
|
1730
1780
|
}, null, 2) + "\n", 0o644);
|
|
1731
1781
|
|
|
1732
1782
|
signVendoredApp(staging, vendorExecutable, "ChatGPT", signingIdentity);
|
|
1733
|
-
|
|
1783
|
+
replaceDirectoryUnlocked(bundle, staging);
|
|
1734
1784
|
} catch (error) {
|
|
1735
|
-
|
|
1785
|
+
removeBundleArtifact(staging);
|
|
1736
1786
|
throw error;
|
|
1737
1787
|
}
|
|
1738
1788
|
}
|
|
@@ -2536,24 +2586,105 @@ export function sweepStaleBundleArtifacts(dir, baseName) {
|
|
|
2536
2586
|
throw error;
|
|
2537
2587
|
}
|
|
2538
2588
|
const prefixes = [`${baseName}.tmp-`, `${baseName}.previous-`];
|
|
2539
|
-
const removed =
|
|
2540
|
-
for (const entry of
|
|
2541
|
-
|
|
2589
|
+
const removed = [];
|
|
2590
|
+
for (const entry of entries.filter((candidate) => prefixes.some((prefix) => candidate.startsWith(prefix)))) {
|
|
2591
|
+
if (removeBundleArtifact(path.join(dir, entry))) removed.push(entry);
|
|
2542
2592
|
}
|
|
2543
2593
|
return removed;
|
|
2544
2594
|
}
|
|
2545
2595
|
|
|
2546
|
-
function
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2596
|
+
function uniqueBundleArtifactPath(destination, kind) {
|
|
2597
|
+
return `${destination}.${kind}-${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
function removeBundleArtifact(candidate) {
|
|
2601
|
+
try {
|
|
2602
|
+
fs.rmSync(candidate, BUNDLE_ARTIFACT_REMOVE_OPTIONS);
|
|
2603
|
+
return !fs.existsSync(candidate);
|
|
2604
|
+
} catch {
|
|
2605
|
+
return false;
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
export function withBundleMutationLock(destination, run) {
|
|
2610
|
+
const lockPath = path.join(
|
|
2611
|
+
path.dirname(destination),
|
|
2612
|
+
`.${path.basename(destination)}.impel-install.lock`,
|
|
2613
|
+
);
|
|
2614
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
2615
|
+
|
|
2616
|
+
if (process.platform !== "darwin") {
|
|
2617
|
+
if (portableBundleMutationLocks.has(lockPath)) {
|
|
2618
|
+
throw new Error(`another Impel app update is already replacing ${path.basename(destination)}`);
|
|
2619
|
+
}
|
|
2620
|
+
const descriptor = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_RDWR, 0o600);
|
|
2621
|
+
portableBundleMutationLocks.add(lockPath);
|
|
2622
|
+
try {
|
|
2623
|
+
return run();
|
|
2624
|
+
} finally {
|
|
2625
|
+
portableBundleMutationLocks.delete(lockPath);
|
|
2626
|
+
fs.closeSync(descriptor);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
const deadline = Date.now() + BUNDLE_LOCK_WAIT_MS;
|
|
2631
|
+
let descriptor;
|
|
2632
|
+
while (descriptor === undefined) {
|
|
2633
|
+
try {
|
|
2634
|
+
descriptor = fs.openSync(
|
|
2635
|
+
lockPath,
|
|
2636
|
+
fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_NONBLOCK | MACOS_O_EXLOCK,
|
|
2637
|
+
0o600,
|
|
2638
|
+
);
|
|
2639
|
+
} catch (error) {
|
|
2640
|
+
if (!["EAGAIN", "EWOULDBLOCK"].includes(error?.code)) throw error;
|
|
2641
|
+
if (Date.now() >= deadline) {
|
|
2642
|
+
throw new Error(`timed out waiting for another Impel app update to replace ${path.basename(destination)}`);
|
|
2643
|
+
}
|
|
2644
|
+
Atomics.wait(BUNDLE_LOCK_SLEEP, 0, 0, BUNDLE_LOCK_POLL_MS);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
try {
|
|
2648
|
+
return run();
|
|
2649
|
+
} finally {
|
|
2650
|
+
fs.closeSync(descriptor);
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
function replaceDirectoryUnlocked(destination, staging, {
|
|
2655
|
+
removeArtifact = removeBundleArtifact,
|
|
2656
|
+
} = {}) {
|
|
2657
|
+
const backup = fs.existsSync(destination)
|
|
2658
|
+
? uniqueBundleArtifactPath(destination, "previous")
|
|
2659
|
+
: null;
|
|
2660
|
+
if (backup) fs.renameSync(destination, backup);
|
|
2550
2661
|
try {
|
|
2551
2662
|
fs.renameSync(staging, destination);
|
|
2552
|
-
fs.rmSync(backup, { recursive: true, force: true });
|
|
2553
2663
|
} catch (error) {
|
|
2554
|
-
if (!fs.existsSync(destination) && fs.existsSync(backup))
|
|
2664
|
+
if (backup && !fs.existsSync(destination) && fs.existsSync(backup)) {
|
|
2665
|
+
fs.renameSync(backup, destination);
|
|
2666
|
+
}
|
|
2555
2667
|
throw error;
|
|
2556
2668
|
}
|
|
2669
|
+
|
|
2670
|
+
// The new bundle is committed once staging has been renamed into place.
|
|
2671
|
+
// Finder, LaunchServices, or a just-closed helper can briefly keep changing
|
|
2672
|
+
// the old tree and make recursive removal end with ENOTEMPTY. Do not turn
|
|
2673
|
+
// that post-commit housekeeping race into a failed tenant update: the next
|
|
2674
|
+
// sweep will retry any backup that remains.
|
|
2675
|
+
if (backup) {
|
|
2676
|
+
try {
|
|
2677
|
+
removeArtifact(backup);
|
|
2678
|
+
} catch {
|
|
2679
|
+
// Cleanup is best-effort after the committed rename.
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
export function replaceDirectory(destination, staging, options = {}) {
|
|
2685
|
+
return withBundleMutationLock(destination, () => (
|
|
2686
|
+
replaceDirectoryUnlocked(destination, staging, options)
|
|
2687
|
+
));
|
|
2557
2688
|
}
|
|
2558
2689
|
|
|
2559
2690
|
function isGatewayModel(model) {
|