impel-cli 0.18.15-beta.3 → 0.18.15-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.18.15-beta.3",
3
+ "version": "0.18.15-beta.5",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
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,27 @@ export const PINNED_VENDOR_APPS = Object.freeze({
81
92
  }),
82
93
  }),
83
94
  chatgpt: Object.freeze({
84
- version: "26.721.41059",
85
- codexVersion: "0.146.0-alpha.3.1",
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
+ 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
+ }),
87
108
  downloads: Object.freeze({
88
109
  arm64: Object.freeze({
89
- url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.721.41059.zip",
90
- sha256: "e2b45056f3d1f8ab90f7f16249bfb5a40d09d0f8099f12ca0d8d7a8fdf9108ce",
110
+ url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.727.40816.zip",
111
+ sha256: "fdbede9b8a28b5bf3bbf1213fa04291724faa6ed2d1d61bb04853bbdfebce219",
91
112
  }),
92
113
  x64: Object.freeze({
93
- url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.721.41059.zip",
94
- sha256: "f00b09d9e1f5138f055fa972e5243991b493423c41f5d78951dc2e21a84089f4",
114
+ url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.727.40816.zip",
115
+ sha256: "12622d916a3993d61fb52a318dcff7af7449eea5070cdd7e094da29f380e3374",
95
116
  }),
96
117
  }),
97
118
  }),
@@ -245,7 +266,7 @@ export const CURRENT_CONFIG_VERSION = 25;
245
266
  // — which is what made every `impel update` re-trigger macOS permission
246
267
  // prompts. Bump this ONLY when a code change alters the bytes of a built
247
268
  // bundle; leave it alone for changes that don't touch bundle contents.
248
- export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-25.1";
269
+ export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-31.1";
249
270
 
250
271
  /** Parse the tenant's install manifest, or null when absent/corrupt. */
251
272
  export function readTenantManifest(homeDir = os.homedir(), tenantId = null) {
@@ -962,7 +983,6 @@ function installPinnedVendorApp(target, homeDir) {
962
983
  homeDir, ".config", RUNTIME_BRAND.cli.configNamespace, "vendor", target, pin.version, pin.bundleName,
963
984
  );
964
985
  fs.mkdirSync(path.dirname(destination), { recursive: true });
965
- sweepStaleBundleArtifacts(path.dirname(destination), path.basename(destination));
966
986
  replaceDirectory(destination, extractedApp);
967
987
  return destination;
968
988
  } finally {
@@ -1519,6 +1539,12 @@ exec "$NODE" "$CLI" token${tenantArgs}
1519
1539
  }
1520
1540
 
1521
1541
  function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageName) {
1542
+ return withBundleMutationLock(paths.claude.launcher, () => (
1543
+ writeVendoredClaudeBundleLocked(paths, vendorPath, gatewayUrl, safeStorageName)
1544
+ ));
1545
+ }
1546
+
1547
+ function writeVendoredClaudeBundleLocked(paths, vendorPath, gatewayUrl, safeStorageName) {
1522
1548
  if (!vendorPath) throw new Error("Claude vendor app is unavailable");
1523
1549
  if (!isVendableVendorApp("claude", vendorPath)) {
1524
1550
  const bundleId = readBundleIdentifier(vendorPath);
@@ -1532,9 +1558,8 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
1532
1558
  if (!executableName) throw new Error(`Claude executable is missing from ${vendorPath}`);
1533
1559
 
1534
1560
  const bundle = paths.claude.launcher;
1535
- const staging = `${bundle}.tmp-${process.pid}`;
1536
1561
  sweepStaleBundleArtifacts(path.dirname(bundle), path.basename(bundle));
1537
- fs.rmSync(staging, { recursive: true, force: true });
1562
+ const staging = uniqueBundleArtifactPath(bundle, "tmp");
1538
1563
  cloneAppBundle(vendorPath, staging);
1539
1564
  try {
1540
1565
  const plistPath = path.join(staging, "Contents", "Info.plist");
@@ -1616,9 +1641,9 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
1616
1641
  }, null, 2) + "\n", 0o644);
