impel-cli 0.18.15-beta.9 → 0.18.15
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 +0 -49
- package/package.json +1 -2
- package/src/agents.js +173 -8
- package/src/apps.js +43 -211
- package/src/commands/apps.js +8 -43
- package/src/commands/converge.js +3 -18
- package/src/commands/experimental.js +2 -8
- package/src/commands/launch.js +2 -11
- package/src/commands/status.js +2 -3
- package/src/commands/update.js +11 -23
- package/src/macSetup.js +38 -20
- package/src/provisioning.js +4 -10
- package/src/skills.js +8 -24
- package/src/updates.js +10 -57
- package/src/windowsApps.js +17 -61
- package/src/windowsSetup.js +4 -7
- package/docs/experimental-managed-cursor.md +0 -205
- package/src/commands/cursorExperimental.js +0 -192
- package/src/cursorLocal.js +0 -1554
- package/src/vendorCliBinaries.js +0 -121
- package/src/vendorCliVersions.js +0 -7
package/src/apps.js
CHANGED
|
@@ -37,17 +37,6 @@ 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();
|
|
51
40
|
|
|
52
41
|
// Vendor updates can change minified renderer contracts without notice. Each
|
|
53
42
|
// CLI release therefore installs and accepts only the exact app builds tested
|
|
@@ -92,27 +81,17 @@ export const PINNED_VENDOR_APPS = Object.freeze({
|
|
|
92
81
|
}),
|
|
93
82
|
}),
|
|
94
83
|
chatgpt: Object.freeze({
|
|
95
|
-
version: "26.
|
|
96
|
-
codexVersion: "0.146.0-alpha.
|
|
84
|
+
version: "26.721.41059",
|
|
85
|
+
codexVersion: "0.146.0-alpha.3.1",
|
|
97
86
|
bundleName: "ChatGPT.app",
|
|
98
|
-
windows: Object.freeze({
|
|
99
|
-
storeProductId: "9PLM9XGG6VKS",
|
|
100
|
-
packageName: "OpenAI.Codex",
|
|
101
|
-
packageVersion: "26.721.11231.0",
|
|
102
|
-
advertisedPackageVersion: "26.727.4816.0",
|
|
103
|
-
codexVersion: "0.146.0-alpha.3.1",
|
|
104
|
-
publisherId: "2p2nqsd0c76g0",
|
|
105
|
-
executable: "app\\ChatGPT.exe",
|
|
106
|
-
updateManifestUrl: "https://persistent.oaistatic.com/codex-app-prod/windows-store-update.json",
|
|
107
|
-
}),
|
|
108
87
|
downloads: Object.freeze({
|
|
109
88
|
arm64: Object.freeze({
|
|
110
|
-
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.
|
|
111
|
-
sha256: "
|
|
89
|
+
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.721.41059.zip",
|
|
90
|
+
sha256: "e2b45056f3d1f8ab90f7f16249bfb5a40d09d0f8099f12ca0d8d7a8fdf9108ce",
|
|
112
91
|
}),
|
|
113
92
|
x64: Object.freeze({
|
|
114
|
-
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.
|
|
115
|
-
sha256: "
|
|
93
|
+
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.721.41059.zip",
|
|
94
|
+
sha256: "f00b09d9e1f5138f055fa972e5243991b493423c41f5d78951dc2e21a84089f4",
|
|
116
95
|
}),
|
|
117
96
|
}),
|
|
118
97
|
}),
|
|
@@ -257,10 +236,7 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
|
257
236
|
// and the tenant manifest so unsupported hosts keep Chat and Code usable.
|
|
258
237
|
// 25: remove the unproven Windows Cowork capability gate from beta.0 profiles
|
|
259
238
|
// and restore the reviewed always-enabled managed policy.
|
|
260
|
-
|
|
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;
|
|
239
|
+
export const CURRENT_CONFIG_VERSION = 25;
|
|
264
240
|
|
|
265
241
|
// Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
|
|
266
242
|
// helper rebranding, and signing. A vendored bundle is rebuilt only when this
|
|
@@ -269,7 +245,7 @@ export const CURRENT_CONFIG_VERSION = 26;
|
|
|
269
245
|
// — which is what made every `impel update` re-trigger macOS permission
|
|
270
246
|
// prompts. Bump this ONLY when a code change alters the bytes of a built
|
|
271
247
|
// bundle; leave it alone for changes that don't touch bundle contents.
|
|
272
|
-
export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-
|
|
248
|
+
export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-25.1";
|
|
273
249
|
|
|
274
250
|
/** Parse the tenant's install manifest, or null when absent/corrupt. */
|
|
275
251
|
export function readTenantManifest(homeDir = os.homedir(), tenantId = null) {
|
|
@@ -347,7 +323,7 @@ export function bundleIsCurrent(status, {
|
|
|
347
323
|
compatibility.vendorSignaturesPreserved === true
|
|
348
324
|
&& compatibility.resourceRoot === CHATGPT_RESOURCE_ROOT
|
|
349
325
|
&& compatibility.patches?.fastModeForGatewayAuth > 0
|
|
350
|
-
&& compatibility.patches?.tenantDisplayNamePreload ===
|
|
326
|
+
&& compatibility.patches?.tenantDisplayNamePreload === 1
|
|
351
327
|
)),
|
|
352
328
|
);
|
|
353
329
|
}
|
|
@@ -703,7 +679,7 @@ export function migrateLegacyClaudeAppSessions(userData, homeDir = os.homedir())
|
|
|
703
679
|
return copied;
|
|
704
680
|
}
|
|
705
681
|
|
|
706
|
-
export async function fetchGatewayModels(config, fetchImpl = fetch
|
|
682
|
+
export async function fetchGatewayModels(config, fetchImpl = fetch) {
|
|
707
683
|
const controller = new AbortController();
|
|
708
684
|
const timeout = setTimeout(() => controller.abort(), 20000);
|
|
709
685
|
try {
|
|
@@ -721,9 +697,7 @@ export async function fetchGatewayModels(config, fetchImpl = fetch, { allowEmpty
|
|
|
721
697
|
}
|
|
722
698
|
if (!Array.isArray(payload?.data)) throw new Error("response has no model data array");
|
|
723
699
|
const models = payload.data.filter(isGatewayModel);
|
|
724
|
-
if (models.length === 0
|
|
725
|
-
throw new Error("gateway returned no supported models");
|
|
726
|
-
}
|
|
700
|
+
if (models.length === 0) throw new Error("gateway returned no supported models");
|
|
727
701
|
return { models, source: "gateway", version: payload.version ?? null };
|
|
728
702
|
} finally {
|
|
729
703
|
clearTimeout(timeout);
|
|
@@ -759,7 +733,6 @@ export function installManagedAppFiles({
|
|
|
759
733
|
target,
|
|
760
734
|
vendorPaths[target] || findVendorApp(target, homeDir),
|
|
761
735
|
]));
|
|
762
|
-
const previousManifest = readTenantManifest(homeDir, config.tenantId);
|
|
763
736
|
const vendorCodexModels = targets.includes("chatgpt")
|
|
764
737
|
? readVendorCodexModels(resolvedVendorPaths.chatgpt)
|
|
765
738
|
: new Map();
|
|
@@ -810,7 +783,6 @@ export function installManagedAppFiles({
|
|
|
810
783
|
vendorCodexModels,
|
|
811
784
|
chatgptInvocations,
|
|
812
785
|
generatedAt,
|
|
813
|
-
previousManifest,
|
|
814
786
|
);
|
|
815
787
|
if (RUNTIME_BRAND.features.sessions) ensureCodexSessionHooks(paths.chatgpt.codexHome, config.tenantId, "codex_desktop");
|
|
816
788
|
}
|
|
@@ -822,7 +794,7 @@ export function installManagedAppFiles({
|
|
|
822
794
|
}
|
|
823
795
|
|
|
824
796
|
writeAtomic(path.join(paths.tenantRoot, "manifest.json"), JSON.stringify({
|
|
825
|
-
schemaVersion:
|
|
797
|
+
schemaVersion: 6,
|
|
826
798
|
configVersion: CURRENT_CONFIG_VERSION,
|
|
827
799
|
modelCatalogVersion: 3,
|
|
828
800
|
gatewayUrl: config.gatewayUrl,
|
|
@@ -835,10 +807,6 @@ export function installManagedAppFiles({
|
|
|
835
807
|
models,
|
|
836
808
|
config,
|
|
837
809
|
),
|
|
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
|
-
},
|
|
842
810
|
models: models.map((model) => model.id),
|
|
843
811
|
updatedAt: generatedAt,
|
|
844
812
|
}, null, 2) + "\n", 0o600);
|
|
@@ -992,6 +960,7 @@ function installPinnedVendorApp(target, homeDir) {
|
|
|
992
960
|
homeDir, ".config", RUNTIME_BRAND.cli.configNamespace, "vendor", target, pin.version, pin.bundleName,
|
|
993
961
|
);
|
|
994
962
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
963
|
+
sweepStaleBundleArtifacts(path.dirname(destination), path.basename(destination));
|
|
995
964
|
replaceDirectory(destination, extractedApp);
|
|
996
965
|
return destination;
|
|
997
966
|
} finally {
|
|
@@ -1172,7 +1141,6 @@ function writeChatGPTConfig(
|
|
|
1172
1141
|
vendorCodexModels,
|
|
1173
1142
|
invocations = null,
|
|
1174
1143
|
generatedAt = new Date().toISOString(),
|
|
1175
|
-
previousManifest = null,
|
|
1176
1144
|
) {
|
|
1177
1145
|
const experimental = crossAppModelsEnabled(config);
|
|
1178
1146
|
const orderedModels = experimental
|
|
@@ -1188,28 +1156,8 @@ function writeChatGPTConfig(
|
|
|
1188
1156
|
const currentToml = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
|
|
1189
1157
|
const configuredModel = readTopLevelTomlString(currentToml, "model");
|
|
1190
1158
|
const defaultModelID = orderedModels.find((model) => model.provider === "codex" && model.default)?.id;
|
|
1191
|
-
const
|
|
1192
|
-
|
|
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
|
|
1159
|
+
const selectedModel = codexModels.find((model) => model.slug === configuredModel)
|
|
1160
|
+
|| codexModels.find((model) => model.slug === defaultModelID)
|
|
1213
1161
|
|| codexModels[0];
|
|
1214
1162
|
if (!selectedModel) throw new Error("no Codex models are available for Impel ChatGPT");
|
|
1215
1163
|
const configuredEffort = readTopLevelTomlString(currentToml, "model_reasoning_effort");
|
|
@@ -1569,12 +1517,6 @@ exec "$NODE" "$CLI" token${tenantArgs}
|
|
|
1569
1517
|
}
|
|
1570
1518
|
|
|
1571
1519
|
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) {
|
|
1578
1520
|
if (!vendorPath) throw new Error("Claude vendor app is unavailable");
|
|
1579
1521
|
if (!isVendableVendorApp("claude", vendorPath)) {
|
|
1580
1522
|
const bundleId = readBundleIdentifier(vendorPath);
|
|
@@ -1588,8 +1530,9 @@ function writeVendoredClaudeBundleLocked(paths, vendorPath, gatewayUrl, safeStor
|
|
|
1588
1530
|
if (!executableName) throw new Error(`Claude executable is missing from ${vendorPath}`);
|
|
1589
1531
|
|
|
1590
1532
|
const bundle = paths.claude.launcher;
|
|
1533
|
+
const staging = `${bundle}.tmp-${process.pid}`;
|
|
1591
1534
|
sweepStaleBundleArtifacts(path.dirname(bundle), path.basename(bundle));
|
|
1592
|
-
|
|
1535
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
1593
1536
|
cloneAppBundle(vendorPath, staging);
|
|
1594
1537
|
try {
|
|
1595
1538
|
const plistPath = path.join(staging, "Contents", "Info.plist");
|
|
@@ -1671,9 +1614,9 @@ function writeVendoredClaudeBundleLocked(paths, vendorPath, gatewayUrl, safeStor
|
|
|
1671
1614
|
}, null, 2) + "\n", 0o644);
|
|
1672
1615
|
|
|
1673
1616
|
signVendoredApp(staging, vendorExecutable, "Claude", signingIdentity);
|
|
1674
|
-
|
|
1617
|
+
replaceDirectory(bundle, staging);
|
|
1675
1618
|
} catch (error) {
|
|
1676
|
-
|
|
1619
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
1677
1620
|
throw error;
|
|
1678
1621
|
}
|
|
1679
1622
|
}
|
|
@@ -1687,12 +1630,6 @@ function claudeUsageCompatibilityEnabled(gatewayUrl) {
|
|
|
1687
1630
|
}
|
|
1688
1631
|
|
|
1689
1632
|
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) {
|
|
1696
1633
|
if (!vendorPath) throw new Error("ChatGPT/Codex vendor app is unavailable");
|
|
1697
1634
|
const definition = APP_DEFINITIONS.chatgpt;
|
|
1698
1635
|
const executableName = definition.executableNames.find((name) => (
|
|
@@ -1701,10 +1638,11 @@ function writeVendoredChatGPTBundleLocked(paths, vendorPath, gatewayUrl, homeDir
|
|
|
1701
1638
|
if (!executableName) throw new Error(`ChatGPT/Codex executable is missing from ${vendorPath}`);
|
|
1702
1639
|
|
|
1703
1640
|
const bundle = paths.chatgpt.launcher;
|
|
1704
|
-
|
|
1705
|
-
const staging = uniqueBundleArtifactPath(bundle, "tmp");
|
|
1641
|
+
const staging = `${bundle}.tmp-${process.pid}`;
|
|
1706
1642
|
const vendorBundleName = path.basename(vendorPath);
|
|
1707
1643
|
const vendorBundle = path.join(staging, "Contents", "Resources", vendorBundleName);
|
|
1644
|
+
sweepStaleBundleArtifacts(path.dirname(bundle), path.basename(bundle));
|
|
1645
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
1708
1646
|
cloneAppBundle(vendorPath, vendorBundle);
|
|
1709
1647
|
try {
|
|
1710
1648
|
const asarPath = path.join(vendorBundle, "Contents", "Resources", "app.asar");
|
|
@@ -1772,7 +1710,7 @@ function writeVendoredChatGPTBundleLocked(paths, vendorPath, gatewayUrl, homeDir
|
|
|
1772
1710
|
patches: {
|
|
1773
1711
|
fastModeForGatewayAuth: fastModePatchCount,
|
|
1774
1712
|
desktopAPIForGatewayAuth: 0,
|
|
1775
|
-
tenantDisplayNamePreload:
|
|
1713
|
+
tenantDisplayNamePreload: 1,
|
|
1776
1714
|
},
|
|
1777
1715
|
vendorSignaturesPreserved: true,
|
|
1778
1716
|
resourceRoot: CHATGPT_RESOURCE_ROOT,
|
|
@@ -1780,9 +1718,9 @@ function writeVendoredChatGPTBundleLocked(paths, vendorPath, gatewayUrl, homeDir
|
|
|
1780
1718
|
}, null, 2) + "\n", 0o644);
|
|
1781
1719
|
|
|
1782
1720
|
signVendoredApp(staging, vendorExecutable, "ChatGPT", signingIdentity);
|
|
1783
|
-
|
|
1721
|
+
replaceDirectory(bundle, staging);
|
|
1784
1722
|
} catch (error) {
|
|
1785
|
-
|
|
1723
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
1786
1724
|
throw error;
|
|
1787
1725
|
}
|
|
1788
1726
|
}
|
|
@@ -2275,7 +2213,6 @@ HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
|
2275
2213
|
VENDOR_APP="$HERE/../Resources/"${shellQuote(vendorBundleName)}
|
|
2276
2214
|
export CODEX_HOME=${shellQuote(paths.chatgpt.codexHome)}
|
|
2277
2215
|
export CODEX_AUTHAPI_BASE_URL=${shellQuote(codexAccountBaseUrl)}
|
|
2278
|
-
export CODEX_SPARKLE_ENABLED=false
|
|
2279
2216
|
unset CODEX_ACCESS_TOKEN CODEX_API_KEY OPENAI_API_KEY OPENAI_BASE_URL
|
|
2280
2217
|
secure_codex_home() {
|
|
2281
2218
|
[ ! -L "$CODEX_HOME" ] || exit 1
|
|
@@ -2299,7 +2236,6 @@ secure_codex_home
|
|
|
2299
2236
|
BROWSER_DATA=${shellQuote(paths.chatgpt.browserData)}
|
|
2300
2237
|
mkdir -p "$BROWSER_DATA"
|
|
2301
2238
|
export IMPEL_APP_DISPLAY_NAME=${shellQuote(paths.chatgpt.displayName)}
|
|
2302
|
-
export IMPEL_APP_BUNDLE_ID=${shellQuote(paths.chatgpt.bundleIdentifier)}
|
|
2303
2239
|
PRELOAD="$HERE/../Resources/impel-chatgpt-preload.cjs"
|
|
2304
2240
|
export NODE_OPTIONS="--require=\\\"$PRELOAD\\\""
|
|
2305
2241
|
# Electron resolves resources from the Impel wrapper when the nested app is
|
|
@@ -2314,14 +2250,13 @@ function vendoredChatGPTDisplayNamePreload() {
|
|
|
2314
2250
|
return `"use strict";
|
|
2315
2251
|
|
|
2316
2252
|
const displayName = process.env.IMPEL_APP_DISPLAY_NAME;
|
|
2317
|
-
|
|
2318
|
-
if (process.type === "browser" && displayName && bundleIdentifier) {
|
|
2253
|
+
if (process.type === "browser" && displayName) {
|
|
2319
2254
|
const Module = require("node:module");
|
|
2320
2255
|
const path = require("node:path");
|
|
2321
2256
|
const load = Module._load;
|
|
2322
2257
|
let objc;
|
|
2323
2258
|
let appKit;
|
|
2324
|
-
let
|
|
2259
|
+
let launchServicesNamed = false;
|
|
2325
2260
|
let activationPolicyRefreshed = false;
|
|
2326
2261
|
|
|
2327
2262
|
const loadNativeBridge = () => {
|
|
@@ -2333,31 +2268,20 @@ if (process.type === "browser" && displayName && bundleIdentifier) {
|
|
|
2333
2268
|
return { objc, appKit };
|
|
2334
2269
|
};
|
|
2335
2270
|
|
|
2336
|
-
const
|
|
2337
|
-
if (
|
|
2271
|
+
const setLaunchServicesApplicationName = () => {
|
|
2272
|
+
if (launchServicesNamed) return;
|
|
2338
2273
|
try {
|
|
2339
2274
|
const native = loadNativeBridge();
|
|
2340
2275
|
new native.objc.NobjcLibrary(
|
|
2341
2276
|
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices",
|
|
2342
2277
|
);
|
|
2343
2278
|
const title = native.appKit.NSString.stringWithUTF8String$(displayName);
|
|
2344
|
-
const identifier = native.appKit.NSString.stringWithUTF8String$(bundleIdentifier);
|
|
2345
2279
|
const info = native.appKit.NSMutableDictionary.dictionaryWithDictionary$(
|
|
2346
2280
|
native.appKit.NSBundle.mainBundle().infoDictionary(),
|
|
2347
2281
|
);
|
|
2348
2282
|
for (const key of ["CFBundleName", "CFBundleDisplayName"]) {
|
|
2349
2283
|
info.setObject$forKey$(title, native.appKit.NSString.stringWithUTF8String$(key));
|
|
2350
2284
|
}
|
|
2351
|
-
// The signed vendor executable runs from the nested ChatGPT.app, so
|
|
2352
|
-
// LaunchServices initially checks it in as com.openai.codex. When the
|
|
2353
|
-
// native app is also open, Dock groups both processes under ChatGPT even
|
|
2354
|
-
// though LSDisplayName was already tenant-branded. Check the process in
|
|
2355
|
-
// again with the unique outer-wrapper identity to keep each tenant's
|
|
2356
|
-
// Dock item distinct while preserving the vendor executable signature.
|
|
2357
|
-
info.setObject$forKey$(
|
|
2358
|
-
identifier,
|
|
2359
|
-
native.appKit.NSString.stringWithUTF8String$("CFBundleIdentifier"),
|
|
2360
|
-
);
|
|
2361
2285
|
native.objc.callFunction(
|
|
2362
2286
|
"_LSSetApplicationLaunchServicesServerConnectionStatus",
|
|
2363
2287
|
{ returns: "v", args: ["Q", "@"] },
|
|
@@ -2381,7 +2305,7 @@ if (process.type === "browser" && displayName && bundleIdentifier) {
|
|
|
2381
2305
|
title,
|
|
2382
2306
|
null,
|
|
2383
2307
|
);
|
|
2384
|
-
|
|
2308
|
+
launchServicesNamed = status === 0;
|
|
2385
2309
|
} catch {
|
|
2386
2310
|
// Keep the signed vendor app usable if LaunchServices changes this
|
|
2387
2311
|
// private-but-established process-title API in a future macOS release.
|
|
@@ -2416,7 +2340,7 @@ if (process.type === "browser" && displayName && bundleIdentifier) {
|
|
|
2416
2340
|
const setName = loaded.app.setName.bind(loaded.app);
|
|
2417
2341
|
setName(displayName);
|
|
2418
2342
|
loaded.app.setName = () => setName(displayName);
|
|
2419
|
-
|
|
2343
|
+
setLaunchServicesApplicationName();
|
|
2420
2344
|
setNativeApplicationMenuTitle();
|
|
2421
2345
|
const setApplicationMenu = loaded.Menu.setApplicationMenu.bind(loaded.Menu);
|
|
2422
2346
|
loaded.Menu.setApplicationMenu = (menu) => {
|
|
@@ -2427,11 +2351,11 @@ if (process.type === "browser" && displayName && bundleIdentifier) {
|
|
|
2427
2351
|
loaded.app.setActivationPolicy("accessory");
|
|
2428
2352
|
setTimeout(() => {
|
|
2429
2353
|
loaded.app.setActivationPolicy("regular");
|
|
2430
|
-
|
|
2431
|
-
|
|
2354
|
+
launchServicesNamed = false;
|
|
2355
|
+
setLaunchServicesApplicationName();
|
|
2432
2356
|
setApplicationMenu(menu);
|
|
2433
2357
|
setNativeApplicationMenuTitle();
|
|
2434
|
-
},
|
|
2358
|
+
}, 0);
|
|
2435
2359
|
}
|
|
2436
2360
|
setTimeout(setNativeApplicationMenuTitle, 0);
|
|
2437
2361
|
return result;
|
|
@@ -2599,122 +2523,30 @@ export function sweepStaleBundleArtifacts(dir, baseName) {
|
|
|
2599
2523
|
throw error;
|
|
2600
2524
|
}
|
|
2601
2525
|
const prefixes = [`${baseName}.tmp-`, `${baseName}.previous-`];
|
|
2602
|
-
const removed =
|
|
2603
|
-
for (const entry of
|
|
2604
|
-
|
|
2526
|
+
const removed = entries.filter((entry) => prefixes.some((prefix) => entry.startsWith(prefix)));
|
|
2527
|
+
for (const entry of removed) {
|
|
2528
|
+
fs.rmSync(path.join(dir, entry), { recursive: true, force: true });
|
|
2605
2529
|
}
|
|
2606
2530
|
return removed;
|
|
2607
2531
|
}
|
|
2608
2532
|
|
|
2609
|
-
function
|
|
2610
|
-
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
|
-
function removeBundleArtifact(candidate) {
|
|
2614
|
-
try {
|
|
2615
|
-
fs.rmSync(candidate, BUNDLE_ARTIFACT_REMOVE_OPTIONS);
|
|
2616
|
-
return !fs.existsSync(candidate);
|
|
2617
|
-
} catch {
|
|
2618
|
-
return false;
|
|
2619
|
-
}
|
|
2620
|
-
}
|
|
2621
|
-
|
|
2622
|
-
export function withBundleMutationLock(destination, run) {
|
|
2623
|
-
const lockPath = path.join(
|
|
2624
|
-
path.dirname(destination),
|
|
2625
|
-
`.${path.basename(destination)}.impel-install.lock`,
|
|
2626
|
-
);
|
|
2627
|
-
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
2628
|
-
|
|
2629
|
-
if (process.platform !== "darwin") {
|
|
2630
|
-
if (portableBundleMutationLocks.has(lockPath)) {
|
|
2631
|
-
throw new Error(`another Impel app update is already replacing ${path.basename(destination)}`);
|
|
2632
|
-
}
|
|
2633
|
-
const descriptor = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_RDWR, 0o600);
|
|
2634
|
-
portableBundleMutationLocks.add(lockPath);
|
|
2635
|
-
try {
|
|
2636
|
-
return run();
|
|
2637
|
-
} finally {
|
|
2638
|
-
portableBundleMutationLocks.delete(lockPath);
|
|
2639
|
-
fs.closeSync(descriptor);
|
|
2640
|
-
}
|
|
2641
|
-
}
|
|
2642
|
-
|
|
2643
|
-
const deadline = Date.now() + BUNDLE_LOCK_WAIT_MS;
|
|
2644
|
-
let descriptor;
|
|
2645
|
-
while (descriptor === undefined) {
|
|
2646
|
-
try {
|
|
2647
|
-
descriptor = fs.openSync(
|
|
2648
|
-
lockPath,
|
|
2649
|
-
fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_NONBLOCK | MACOS_O_EXLOCK,
|
|
2650
|
-
0o600,
|
|
2651
|
-
);
|
|
2652
|
-
} catch (error) {
|
|
2653
|
-
if (!["EAGAIN", "EWOULDBLOCK"].includes(error?.code)) throw error;
|
|
2654
|
-
if (Date.now() >= deadline) {
|
|
2655
|
-
throw new Error(`timed out waiting for another Impel app update to replace ${path.basename(destination)}`);
|
|
2656
|
-
}
|
|
2657
|
-
Atomics.wait(BUNDLE_LOCK_SLEEP, 0, 0, BUNDLE_LOCK_POLL_MS);
|
|
2658
|
-
}
|
|
2659
|
-
}
|
|
2660
|
-
try {
|
|
2661
|
-
return run();
|
|
2662
|
-
} finally {
|
|
2663
|
-
fs.closeSync(descriptor);
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
function replaceDirectoryUnlocked(destination, staging, {
|
|
2668
|
-
removeArtifact = removeBundleArtifact,
|
|
2669
|
-
} = {}) {
|
|
2670
|
-
const backup = fs.existsSync(destination)
|
|
2671
|
-
? uniqueBundleArtifactPath(destination, "previous")
|
|
2672
|
-
: null;
|
|
2673
|
-
if (backup) fs.renameSync(destination, backup);
|
|
2533
|
+
function replaceDirectory(destination, staging) {
|
|
2534
|
+
const backup = `${destination}.previous-${process.pid}`;
|
|
2535
|
+
fs.rmSync(backup, { recursive: true, force: true });
|
|
2536
|
+
if (fs.existsSync(destination)) fs.renameSync(destination, backup);
|
|
2674
2537
|
try {
|
|
2675
2538
|
fs.renameSync(staging, destination);
|
|
2539
|
+
fs.rmSync(backup, { recursive: true, force: true });
|
|
2676
2540
|
} catch (error) {
|
|
2677
|
-
if (
|
|
2678
|
-
fs.renameSync(backup, destination);
|
|
2679
|
-
}
|
|
2541
|
+
if (!fs.existsSync(destination) && fs.existsSync(backup)) fs.renameSync(backup, destination);
|
|
2680
2542
|
throw error;
|
|
2681
2543
|
}
|
|
2682
|
-
|
|
2683
|
-
// The new bundle is committed once staging has been renamed into place.
|
|
2684
|
-
// Finder, LaunchServices, or a just-closed helper can briefly keep changing
|
|
2685
|
-
// the old tree and make recursive removal end with ENOTEMPTY. Do not turn
|
|
2686
|
-
// that post-commit housekeeping race into a failed tenant update: the next
|
|
2687
|
-
// sweep will retry any backup that remains.
|
|
2688
|
-
if (backup) {
|
|
2689
|
-
try {
|
|
2690
|
-
removeArtifact(backup);
|
|
2691
|
-
} catch {
|
|
2692
|
-
// Cleanup is best-effort after the committed rename.
|
|
2693
|
-
}
|
|
2694
|
-
}
|
|
2695
|
-
}
|
|
2696
|
-
|
|
2697
|
-
export function replaceDirectory(destination, staging, options = {}) {
|
|
2698
|
-
return withBundleMutationLock(destination, () => (
|
|
2699
|
-
replaceDirectoryUnlocked(destination, staging, options)
|
|
2700
|
-
));
|
|
2701
2544
|
}
|
|
2702
2545
|
|
|
2703
2546
|
function isGatewayModel(model) {
|
|
2704
2547
|
return model && typeof model.id === "string" && (model.provider === "claude" || model.provider === "codex");
|
|
2705
2548
|
}
|
|
2706
2549
|
|
|
2707
|
-
function isExplicitNoSeatCatalog(payload) {
|
|
2708
|
-
const statuses = payload?.provider_status;
|
|
2709
|
-
return Boolean(statuses
|
|
2710
|
-
&& typeof statuses === "object"
|
|
2711
|
-
&& !Array.isArray(statuses)
|
|
2712
|
-
&& ["claude", "codex"].every((provider) => (
|
|
2713
|
-
statuses[provider]?.state === "no_seat"
|
|
2714
|
-
&& statuses[provider]?.routable === false
|
|
2715
|
-
)));
|
|
2716
|
-
}
|
|
2717
|
-
|
|
2718
2550
|
function writeAtomic(target, contents, mode) {
|
|
2719
2551
|
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
2720
2552
|
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 } = {},
|
|
80
80
|
) {
|
|
81
81
|
const supported = appTargetsSupportedByModels(
|
|
82
82
|
requestedTargets,
|
|
@@ -87,14 +87,6 @@ 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
|
-
|
|
98
90
|
if (requestedTargets.length === 1 || supported.length === 0) {
|
|
99
91
|
const target = unavailable[0];
|
|
100
92
|
const capability = APP_CAPABILITIES[target];
|
|
@@ -305,9 +297,9 @@ export function openManagedLauncher(launcher, {
|
|
|
305
297
|
return false;
|
|
306
298
|
}
|
|
307
299
|
|
|
308
|
-
async function fetchWindowsCatalog(config, io
|
|
300
|
+
async function fetchWindowsCatalog(config, io) {
|
|
309
301
|
try {
|
|
310
|
-
return await io.fetchModels(config
|
|
302
|
+
return await io.fetchModels(config);
|
|
311
303
|
} catch (error) {
|
|
312
304
|
throw new Error(`tenant model catalog is unavailable (${redactSecretText(error.message)}); the Impel app profiles were not changed`);
|
|
313
305
|
}
|
|
@@ -382,25 +374,12 @@ export async function reconcileWindowsTenantApps({
|
|
|
382
374
|
...overrides,
|
|
383
375
|
};
|
|
384
376
|
const config = explicitTenantAppConfig(baseConfig, tenant, targets);
|
|
385
|
-
const catalog = await fetchWindowsCatalog(config, io
|
|
386
|
-
const actionTargets = selectCatalogAppTargets(targets, catalog.models, config, {
|
|
387
|
-
log: io.log,
|
|
388
|
-
allowNone: true,
|
|
389
|
-
});
|
|
377
|
+
const catalog = await fetchWindowsCatalog(config, io);
|
|
378
|
+
const actionTargets = selectCatalogAppTargets(targets, catalog.models, config, { log: io.log });
|
|
390
379
|
const supported = new Set(actionTargets);
|
|
391
380
|
const unsupported = targets.filter((target) => !supported.has(target));
|
|
392
381
|
const userData = io.claudeUserData(environment, config.tenantId);
|
|
393
382
|
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
|
-
}
|
|
404
383
|
const vendorPaths = {};
|
|
405
384
|
const preparedTargets = new Set(skipVendor ? actionTargets : skipVendorTargets);
|
|
406
385
|
|
|
@@ -522,28 +501,13 @@ export async function reconcileMacTenantApps({
|
|
|
522
501
|
const config = explicitTenantAppConfig(baseConfig, tenant, targets);
|
|
523
502
|
let catalog;
|
|
524
503
|
try {
|
|
525
|
-
catalog = await io.fetchModels(config
|
|
504
|
+
catalog = await io.fetchModels(config);
|
|
526
505
|
} catch (error) {
|
|
527
506
|
throw new Error(`tenant model catalog is unavailable (${redactSecretText(error?.message || error)}); no Impel app files were changed`);
|
|
528
507
|
}
|
|
529
|
-
const actionTargets = selectCatalogAppTargets(targets, catalog.models, config, {
|
|
530
|
-
log: io.log,
|
|
531
|
-
allowNone: true,
|
|
532
|
-
});
|
|
508
|
+
const actionTargets = selectCatalogAppTargets(targets, catalog.models, config, { log: io.log });
|
|
533
509
|
const supported = new Set(actionTargets);
|
|
534
510
|
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
|
-
}
|
|
547
511
|
const vendorPaths = {};
|
|
548
512
|
const statuses = io.status(actionTargets, homeDir, config.tenantId, config.tenantName);
|
|
549
513
|
for (const status of statuses) vendorPaths[status.target] ||= status.vendorPath;
|
|
@@ -585,6 +549,7 @@ export async function reconcileMacTenantApps({
|
|
|
585
549
|
vendorPaths,
|
|
586
550
|
writeBundles: staleTargets,
|
|
587
551
|
}) : [];
|
|
552
|
+
const paths = appPaths(homeDir, config.tenantId, { tenantName: config.tenantName });
|
|
588
553
|
const agentItems = [];
|
|
589
554
|
for (const item of installed) {
|
|
590
555
|
const { client, env, label } = appSkillTarget(item.target, paths);
|
package/src/commands/converge.js
CHANGED
|
@@ -13,8 +13,6 @@ 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
|
-
|
|
18
16
|
function confirmed(answer) {
|
|
19
17
|
return /^(?:y|yes)$/iu.test(String(answer || "").trim());
|
|
20
18
|
}
|
|
@@ -46,7 +44,6 @@ export async function cmdConverge(argv = [], overrides = {}) {
|
|
|
46
44
|
platform: process.platform,
|
|
47
45
|
environment: process.env,
|
|
48
46
|
preparePlatformClis,
|
|
49
|
-
discoveryLogger: console,
|
|
50
47
|
...overrides,
|
|
51
48
|
};
|
|
52
49
|
if (overrides.prepareWindowsClis && !overrides.preparePlatformClis) {
|
|
@@ -61,22 +58,10 @@ export async function cmdConverge(argv = [], overrides = {}) {
|
|
|
61
58
|
let listing;
|
|
62
59
|
try {
|
|
63
60
|
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.
|
|
66
61
|
} catch (error) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
}
|
|
62
|
+
console.error(`impel update: tenant discovery failed (${redactSecretText(error?.message || error)})`);
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return false;
|
|
80
65
|
}
|
|
81
66
|
const selected = selectDefaultTenant(listing, { currentTenantId: config.tenantId });
|
|
82
67
|
config.tenantId = selected.id;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { crossAppModelsEnabled, loadConfig, saveConfig } from "../config.js";
|
|
2
2
|
import { assertProviderScopes, ensureTenantSelection } from "../tenants.js";
|
|
3
|
-
import { cmdCursorExperimental, CURSOR_EXPERIMENT_HELP } from "./cursorExperimental.js";
|
|
4
3
|
|
|
5
4
|
const HELP = `impel experimental cross-app-models enable|disable|status
|
|
6
5
|
|
|
@@ -11,9 +10,7 @@ It does not modify native profiles or support the consumer ChatGPT app
|
|
|
11
10
|
(com.openai.chat).
|
|
12
11
|
|
|
13
12
|
Run \`impel app refresh all\` to refresh desktop configs. Reopen an Impel app
|
|
14
|
-
or isolated CLI to apply the experiment
|
|
15
|
-
|
|
16
|
-
${CURSOR_EXPERIMENT_HELP}`;
|
|
13
|
+
or isolated CLI to apply the experiment.`;
|
|
17
14
|
|
|
18
15
|
export async function cmdExperimental(argv, dependencies = {}) {
|
|
19
16
|
const read = dependencies.loadConfig || loadConfig;
|
|
@@ -26,11 +23,8 @@ export async function cmdExperimental(argv, dependencies = {}) {
|
|
|
26
23
|
log(HELP);
|
|
27
24
|
return;
|
|
28
25
|
}
|
|
29
|
-
if (experiment === "cursor") {
|
|
30
|
-
return cmdCursorExperimental([action, ...extra], dependencies);
|
|
31
|
-
}
|
|
32
26
|
if (experiment !== "cross-app-models" || !["enable", "disable", "status"].includes(action) || extra.length > 0) {
|
|
33
|
-
throw new Error("usage: impel experimental cross-app-models enable|disable|status
|
|
27
|
+
throw new Error("usage: impel experimental cross-app-models enable|disable|status");
|
|
34
28
|
}
|
|
35
29
|
|
|
36
30
|
const config = read();
|