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/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.727.40816",
96
- codexVersion: "0.146.0-alpha.9.2",
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.727.40816.zip",
111
- sha256: "fdbede9b8a28b5bf3bbf1213fa04291724faa6ed2d1d61bb04853bbdfebce219",
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.727.40816.zip",
115
- sha256: "12622d916a3993d61fb52a318dcff7af7449eea5070cdd7e094da29f380e3374",
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
- // 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;
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-31.2";
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 === 2
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, { allowEmpty = false } = {}) {
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 && !(allowEmpty && isExplicitNoSeatCatalog(payload))) {
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: 7,
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 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
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
- const staging = uniqueBundleArtifactPath(bundle, "tmp");
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
- replaceDirectoryUnlocked(bundle, staging);
1617
+ replaceDirectory(bundle, staging);
1675
1618
  } catch (error) {
1676
- removeBundleArtifact(staging);
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
- sweepStaleBundleArtifacts(path.dirname(bundle), path.basename(bundle));
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: 2,
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
- replaceDirectoryUnlocked(bundle, staging);
1721
+ replaceDirectory(bundle, staging);
1784
1722
  } catch (error) {
1785
- removeBundleArtifact(staging);
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
- const bundleIdentifier = process.env.IMPEL_APP_BUNDLE_ID;
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 launchServicesIdentified = false;
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 setLaunchServicesApplicationIdentity = () => {
2337
- if (launchServicesIdentified) return;
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
- launchServicesIdentified = status === 0;
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
- setLaunchServicesApplicationIdentity();
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
- launchServicesIdentified = false;
2431
- setLaunchServicesApplicationIdentity();
2354
+ launchServicesNamed = false;
2355
+ setLaunchServicesApplicationName();
2432
2356
  setApplicationMenu(menu);
2433
2357
  setNativeApplicationMenuTitle();
2434
- }, 100);
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 entries.filter((candidate) => prefixes.some((prefix) => candidate.startsWith(prefix)))) {
2604
- if (removeBundleArtifact(path.join(dir, entry))) removed.push(entry);
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 uniqueBundleArtifactPath(destination, kind) {
2610
- return `${destination}.${kind}-${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
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 (backup && !fs.existsSync(destination) && fs.existsSync(backup)) {
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) {
@@ -76,7 +76,7 @@ export function selectCatalogAppTargets(
76
76
  requestedTargets,
77
77
  models,
78
78
  config,
79
- { log = console.log, allowNone = false } = {},
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, { allowEmpty = false } = {}) {
300
+ async function fetchWindowsCatalog(config, io) {
309
301
  try {
310
- return await io.fetchModels(config, undefined, { allowEmpty });
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, { allowEmpty: true });
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, undefined, { allowEmpty: true });
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);
@@ -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
- 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
- }
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 | impel experimental cursor prepare|open|status");
27
+ throw new Error("usage: impel experimental cross-app-models enable|disable|status");
34
28
  }
35
29
 
36
30
  const config = read();