1617
1642
 
1618
1643
  signVendoredApp(staging, vendorExecutable, "Claude", signingIdentity);
1619
- replaceDirectory(bundle, staging);
1644
+ replaceDirectoryUnlocked(bundle, staging);
1620
1645
  } catch (error) {
1621
- fs.rmSync(staging, { recursive: true, force: true });
1646
+ removeBundleArtifact(staging);
1622
1647
  throw error;
1623
1648
  }
1624
1649
  }
@@ -1632,6 +1657,12 @@ function claudeUsageCompatibilityEnabled(gatewayUrl) {
1632
1657
  }
1633
1658
 
1634
1659
  function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
1660
+ return withBundleMutationLock(paths.chatgpt.launcher, () => (
1661
+ writeVendoredChatGPTBundleLocked(paths, vendorPath, gatewayUrl, homeDir)
1662
+ ));
1663
+ }
1664
+
1665
+ function writeVendoredChatGPTBundleLocked(paths, vendorPath, gatewayUrl, homeDir) {
1635
1666
  if (!vendorPath) throw new Error("ChatGPT/Codex vendor app is unavailable");
1636
1667
  const definition = APP_DEFINITIONS.chatgpt;
1637
1668
  const executableName = definition.executableNames.find((name) => (
@@ -1640,11 +1671,10 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
1640
1671
  if (!executableName) throw new Error(`ChatGPT/Codex executable is missing from ${vendorPath}`);
1641
1672
 
1642
1673
  const bundle = paths.chatgpt.launcher;
1643
- const staging = `${bundle}.tmp-${process.pid}`;
1674
+ sweepStaleBundleArtifacts(path.dirname(bundle), path.basename(bundle));
1675
+ const staging = uniqueBundleArtifactPath(bundle, "tmp");
1644
1676
  const vendorBundleName = path.basename(vendorPath);
1645
1677
  const vendorBundle = path.join(staging, "Contents", "Resources", vendorBundleName);
1646
- sweepStaleBundleArtifacts(path.dirname(bundle), path.basename(bundle));
1647
- fs.rmSync(staging, { recursive: true, force: true });
1648
1678
  cloneAppBundle(vendorPath, vendorBundle);
1649
1679
  try {
1650
1680
  const asarPath = path.join(vendorBundle, "Contents", "Resources", "app.asar");
@@ -1720,9 +1750,9 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
1720
1750
  }, null, 2) + "\n", 0o644);
1721
1751
 
1722
1752
  signVendoredApp(staging, vendorExecutable, "ChatGPT", signingIdentity);
1723
- replaceDirectory(bundle, staging);
1753
+ replaceDirectoryUnlocked(bundle, staging);
1724
1754
  } catch (error) {
1725
- fs.rmSync(staging, { recursive: true, force: true });
1755
+ removeBundleArtifact(staging);
1726
1756
  throw error;
1727
1757
  }
1728
1758
  }
@@ -2215,6 +2245,7 @@ HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
2215
2245
  VENDOR_APP="$HERE/../Resources/"${shellQuote(vendorBundleName)}
2216
2246
  export CODEX_HOME=${shellQuote(paths.chatgpt.codexHome)}
2217
2247
  export CODEX_AUTHAPI_BASE_URL=${shellQuote(codexAccountBaseUrl)}
2248
+ export CODEX_SPARKLE_ENABLED=false
2218
2249
  unset CODEX_ACCESS_TOKEN CODEX_API_KEY OPENAI_API_KEY OPENAI_BASE_URL
