impel-cli 0.18.15 → 0.18.16
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/README.md +55 -0
- package/docs/experimental-managed-cursor.md +205 -0
- package/package.json +2 -1
- package/src/apps.js +213 -43
- package/src/commands/apps.js +43 -8
- package/src/commands/converge.js +18 -3
- package/src/commands/cursorExperimental.js +192 -0
- package/src/commands/experimental.js +8 -2
- package/src/commands/launch.js +11 -2
- package/src/commands/status.js +3 -2
- package/src/commands/update.js +23 -11
- package/src/cursorLocal.js +1554 -0
- package/src/macSetup.js +20 -38
- package/src/provisioning.js +10 -4
- package/src/skills.js +24 -8
- package/src/updates.js +57 -10
- package/src/vendorCliBinaries.js +121 -0
- package/src/vendorCliVersions.js +7 -0
- package/src/windowsApps.js +64 -19
- package/src/windowsSetup.js +7 -4
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
|
|
@@ -81,17 +92,29 @@ export const PINNED_VENDOR_APPS = Object.freeze({
|
|
|
81
92
|
}),
|
|
82
93
|
}),
|
|
83
94
|
chatgpt: Object.freeze({
|
|
84
|
-
version: "26.
|
|
85
|
-
codexVersion: "0.146.0-alpha.
|
|
95
|
+
version: "26.727.40816",
|
|
96
|
+
codexVersion: "0.146.0-alpha.9.2",
|
|
86
97
|
bundleName: "ChatGPT.app",
|
|
98
|
+
windows: Object.freeze({
|
|
99
|
+
storeProductId: "9PLM9XGG6VKS",
|
|
100
|
+
packageName: "OpenAI.Codex",
|
|
101
|
+
// The Microsoft Store manifest identifies this exact reviewed build.
|
|
102
|
+
// Keep one Windows package version so the manifest contract and local
|
|
103
|
+
// AppX validation cannot silently drift apart again.
|
|
104
|
+
packageVersion: "26.727.6591.0",
|
|
105
|
+
codexVersion: "0.146.0-alpha.9.2",
|
|
106
|
+
publisherId: "2p2nqsd0c76g0",
|
|
107
|
+
executable: "app\\ChatGPT.exe",
|
|
108
|
+
updateManifestUrl: "https://persistent.oaistatic.com/codex-app-prod/windows-store-update.json",
|
|
109
|
+
}),
|
|
87
110
|
downloads: Object.freeze({
|
|
88
111
|
arm64: Object.freeze({
|
|
89
|
-
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.
|
|
90
|
-
sha256: "
|
|
112
|
+
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.727.40816.zip",
|
|
113
|
+
sha256: "fdbede9b8a28b5bf3bbf1213fa04291724faa6ed2d1d61bb04853bbdfebce219",
|
|
91
114
|
}),
|
|
92
115
|
x64: Object.freeze({
|
|
93
|
-
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.
|
|
94
|
-
sha256: "
|
|
116
|
+
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.727.40816.zip",
|
|
117
|
+
sha256: "12622d916a3993d61fb52a318dcff7af7449eea5070cdd7e094da29f380e3374",
|
|
95
118
|
}),
|
|
96
119
|
}),
|
|
97
120
|
}),
|
|
@@ -236,7 +259,10 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
|
236
259
|
// and the tenant manifest so unsupported hosts keep Chat and Code usable.
|
|
237
260
|
// 25: remove the unproven Windows Cowork capability gate from beta.0 profiles
|
|
238
261
|
// and restore the reviewed always-enabled managed policy.
|
|
239
|
-
|
|
262
|
+
// 26: remember the managed provider defaults and move legacy GPT-5.5 profiles
|
|
263
|
+
// onto GPT-5.6 Sol so ChatGPT's compact Work picker does not fall back to its
|
|
264
|
+
// unsupported-selection "Reset to default" treatment.
|
|
265
|
+
export const CURRENT_CONFIG_VERSION = 26;
|
|
240
266
|
|
|
241
267
|
// Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
|
|
242
268
|
// helper rebranding, and signing. A vendored bundle is rebuilt only when this
|
|
@@ -245,7 +271,7 @@ export const CURRENT_CONFIG_VERSION = 25;
|
|
|
245
271
|
// — which is what made every `impel update` re-trigger macOS permission
|
|
246
272
|
// prompts. Bump this ONLY when a code change alters the bytes of a built
|
|
247
273
|
// bundle; leave it alone for changes that don't touch bundle contents.
|
|
248
|
-
export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-
|
|
274
|
+
export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-31.2";
|
|
249
275
|
|
|
250
276
|
/** Parse the tenant's install manifest, or null when absent/corrupt. */
|
|
251
277
|
export function readTenantManifest(homeDir = os.homedir(), tenantId = null) {
|
|
@@ -323,7 +349,7 @@ export function bundleIsCurrent(status, {
|
|
|
323
349
|
compatibility.vendorSignaturesPreserved === true
|
|
324
350
|
&& compatibility.resourceRoot === CHATGPT_RESOURCE_ROOT
|
|
325
351
|
&& compatibility.patches?.fastModeForGatewayAuth > 0
|
|
326
|
-
&& compatibility.patches?.tenantDisplayNamePreload ===
|
|
352
|
+
&& compatibility.patches?.tenantDisplayNamePreload === 2
|
|
327
353
|
)),
|
|
328
354
|
);
|
|
329
355
|
}
|
|
@@ -679,7 +705,7 @@ export function migrateLegacyClaudeAppSessions(userData, homeDir = os.homedir())
|
|
|
679
705
|
return copied;
|
|
680
706
|
}
|
|
681
707
|
|
|
682
|
-
export async function fetchGatewayModels(config, fetchImpl = fetch) {
|
|
708
|
+
export async function fetchGatewayModels(config, fetchImpl = fetch, { allowEmpty = false } = {}) {
|
|
683
709
|
const controller = new AbortController();
|
|
684
710
|
const timeout = setTimeout(() => controller.abort(), 20000);
|
|
685
711
|
try {
|
|
@@ -697,7 +723,9 @@ export async function fetchGatewayModels(config, fetchImpl = fetch) {
|
|
|
697
723
|
}
|
|
698
724
|
if (!Array.isArray(payload?.data)) throw new Error("response has no model data array");
|
|
699
725
|
const models = payload.data.filter(isGatewayModel);
|
|
700
|
-
if (models.length === 0
|
|
726
|
+
if (models.length === 0 && !(allowEmpty && isExplicitNoSeatCatalog(payload))) {
|
|
727
|
+
throw new Error("gateway returned no supported models");
|
|
728
|
+
}
|
|
701
729
|
return { models, source: "gateway", version: payload.version ?? null };
|
|
702
730
|
} finally {
|
|
703
731
|
clearTimeout(timeout);
|
|
@@ -733,6 +761,7 @@ export function installManagedAppFiles({
|
|
|
733
761
|
target,
|
|
734
762
|
vendorPaths[target] || findVendorApp(target, homeDir),
|
|
735
763
|
]));
|
|
764
|
+
const previousManifest = readTenantManifest(homeDir, config.tenantId);
|
|
736
765
|
const vendorCodexModels = targets.includes("chatgpt")
|
|
737
766
|
? readVendorCodexModels(resolvedVendorPaths.chatgpt)
|
|
738
767
|
: new Map();
|
|
@@ -783,6 +812,7 @@ export function installManagedAppFiles({
|
|
|
783
812
|
vendorCodexModels,
|
|
784
813
|
chatgptInvocations,
|
|
785
814
|
generatedAt,
|
|
815
|
+
previousManifest,
|
|
786
816
|
);
|
|
787
817
|
if (RUNTIME_BRAND.features.sessions) ensureCodexSessionHooks(paths.chatgpt.codexHome, config.tenantId, "codex_desktop");
|
|
788
818
|
}
|
|
@@ -794,7 +824,7 @@ export function installManagedAppFiles({
|
|
|
794
824
|
}
|
|
795
825
|
|
|
796
826
|
writeAtomic(path.join(paths.tenantRoot, "manifest.json"), JSON.stringify({
|
|
797
|
-
schemaVersion:
|
|
827
|
+
schemaVersion: 7,
|
|
798
828
|
configVersion: CURRENT_CONFIG_VERSION,
|
|
799
829
|
modelCatalogVersion: 3,
|
|
800
830
|
gatewayUrl: config.gatewayUrl,
|
|
@@ -807,6 +837,10 @@ export function installManagedAppFiles({
|
|
|
807
837
|
models,
|
|
808
838
|
config,
|
|
809
839
|
),
|
|
840
|
+
modelDefaults: {
|
|
841
|
+
claude: models.find((model) => model.provider === "claude" && model.default)?.id ?? null,
|
|
842
|
+
chatgpt: models.find((model) => model.provider === "codex" && model.default)?.id ?? null,
|
|
843
|
+
},
|
|
810
844
|
models: models.map((model) => model.id),
|
|
811
845
|
updatedAt: generatedAt,
|
|
812
846
|
}, null, 2) + "\n", 0o600);
|
|
@@ -960,7 +994,6 @@ function installPinnedVendorApp(target, homeDir) {
|
|
|
960
994
|
homeDir, ".config", RUNTIME_BRAND.cli.configNamespace, "vendor", target, pin.version, pin.bundleName,
|
|
961
995
|
);
|
|
962
996
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
963
|
-
sweepStaleBundleArtifacts(path.dirname(destination), path.basename(destination));
|
|
964
997
|
replaceDirectory(destination, extractedApp);
|
|
965
998
|
return destination;
|
|
966
999
|
} finally {
|
|
@@ -1141,6 +1174,7 @@ function writeChatGPTConfig(
|
|
|
1141
1174
|
vendorCodexModels,
|
|
1142
1175
|
invocations = null,
|
|
1143
1176
|
generatedAt = new Date().toISOString(),
|
|
1177
|
+
previousManifest = null,
|
|
1144
1178
|
) {
|
|
1145
1179
|
const experimental = crossAppModelsEnabled(config);
|
|
1146
1180
|
const orderedModels = experimental
|
|
@@ -1156,8 +1190,28 @@ function writeChatGPTConfig(
|
|
|
1156
1190
|
const currentToml = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
|
|
1157
1191
|
const configuredModel = readTopLevelTomlString(currentToml, "model");
|
|
1158
1192
|
const defaultModelID = orderedModels.find((model) => model.provider === "codex" && model.default)?.id;
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1193
|
+
const defaultModel = codexModels.find((model) => model.slug === defaultModelID);
|
|
1194
|
+
const previousDefaultModelID = previousManifest?.modelDefaults?.chatgpt;
|
|
1195
|
+
const followsPreviousManagedDefault = (
|
|
1196
|
+
typeof previousDefaultModelID === "string"
|
|
1197
|
+
&& configuredModel === previousDefaultModelID
|
|
1198
|
+
&& configuredModel !== defaultModelID
|
|
1199
|
+
);
|
|
1200
|
+
// Profiles written before the default-model field existed cannot distinguish
|
|
1201
|
+
// an old managed default from an explicit user choice. GPT-5.5 is the one
|
|
1202
|
+
// observed legacy default that ChatGPT 26.727 renders as an unsupported
|
|
1203
|
+
// compact-picker selection; migrate it once, while preserving every other
|
|
1204
|
+
// explicit model choice.
|
|
1205
|
+
const hasLegacyCompactPickerDefault = (
|
|
1206
|
+
previousDefaultModelID == null
|
|
1207
|
+
&& Number(previousManifest?.configVersion ?? 0) < CURRENT_CONFIG_VERSION
|
|
1208
|
+
&& configuredModel === "gpt-5.5"
|
|
1209
|
+
&& defaultModelID === "gpt-5.6-sol"
|
|
1210
|
+
);
|
|
1211
|
+
const selectedModel = (followsPreviousManagedDefault || hasLegacyCompactPickerDefault
|
|
1212
|
+
? defaultModel
|
|
1213
|
+
: codexModels.find((model) => model.slug === configuredModel))
|
|
1214
|
+
|| defaultModel
|
|
1161
1215
|
|| codexModels[0];
|
|
1162
1216
|
if (!selectedModel) throw new Error("no Codex models are available for Impel ChatGPT");
|
|
1163
1217
|
const configuredEffort = readTopLevelTomlString(currentToml, "model_reasoning_effort");
|
|
@@ -1517,6 +1571,12 @@ exec "$NODE" "$CLI" token${tenantArgs}
|
|
|
1517
1571
|
}
|
|
1518
1572
|
|
|
1519
1573
|
function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageName) {
|
|
1574
|
+
return withBundleMutationLock(paths.claude.launcher, () => (
|
|
1575
|
+
writeVendoredClaudeBundleLocked(paths, vendorPath, gatewayUrl, safeStorageName)
|
|
1576
|
+
));
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
function writeVendoredClaudeBundleLocked(paths, vendorPath, gatewayUrl, safeStorageName) {
|
|
1520
1580
|
if (!vendorPath) throw new Error("Claude vendor app is unavailable");
|
|
1521
1581
|
if (!isVendableVendorApp("claude", vendorPath)) {
|
|
1522
1582
|
const bundleId = readBundleIdentifier(vendorPath);
|
|
@@ -1530,9 +1590,8 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
|
|
|
1530
1590
|
if (!executableName) throw new Error(`Claude executable is missing from ${vendorPath}`);
|
|
1531
1591
|
|
|
1532
1592
|
const bundle = paths.claude.launcher;
|
|
1533
|
-
const staging = `${bundle}.tmp-${process.pid}`;
|
|
1534
1593
|
sweepStaleBundleArtifacts(path.dirname(bundle), path.basename(bundle));
|
|
1535
|
-
|
|
1594
|
+
const staging = uniqueBundleArtifactPath(bundle, "tmp");
|
|
1536
1595
|
cloneAppBundle(vendorPath, staging);
|
|
1537
1596
|
try {
|
|
1538
1597
|
const plistPath = path.join(staging, "Contents", "Info.plist");
|
|
@@ -1614,9 +1673,9 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
|
|
|
1614
1673
|
}, null, 2) + "\n", 0o644);
|
|
1615
1674
|
|
|
1616
1675
|
signVendoredApp(staging, vendorExecutable, "Claude", signingIdentity);
|
|
1617
|
-
|
|
1676
|
+
replaceDirectoryUnlocked(bundle, staging);
|
|
1618
1677
|
} catch (error) {
|
|
1619
|
-
|
|
1678
|
+
removeBundleArtifact(staging);
|
|
1620
1679
|
throw error;
|
|
1621
1680
|
}
|
|
1622
1681
|
}
|
|
@@ -1630,6 +1689,12 @@ function claudeUsageCompatibilityEnabled(gatewayUrl) {
|
|
|
1630
1689
|
}
|
|
1631
1690
|
|
|
1632
1691
|
function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
|
|
1692
|
+
return withBundleMutationLock(paths.chatgpt.launcher, () => (
|
|
1693
|
+
writeVendoredChatGPTBundleLocked(paths, vendorPath, gatewayUrl, homeDir)
|
|
1694
|
+
));
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
function writeVendoredChatGPTBundleLocked(paths, vendorPath, gatewayUrl, homeDir) {
|
|
1633
1698
|
if (!vendorPath) throw new Error("ChatGPT/Codex vendor app is unavailable");
|
|
1634
1699
|
const definition = APP_DEFINITIONS.chatgpt;
|
|
1635
1700
|
const executableName = definition.executableNames.find((name) => (
|
|
@@ -1638,11 +1703,10 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
|
|
|
1638
1703
|
if (!executableName) throw new Error(`ChatGPT/Codex executable is missing from ${vendorPath}`);
|
|
1639
1704
|
|
|
1640
1705
|
const bundle = paths.chatgpt.launcher;
|
|
1641
|
-
|
|
1706
|
+
sweepStaleBundleArtifacts(path.dirname(bundle), path.basename(bundle));
|
|
1707
|
+
const staging = uniqueBundleArtifactPath(bundle, "tmp");
|
|
1642
1708
|
const vendorBundleName = path.basename(vendorPath);
|
|
1643
1709
|
const vendorBundle = path.join(staging, "Contents", "Resources", vendorBundleName);
|
|
1644
|
-
sweepStaleBundleArtifacts(path.dirname(bundle), path.basename(bundle));
|
|
1645
|
-
fs.rmSync(staging, { recursive: true, force: true });
|
|
1646
1710
|
cloneAppBundle(vendorPath, vendorBundle);
|
|
1647
1711
|
try {
|
|
1648
1712
|
const asarPath = path.join(vendorBundle, "Contents", "Resources", "app.asar");
|
|
@@ -1710,7 +1774,7 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
|
|
|
1710
1774
|
patches: {
|
|
1711
1775
|
fastModeForGatewayAuth: fastModePatchCount,
|
|
1712
1776
|
desktopAPIForGatewayAuth: 0,
|
|
1713
|
-
tenantDisplayNamePreload:
|
|
1777
|
+
tenantDisplayNamePreload: 2,
|
|
1714
1778
|
},
|
|
1715
1779
|
vendorSignaturesPreserved: true,
|
|
1716
1780
|
resourceRoot: CHATGPT_RESOURCE_ROOT,
|
|
@@ -1718,9 +1782,9 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
|
|
|
1718
1782
|
}, null, 2) + "\n", 0o644);
|
|
1719
1783
|
|
|
1720
1784
|
signVendoredApp(staging, vendorExecutable, "ChatGPT", signingIdentity);
|
|
1721
|
-
|
|
1785
|
+
replaceDirectoryUnlocked(bundle, staging);
|
|
1722
1786
|
} catch (error) {
|
|
1723
|
-
|
|
1787
|
+
removeBundleArtifact(staging);
|
|
1724
1788
|
throw error;
|
|
1725
1789
|
}
|
|
1726
1790
|
}
|
|
@@ -2213,6 +2277,7 @@ HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
|
2213
2277
|
VENDOR_APP="$HERE/../Resources/"${shellQuote(vendorBundleName)}
|
|
2214
2278
|
export CODEX_HOME=${shellQuote(paths.chatgpt.codexHome)}
|
|
2215
2279
|
export CODEX_AUTHAPI_BASE_URL=${shellQuote(codexAccountBaseUrl)}
|
|
2280
|
+
export CODEX_SPARKLE_ENABLED=false
|
|
2216
2281
|
unset CODEX_ACCESS_TOKEN CODEX_API_KEY OPENAI_API_KEY OPENAI_BASE_URL
|
|
2217
2282
|
secure_codex_home() {
|
|
2218
2283
|
[ ! -L "$CODEX_HOME" ] || exit 1
|
|
@@ -2236,6 +2301,7 @@ secure_codex_home
|
|
|
2236
2301
|
BROWSER_DATA=${shellQuote(paths.chatgpt.browserData)}
|
|
2237
2302
|
mkdir -p "$BROWSER_DATA"
|
|
2238
2303
|
export IMPEL_APP_DISPLAY_NAME=${shellQuote(paths.chatgpt.displayName)}
|
|
2304
|
+
export IMPEL_APP_BUNDLE_ID=${shellQuote(paths.chatgpt.bundleIdentifier)}
|
|
2239
2305
|
PRELOAD="$HERE/../Resources/impel-chatgpt-preload.cjs"
|
|
2240
2306
|
export NODE_OPTIONS="--require=\\\"$PRELOAD\\\""
|
|
2241
2307
|
# Electron resolves resources from the Impel wrapper when the nested app is
|
|
@@ -2250,13 +2316,14 @@ function vendoredChatGPTDisplayNamePreload() {
|
|
|
2250
2316
|
return `"use strict";
|
|
2251
2317
|
|
|
2252
2318
|
const displayName = process.env.IMPEL_APP_DISPLAY_NAME;
|
|
2253
|
-
|
|
2319
|
+
const bundleIdentifier = process.env.IMPEL_APP_BUNDLE_ID;
|
|
2320
|
+
if (process.type === "browser" && displayName && bundleIdentifier) {
|
|
2254
2321
|
const Module = require("node:module");
|
|
2255
2322
|
const path = require("node:path");
|
|
2256
2323
|
const load = Module._load;
|
|
2257
2324
|
let objc;
|
|
2258
2325
|
let appKit;
|
|
2259
|
-
let
|
|
2326
|
+
let launchServicesIdentified = false;
|
|
2260
2327
|
let activationPolicyRefreshed = false;
|
|
2261
2328
|
|
|
2262
2329
|
const loadNativeBridge = () => {
|
|
@@ -2268,20 +2335,31 @@ if (process.type === "browser" && displayName) {
|
|
|
2268
2335
|
return { objc, appKit };
|
|
2269
2336
|
};
|
|
2270
2337
|
|
|
2271
|
-
const
|
|
2272
|
-
if (
|
|
2338
|
+
const setLaunchServicesApplicationIdentity = () => {
|
|
2339
|
+
if (launchServicesIdentified) return;
|
|
2273
2340
|
try {
|
|
2274
2341
|
const native = loadNativeBridge();
|
|
2275
2342
|
new native.objc.NobjcLibrary(
|
|
2276
2343
|
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices",
|
|
2277
2344
|
);
|
|
2278
2345
|
const title = native.appKit.NSString.stringWithUTF8String$(displayName);
|
|
2346
|
+
const identifier = native.appKit.NSString.stringWithUTF8String$(bundleIdentifier);
|
|
2279
2347
|
const info = native.appKit.NSMutableDictionary.dictionaryWithDictionary$(
|
|
2280
2348
|
native.appKit.NSBundle.mainBundle().infoDictionary(),
|
|
2281
2349
|
);
|
|
2282
2350
|
for (const key of ["CFBundleName", "CFBundleDisplayName"]) {
|
|
2283
2351
|
info.setObject$forKey$(title, native.appKit.NSString.stringWithUTF8String$(key));
|
|
2284
2352
|
}
|
|
2353
|
+
// The signed vendor executable runs from the nested ChatGPT.app, so
|
|
2354
|
+
// LaunchServices initially checks it in as com.openai.codex. When the
|
|
2355
|
+
// native app is also open, Dock groups both processes under ChatGPT even
|
|
2356
|
+
// though LSDisplayName was already tenant-branded. Check the process in
|
|
2357
|
+
// again with the unique outer-wrapper identity to keep each tenant's
|
|
2358
|
+
// Dock item distinct while preserving the vendor executable signature.
|
|
2359
|
+
info.setObject$forKey$(
|
|
2360
|
+
identifier,
|
|
2361
|
+
native.appKit.NSString.stringWithUTF8String$("CFBundleIdentifier"),
|
|
2362
|
+
);
|
|
2285
2363
|
native.objc.callFunction(
|
|
2286
2364
|
"_LSSetApplicationLaunchServicesServerConnectionStatus",
|
|
2287
2365
|
{ returns: "v", args: ["Q", "@"] },
|
|
@@ -2305,7 +2383,7 @@ if (process.type === "browser" && displayName) {
|
|
|
2305
2383
|
title,
|
|
2306
2384
|
null,
|
|
2307
2385
|
);
|
|
2308
|
-
|
|
2386
|
+
launchServicesIdentified = status === 0;
|
|
2309
2387
|
} catch {
|
|
2310
2388
|
// Keep the signed vendor app usable if LaunchServices changes this
|
|
2311
2389
|
// private-but-established process-title API in a future macOS release.
|
|
@@ -2340,7 +2418,7 @@ if (process.type === "browser" && displayName) {
|
|
|
2340
2418
|
const setName = loaded.app.setName.bind(loaded.app);
|
|
2341
2419
|
setName(displayName);
|
|
2342
2420
|
loaded.app.setName = () => setName(displayName);
|
|
2343
|
-
|
|
2421
|
+
setLaunchServicesApplicationIdentity();
|
|
2344
2422
|
setNativeApplicationMenuTitle();
|
|
2345
2423
|
const setApplicationMenu = loaded.Menu.setApplicationMenu.bind(loaded.Menu);
|
|
2346
2424
|
loaded.Menu.setApplicationMenu = (menu) => {
|
|
@@ -2351,11 +2429,11 @@ if (process.type === "browser" && displayName) {
|
|
|
2351
2429
|
loaded.app.setActivationPolicy("accessory");
|
|
2352
2430
|
setTimeout(() => {
|
|
2353
2431
|
loaded.app.setActivationPolicy("regular");
|
|
2354
|
-
|
|
2355
|
-
|
|
2432
|
+
launchServicesIdentified = false;
|
|
2433
|
+
setLaunchServicesApplicationIdentity();
|
|
2356
2434
|
setApplicationMenu(menu);
|
|
2357
2435
|
setNativeApplicationMenuTitle();
|
|
2358
|
-
},
|
|
2436
|
+
}, 100);
|
|
2359
2437
|
}
|
|
2360
2438
|
setTimeout(setNativeApplicationMenuTitle, 0);
|
|
2361
2439
|
return result;
|
|
@@ -2523,30 +2601,122 @@ export function sweepStaleBundleArtifacts(dir, baseName) {
|
|
|
2523
2601
|
throw error;
|
|
2524
2602
|
}
|
|
2525
2603
|
const prefixes = [`${baseName}.tmp-`, `${baseName}.previous-`];
|
|
2526
|
-
const removed =
|
|
2527
|
-
for (const entry of
|
|
2528
|
-
|
|
2604
|
+
const removed = [];
|
|
2605
|
+
for (const entry of entries.filter((candidate) => prefixes.some((prefix) => candidate.startsWith(prefix)))) {
|
|
2606
|
+
if (removeBundleArtifact(path.join(dir, entry))) removed.push(entry);
|
|
2529
2607
|
}
|
|
2530
2608
|
return removed;
|
|
2531
2609
|
}
|
|
2532
2610
|
|
|
2533
|
-
function
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2611
|
+
function uniqueBundleArtifactPath(destination, kind) {
|
|
2612
|
+
return `${destination}.${kind}-${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
function removeBundleArtifact(candidate) {
|
|
2616
|
+
try {
|
|
2617
|
+
fs.rmSync(candidate, BUNDLE_ARTIFACT_REMOVE_OPTIONS);
|
|
2618
|
+
return !fs.existsSync(candidate);
|
|
2619
|
+
} catch {
|
|
2620
|
+
return false;
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
export function withBundleMutationLock(destination, run) {
|
|
2625
|
+
const lockPath = path.join(
|
|
2626
|
+
path.dirname(destination),
|
|
2627
|
+
`.${path.basename(destination)}.impel-install.lock`,
|
|
2628
|
+
);
|
|
2629
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
2630
|
+
|
|
2631
|
+
if (process.platform !== "darwin") {
|
|
2632
|
+
if (portableBundleMutationLocks.has(lockPath)) {
|
|
2633
|
+
throw new Error(`another Impel app update is already replacing ${path.basename(destination)}`);
|
|
2634
|
+
}
|
|
2635
|
+
const descriptor = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_RDWR, 0o600);
|
|
2636
|
+
portableBundleMutationLocks.add(lockPath);
|
|
2637
|
+
try {
|
|
2638
|
+
return run();
|
|
2639
|
+
} finally {
|
|
2640
|
+
portableBundleMutationLocks.delete(lockPath);
|
|
2641
|
+
fs.closeSync(descriptor);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
const deadline = Date.now() + BUNDLE_LOCK_WAIT_MS;
|
|
2646
|
+
let descriptor;
|
|
2647
|
+
while (descriptor === undefined) {
|
|
2648
|
+
try {
|
|
2649
|
+
descriptor = fs.openSync(
|
|
2650
|
+
lockPath,
|
|
2651
|
+
fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_NONBLOCK | MACOS_O_EXLOCK,
|
|
2652
|
+
0o600,
|
|
2653
|
+
);
|
|
2654
|
+
} catch (error) {
|
|
2655
|
+
if (!["EAGAIN", "EWOULDBLOCK"].includes(error?.code)) throw error;
|
|
2656
|
+
if (Date.now() >= deadline) {
|
|
2657
|
+
throw new Error(`timed out waiting for another Impel app update to replace ${path.basename(destination)}`);
|
|
2658
|
+
}
|
|
2659
|
+
Atomics.wait(BUNDLE_LOCK_SLEEP, 0, 0, BUNDLE_LOCK_POLL_MS);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
try {
|
|
2663
|
+
return run();
|
|
2664
|
+
} finally {
|
|
2665
|
+
fs.closeSync(descriptor);
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
function replaceDirectoryUnlocked(destination, staging, {
|
|
2670
|
+
removeArtifact = removeBundleArtifact,
|
|
2671
|
+
} = {}) {
|
|
2672
|
+
const backup = fs.existsSync(destination)
|
|
2673
|
+
? uniqueBundleArtifactPath(destination, "previous")
|
|
2674
|
+
: null;
|
|
2675
|
+
if (backup) fs.renameSync(destination, backup);
|
|
2537
2676
|
try {
|
|
2538
2677
|
fs.renameSync(staging, destination);
|
|
2539
|
-
fs.rmSync(backup, { recursive: true, force: true });
|
|
2540
2678
|
} catch (error) {
|
|
2541
|
-
if (!fs.existsSync(destination) && fs.existsSync(backup))
|
|
2679
|
+
if (backup && !fs.existsSync(destination) && fs.existsSync(backup)) {
|
|
2680
|
+
fs.renameSync(backup, destination);
|
|
2681
|
+
}
|
|
2542
2682
|
throw error;
|
|
2543
2683
|
}
|
|
2684
|
+
|
|
2685
|
+
// The new bundle is committed once staging has been renamed into place.
|
|
2686
|
+
// Finder, LaunchServices, or a just-closed helper can briefly keep changing
|
|
2687
|
+
// the old tree and make recursive removal end with ENOTEMPTY. Do not turn
|
|
2688
|
+
// that post-commit housekeeping race into a failed tenant update: the next
|
|
2689
|
+
// sweep will retry any backup that remains.
|
|
2690
|
+
if (backup) {
|
|
2691
|
+
try {
|
|
2692
|
+
removeArtifact(backup);
|
|
2693
|
+
} catch {
|
|
2694
|
+
// Cleanup is best-effort after the committed rename.
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
export function replaceDirectory(destination, staging, options = {}) {
|
|
2700
|
+
return withBundleMutationLock(destination, () => (
|
|
2701
|
+
replaceDirectoryUnlocked(destination, staging, options)
|
|
2702
|
+
));
|
|
2544
2703
|
}
|
|
2545
2704
|
|
|
2546
2705
|
function isGatewayModel(model) {
|
|
2547
2706
|
return model && typeof model.id === "string" && (model.provider === "claude" || model.provider === "codex");
|
|
2548
2707
|
}
|
|
2549
2708
|
|
|
2709
|
+
function isExplicitNoSeatCatalog(payload) {
|
|
2710
|
+
const statuses = payload?.provider_status;
|
|
2711
|
+
return Boolean(statuses
|
|
2712
|
+
&& typeof statuses === "object"
|
|
2713
|
+
&& !Array.isArray(statuses)
|
|
2714
|
+
&& ["claude", "codex"].every((provider) => (
|
|
2715
|
+
statuses[provider]?.state === "no_seat"
|
|
2716
|
+
&& statuses[provider]?.routable === false
|
|
2717
|
+
)));
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2550
2720
|
function writeAtomic(target, contents, mode) {
|
|
2551
2721
|
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
2552
2722
|
if (fs.existsSync(target) && fs.readFileSync(target, "utf8") === contents) {
|
package/src/commands/apps.js
CHANGED
|
@@ -76,7 +76,7 @@ export function selectCatalogAppTargets(
|
|
|
76
76
|
requestedTargets,
|
|
77
77
|
models,
|
|
78
78
|
config,
|
|
79
|
-
{ log = console.log } = {},
|
|
79
|
+
{ log = console.log, allowNone = false } = {},
|
|
80
80
|
) {
|
|
81
81
|
const supported = appTargetsSupportedByModels(
|
|
82
82
|
requestedTargets,
|
|
@@ -87,6 +87,14 @@ export function selectCatalogAppTargets(
|
|
|
87
87
|
const unavailable = requestedTargets.filter((target) => !supportedSet.has(target));
|
|
88
88
|
if (unavailable.length === 0) return supported;
|
|
89
89
|
|
|
90
|
+
if (allowNone && supported.length === 0) {
|
|
91
|
+
for (const target of unavailable) {
|
|
92
|
+
const capability = APP_CAPABILITIES[target];
|
|
93
|
+
log(`Skipping ${capability.label}: the selected tenant has no ${capability.provider} models.`);
|
|
94
|
+
}
|
|
95
|
+
return supported;
|
|
96
|
+
}
|
|
97
|
+
|
|
90
98
|
if (requestedTargets.length === 1 || supported.length === 0) {
|
|
91
99
|
const target = unavailable[0];
|
|
92
100
|
const capability = APP_CAPABILITIES[target];
|
|
@@ -297,9 +305,9 @@ export function openManagedLauncher(launcher, {
|
|
|
297
305
|
return false;
|
|
298
306
|
}
|
|
299
307
|
|
|
300
|
-
async function fetchWindowsCatalog(config, io) {
|
|
308
|
+
async function fetchWindowsCatalog(config, io, { allowEmpty = false } = {}) {
|
|
301
309
|
try {
|
|
302
|
-
return await io.fetchModels(config);
|
|
310
|
+
return await io.fetchModels(config, undefined, { allowEmpty });
|
|
303
311
|
} catch (error) {
|
|
304
312
|
throw new Error(`tenant model catalog is unavailable (${redactSecretText(error.message)}); the Impel app profiles were not changed`);
|
|
305
313
|
}
|
|
@@ -374,12 +382,25 @@ export async function reconcileWindowsTenantApps({
|
|
|
374
382
|
...overrides,
|
|
375
383
|
};
|
|
376
384
|
const config = explicitTenantAppConfig(baseConfig, tenant, targets);
|
|
377
|
-
const catalog = await fetchWindowsCatalog(config, io);
|
|
378
|
-
const actionTargets = selectCatalogAppTargets(targets, catalog.models, config, {
|
|
385
|
+
const catalog = await fetchWindowsCatalog(config, io, { allowEmpty: true });
|
|
386
|
+
const actionTargets = selectCatalogAppTargets(targets, catalog.models, config, {
|
|
387
|
+
log: io.log,
|
|
388
|
+
allowNone: true,
|
|
389
|
+
});
|
|
379
390
|
const supported = new Set(actionTargets);
|
|
380
391
|
const unsupported = targets.filter((target) => !supported.has(target));
|
|
381
392
|
const userData = io.claudeUserData(environment, config.tenantId);
|
|
382
393
|
const paths = appPaths(homeDir, config.tenantId, { claudeUserData: userData, tenantName: config.tenantName });
|
|
394
|
+
if (actionTargets.length === 0) {
|
|
395
|
+
return {
|
|
396
|
+
tenantId: config.tenantId,
|
|
397
|
+
targets: [],
|
|
398
|
+
unsupported,
|
|
399
|
+
paths,
|
|
400
|
+
verification: {},
|
|
401
|
+
failed: [],
|
|
402
|
+
};
|
|
403
|
+
}
|
|
383
404
|
const vendorPaths = {};
|
|
384
405
|
const preparedTargets = new Set(skipVendor ? actionTargets : skipVendorTargets);
|
|
385
406
|
|
|
@@ -501,13 +522,28 @@ export async function reconcileMacTenantApps({
|
|
|
501
522
|
const config = explicitTenantAppConfig(baseConfig, tenant, targets);
|
|
502
523
|
let catalog;
|
|
503
524
|
try {
|
|
504
|
-
catalog = await io.fetchModels(config);
|
|
525
|
+
catalog = await io.fetchModels(config, undefined, { allowEmpty: true });
|
|
505
526
|
} catch (error) {
|
|
506
527
|
throw new Error(`tenant model catalog is unavailable (${redactSecretText(error?.message || error)}); no Impel app files were changed`);
|
|
507
528
|
}
|
|
508
|
-
const actionTargets = selectCatalogAppTargets(targets, catalog.models, config, {
|
|
529
|
+
const actionTargets = selectCatalogAppTargets(targets, catalog.models, config, {
|
|
530
|
+
log: io.log,
|
|
531
|
+
allowNone: true,
|
|
532
|
+
});
|
|
509
533
|
const supported = new Set(actionTargets);
|
|
510
534
|
const unsupported = targets.filter((target) => !supported.has(target));
|
|
535
|
+
const paths = appPaths(homeDir, config.tenantId, { tenantName: config.tenantName });
|
|
536
|
+
if (actionTargets.length === 0) {
|
|
537
|
+
return {
|
|
538
|
+
tenantId: config.tenantId,
|
|
539
|
+
targets: [],
|
|
540
|
+
unsupported,
|
|
541
|
+
paths,
|
|
542
|
+
verification: {},
|
|
543
|
+
installed: [],
|
|
544
|
+
failed: [],
|
|
545
|
+
};
|
|
546
|
+
}
|
|
511
547
|
const vendorPaths = {};
|
|
512
548
|
const statuses = io.status(actionTargets, homeDir, config.tenantId, config.tenantName);
|
|
513
549
|
for (const status of statuses) vendorPaths[status.target] ||= status.vendorPath;
|
|
@@ -549,7 +585,6 @@ export async function reconcileMacTenantApps({
|
|
|
549
585
|
vendorPaths,
|
|
550
586
|
writeBundles: staleTargets,
|
|
551
587
|
}) : [];
|
|
552
|
-
const paths = appPaths(homeDir, config.tenantId, { tenantName: config.tenantName });
|
|
553
588
|
const agentItems = [];
|
|
554
589
|
for (const item of installed) {
|
|
555
590
|
const { client, env, label } = appSkillTarget(item.target, paths);
|
package/src/commands/converge.js
CHANGED
|
@@ -13,6 +13,8 @@ import { promptText } from "../prompt.js";
|
|
|
13
13
|
import { describeCliFailure, preparePlatformClis } from "../platformSetup.js";
|
|
14
14
|
import { restoreNativeProfiles } from "./use.js";
|
|
15
15
|
|
|
16
|
+
const RETRYABLE_TENANT_DISCOVERY_ERROR = /could not reach|request timed out|fetch failed|ECONNRESET|ECONNABORTED|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|network error|socket/iu;
|
|
17
|
+
|
|
16
18
|
function confirmed(answer) {
|
|
17
19
|
return /^(?:y|yes)$/iu.test(String(answer || "").trim());
|
|
18
20
|
}
|
|
@@ -44,6 +46,7 @@ export async function cmdConverge(argv = [], overrides = {}) {
|
|
|
44
46
|
platform: process.platform,
|
|
45
47
|
environment: process.env,
|
|
46
48
|
preparePlatformClis,
|
|
49
|
+
discoveryLogger: console,
|
|
47
50
|
...overrides,
|
|
48
51
|
};
|
|
49
52
|
if (overrides.prepareWindowsClis && !overrides.preparePlatformClis) {
|
|
@@ -58,10 +61,22 @@ export async function cmdConverge(argv = [], overrides = {}) {
|
|
|
58
61
|
let listing;
|
|
59
62
|
try {
|
|
60
63
|
listing = await io.fetchTenants(config);
|
|
64
|
+
// Tenant discovery is the entry gate for the whole convergence. A single
|
|
65
|
+
// transient timeout should not force a human to rerun every update step.
|
|
61
66
|
} catch (error) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
if (!RETRYABLE_TENANT_DISCOVERY_ERROR.test(String(error?.message || error))) {
|
|
68
|
+
console.error(`impel update: tenant discovery failed (${redactSecretText(error?.message || error)})`);
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
io.discoveryLogger.log("Tenants: discovery request failed transiently; retrying once…");
|
|
73
|
+
try {
|
|
74
|
+
listing = await io.fetchTenants(config);
|
|
75
|
+
} catch (retryError) {
|
|
76
|
+
console.error(`impel update: tenant discovery failed (${redactSecretText(retryError?.message || retryError)})`);
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
65
80
|
}
|
|
66
81
|
const selected = selectDefaultTenant(listing, { currentTenantId: config.tenantId });
|
|
67
82
|
config.tenantId = selected.id;
|