2219
2250
  secure_codex_home() {
2220
2251
  [ ! -L "$CODEX_HOME" ] || exit 1
@@ -2525,24 +2556,105 @@ export function sweepStaleBundleArtifacts(dir, baseName) {
2525
2556
  throw error;
2526
2557
  }
2527
2558
  const prefixes = [`${baseName}.tmp-`, `${baseName}.previous-`];
2528
- const removed = entries.filter((entry) => prefixes.some((prefix) => entry.startsWith(prefix)));
2529
- for (const entry of removed) {
2530
- fs.rmSync(path.join(dir, entry), { recursive: true, force: true });
2559
+ const removed = [];
2560
+ for (const entry of entries.filter((candidate) => prefixes.some((prefix) => candidate.startsWith(prefix)))) {
2561
+ if (removeBundleArtifact(path.join(dir, entry))) removed.push(entry);
2531
2562
  }
2532
2563
  return removed;
2533
2564
  }
2534
2565
 
2535
- function replaceDirectory(destination, staging) {
2536
- const backup = `${destination}.previous-${process.pid}`;
2537
- fs.rmSync(backup, { recursive: true, force: true });
2538
- if (fs.existsSync(destination)) fs.renameSync(destination, backup);
2566
+ function uniqueBundleArtifactPath(destination, kind) {
2567
+ return `${destination}.${kind}-${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
2568
+ }
2569
+
2570
+ function removeBundleArtifact(candidate) {
2571
+ try {
2572
+ fs.rmSync(candidate, BUNDLE_ARTIFACT_REMOVE_OPTIONS);
2573
+ return !fs.existsSync(candidate);
2574
+ } catch {
2575
+ return false;
2576
+ }
2577
+ }
2578
+
2579
+ export function withBundleMutationLock(destination, run) {
2580
+ const lockPath = path.join(
2581
+ path.dirname(destination),
2582
+ `.${path.basename(destination)}.impel-install.lock`,
2583
+ );
2584
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
2585
+
2586
+ if (process.platform !== "darwin") {
2587
+ if (portableBundleMutationLocks.has(lockPath)) {
2588
+ throw new Error(`another Impel app update is already replacing ${path.basename(destination)}`);
2589
+ }
2590
+ const descriptor = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_RDWR, 0o600);
2591
+ portableBundleMutationLocks.add(lockPath);
2592
+ try {
2593
+ return run();
2594
+ } finally {
2595
+ portableBundleMutationLocks.delete(lockPath);
2596
+ fs.closeSync(descriptor);
2597
+ }
2598
+ }
2599
+
2600
+ const deadline = Date.now() + BUNDLE_LOCK_WAIT_MS;
2601
+ let descriptor;
2602
+ while (descriptor === undefined) {
2603
+ try {
2604
+ descriptor = fs.openSync(
2605
+ lockPath,
2606
+ fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_NONBLOCK | MACOS_O_EXLOCK,
2607
+ 0o600,
2608
+ );
2609
+ } catch (error) {
2610
+ if (!["EAGAIN", "EWOULDBLOCK"].includes(error?.code)) throw error;
2611
+ if (Date.now() >= deadline) {
2612
+ throw new Error(`timed out waiting for another Impel app update to replace ${path.basename(destination)}`);
2613
+ }
2614
+ Atomics.wait(BUNDLE_LOCK_SLEEP, 0, 0, BUNDLE_LOCK_POLL_MS);
2615
+ }
2616
+ }
2617
+ try {
2618
+ return run();
2619
+ } finally {
2620
+ fs.closeSync(descriptor);
2621
+ }
2622
+ }
2623
+
2624
+ function replaceDirectoryUnlocked(destination, staging, {
2625
+ removeArtifact = removeBundleArtifact,
2626
+ } = {}) {
2627
+ const backup = fs.existsSync(destination)
2628
+ ? uniqueBundleArtifactPath(destination, "previous")
2629
+ : null;
2630
+ if (backup) fs.renameSync(destination, backup);
2539
2631
  try {
2540
2632
  fs.renameSync(staging, destination);
2541
- fs.rmSync(backup, { recursive: true, force: true });
2542
2633
  } catch (error) {
2543
- if (!fs.existsSync(destination) && fs.existsSync(backup)) fs.renameSync(backup, destination);
2634
+ if (backup && !fs.existsSync(destination) && fs.existsSync(backup)) {
2635
+ fs.renameSync(backup, destination);
2636
+ }
2544
2637
  throw error;
2545
2638
  }
2639
+
2640
+ // The new bundle is committed once staging has been renamed into place.
2641
+ // Finder, LaunchServices, or a just-closed helper can briefly keep changing
2642
+ // the old tree and make recursive removal end with ENOTEMPTY. Do not turn
2643
+ // that post-commit housekeeping race into a failed tenant update: the next
2644
+ // sweep will retry any backup that remains.
2645
+ if (backup) {
2646
+ try {
2647
+ removeArtifact(backup);
2648
+ } catch {
2649
+ // Cleanup is best-effort after the committed rename.
2650
+ }
2651
+ }
2652
+ }
2653
+
2654
+ export function replaceDirectory(destination, staging, options = {}) {
2655
+ return withBundleMutationLock(destination, () => (
2656
+ replaceDirectoryUnlocked(destination, staging, options)
2657
+ ));
2546
2658
  }
2547
2659
 
2548
2660
  function isGatewayModel(model) {
@@ -11,9 +11,10 @@ import { environmentValue, nativeCommandInvocation } from "./nativeProcess.js";
11
11
  import { normalizeTenantId } from "./tenants.js";
12
12
  import { brandedEnvironmentName, RUNTIME_BRAND } from "./runtimeBrand.js";
13
13
 
14
- export const WINDOWS_CHATGPT_PACKAGE = "9PLM9XGG6VKS";
15
- export const WINDOWS_CHATGPT_PACKAGE_NAME = "OpenAI.Codex";
16
- export const WINDOWS_CHATGPT_PUBLISHER_ID = "2p2nqsd0c76g0";
14
+ const WINDOWS_CHATGPT_PIN = PINNED_VENDOR_APPS.chatgpt.windows;
15
+ export const WINDOWS_CHATGPT_PACKAGE = WINDOWS_CHATGPT_PIN.storeProductId;
16
+ export const WINDOWS_CHATGPT_PACKAGE_NAME = WINDOWS_CHATGPT_PIN.packageName;
17
+ export const WINDOWS_CHATGPT_PUBLISHER_ID = WINDOWS_CHATGPT_PIN.publisherId;
17
18
  const WINDOWS_CLAUDE_DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1000;
18
19
  const WINDOWS_WINGET_UPDATE_NOT_APPLICABLE = 0x8A15002B;
19
20
 
@@ -60,20 +61,26 @@ function versionedSquirrelCandidates(root, readDirectory = fs.readdirSync) {
60
61
  }
61
62
  }
62
63
 
63
- function installedMsixChatGPT(environment, run = spawnSync) {
64
+ function installedMsixChatGPT(environment, run = spawnSync, pin = null) {
64
65
  const script = [
65
66
  `$package = Get-AppxPackage -Name '${WINDOWS_CHATGPT_PACKAGE_NAME}' -ErrorAction SilentlyContinue`,
66
- `$package = $package | Where-Object PublisherId -eq '${WINDOWS_CHATGPT_PUBLISHER_ID}' | Sort-Object Version -Descending | Select-Object -First 1`,
67
+ `$package = $package | Where-Object { $_.PublisherId -ceq '${WINDOWS_CHATGPT_PUBLISHER_ID}' -and (-not $env:IMPEL_CHATGPT_PACKAGE_VERSION -or $_.Version.ToString() -ceq $env:IMPEL_CHATGPT_PACKAGE_VERSION) } | Sort-Object Version -Descending | Select-Object -First 1`,
67
68
  "if ($package) {",
68
69
  " $manifest = Get-AppxPackageManifest -Package $package",
69
70
  " $relative = $manifest.Package.Applications.Application | Where-Object Id -eq 'App' | Select-Object -First 1 -ExpandProperty Executable",
70
- " if ($relative) { Join-Path $package.InstallLocation $relative }",
71
+ " $normalized = ([string]$relative).Replace('/', '\\')",
72
+ " $expected = ([string]$env:IMPEL_CHATGPT_PACKAGE_EXECUTABLE).Replace('/', '\\')",
73
+ " if ($relative -and (-not $expected -or $normalized -ieq $expected)) { Join-Path $package.InstallLocation $relative }",
71
74
  "}",
72
75
  ].join("; ");
73
76
  try {
74
77
  const result = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
75
78
  encoding: "utf8",
76
- env: windowsPowerShellEnvironment(environment),
79
+ env: {
80
+ ...windowsPowerShellEnvironment(environment),
81
+ IMPEL_CHATGPT_PACKAGE_VERSION: pin?.packageVersion || "",
82
+ IMPEL_CHATGPT_PACKAGE_EXECUTABLE: pin?.executable || "",
83
+ },
77
84
  stdio: ["ignore", "pipe", "ignore"],
78
85
  windowsHide: true,
79
86
  });
@@ -141,9 +148,23 @@ export function findWindowsChatGPTApp(environment = process.env, dependencies =
141
148
  return msix && exists(msix) ? path.win32.normalize(msix) : null;
142
149
  }
143
150
 
144
- function wingetInvocation(action, environment, { packageId, source = "winget", scope = "user" }) {
151
+ function pinnedWindowsChatGPTApp(environment = process.env, dependencies = {}) {
152
+ const exists = dependencies.isFile || isFile;
153
+ const pin = dependencies.pin || WINDOWS_CHATGPT_PIN;
154
+ const msix = (dependencies.queryMsix || installedMsixChatGPT)(environment, dependencies.run, pin);
155
+ return msix && exists(msix) ? path.win32.normalize(msix) : null;
156
+ }
157
+
158
+ function wingetInvocation(action, environment, {
159
+ packageId,
160
+ source = "winget",
161
+ scope = "user",
162
+ }) {
145
163
  const verb = action === "update" ? "upgrade" : "install";
146
164
  const scopeArgs = scope ? ["--scope", scope] : [];
165
+ // Microsoft Store packages commonly report Version "Unknown" to winget.
166
+ // Without this switch winget refuses to consider their available update.
167
+ const updateArgs = action === "update" ? ["--include-unknown"] : [];
147
168
  return nativeCommandInvocation(
148
169
  "winget",
149
170
  [
@@ -154,6 +175,7 @@ function wingetInvocation(action, environment, { packageId, source = "winget", s
154
175
  "--source",
155
176
  source,
156
177
  ...scopeArgs,
178
+ ...updateArgs,
157
179
  "--silent",
158
180
  "--disable-interactivity",
159
181
  "--accept-package-agreements",
@@ -372,21 +394,31 @@ export async function ensureWindowsClaudeApp(_options = {}, dependencies = {}) {
372
394
 
373
395
  /** Install/update OpenAI's official ChatGPT/Codex Store package. */
374
396
  export function ensureWindowsChatGPTApp({ update = false } = {}, dependencies = {}) {
397
+ const find = dependencies.find || findWindowsChatGPTApp;
398
+ const findPinned = dependencies.findPinned || dependencies.find || pinnedWindowsChatGPTApp;
375
399
  const io = {
376
400
  environment: process.env,
377
- find: findWindowsChatGPTApp,
401
+ find,
402
+ findPinned,
378
403
  logger: console,
404
+ pin: WINDOWS_CHATGPT_PIN,
379
405
  run: spawnSync,
380
406
  ...dependencies,
381
407
  };
382
408
  const before = io.find(io.environment);
383
- if (before && !update) return { binary: before, action: "existing", result: null };
409
+ const pinnedBefore = io.findPinned(io.environment, { pin: io.pin, run: io.run });
410
+ if (pinnedBefore && !update) {
411
+ return { binary: pinnedBefore, action: "existing", result: null };
412
+ }
384
413
 
385
414
  // `impel update` also repairs missing surfaces. A missing Store package must
386
415
  // be installed, not sent to `winget upgrade` (which returns 0x8A150014).
387
416
  const shouldUpdate = Boolean(update && before);
417
+ // The msstore source rejects its AppX package version as a winget --version
418
+ // selector. Install the current Store package, then fail closed below unless
419
+ // Windows registered the exact reviewed package version and executable.
388
420
  const invocation = wingetInvocation(shouldUpdate ? "update" : "install", io.environment, {
389
- packageId: WINDOWS_CHATGPT_PACKAGE,
421
+ packageId: io.pin.storeProductId,
390
422
  source: "msstore",
391
423
  scope: null,
392
424
  });
@@ -400,26 +432,36 @@ export function ensureWindowsChatGPTApp({ update = false } = {}, dependencies =
400
432
  } catch (error) {
401
433
  result = { status: null, error };
402
434
  }
403
- const binary = io.find(io.environment) || before;
435
+ let resultBinary = io.findPinned(io.environment, { pin: io.pin, run: io.run });
404
436
  // winget uses an error HRESULT when `upgrade` finds an installed package
405
437
  // with no newer Store release. That is a healthy idempotent update result,
406
438
  // not an installation failure (APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE).
407
439
  const alreadyUpToDate = Boolean(
408
440
  shouldUpdate
409
- && binary
441
+ && resultBinary
410
442
  && !result?.error
411
443
  && Number.isInteger(result?.status)
412
444
  && (result.status >>> 0) === WINDOWS_WINGET_UPDATE_NOT_APPLICABLE,
413
445
  );
414
446
  if (alreadyUpToDate) {
415
- io.logger.log("ChatGPT/Codex: Microsoft Store package already up to date.");
447
+ io.logger.log(`ChatGPT/Codex: Microsoft Store package ${io.pin.packageVersion} already up to date.`);
448
+ }
449
+ const commandSucceeded = (result?.status === 0 && !result?.error) || alreadyUpToDate;
450
+ if (commandSucceeded && !resultBinary) {
451
+ result = {
452
+ ...result,
453
+ error: new Error(
454
+ `ChatGPT/Codex Store package ${io.pin.packageVersion} did not appear after winget completed`,
455
+ ),
456
+ };
416
457
  }
417
- const succeeded = (result?.status === 0 && !result?.error) || alreadyUpToDate;
458
+ const succeeded = commandSucceeded && Boolean(resultBinary);
459
+ if (!succeeded) resultBinary = null;
418
460
  return {
419
- binary,
461
+ binary: resultBinary,
420
462
  action: alreadyUpToDate
421
463
  ? "existing"
422
- : succeeded ? (shouldUpdate ? "updated" : "installed") : (before ? "existing" : "install-failed"),
464
+ : succeeded ? (shouldUpdate ? "updated" : "installed") : "install-failed",
423
465
  result,
424
466
  };
425
467
  }
@@ -617,6 +659,7 @@ export function launchWindowsChatGPTApp(binary, {
617
659
  "CODEX_AUTHAPI_BASE_URL",
618
660
  "CODEX_ELECTRON_USER_DATA_PATH",
619
661
  "CODEX_HOME",
662
+ "CODEX_SPARKLE_ENABLED",
620
663
  "OPENAI_API_KEY",
621
664
  "OPENAI_BASE_URL",
622
665
  ]);
@@ -625,6 +668,7 @@ export function launchWindowsChatGPTApp(binary, {
625
668
  }
626
669
  environment.CODEX_HOME = codexHome;
627
670
  environment.CODEX_AUTHAPI_BASE_URL = `${gatewayUrl}/chatgpt_passthrough/backend-api`;
671
+ environment.CODEX_SPARKLE_ENABLED = "false";
628
672
  // Codex Desktop resolves this before acquiring Electron's single-instance
629
673
  // lock. Keep --user-data-dir below as a compatibility fallback for builds
630
674
  // that predate the dedicated desktop override.