@bitkyc08/opencodex 2.50.0 → 2.51.0

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.
Files changed (95) hide show
  1. package/bin/ocx.mjs +222 -71
  2. package/gui/dist/assets/{index-C39tnjXO.js → index-D7BdZpZm.js} +1 -1
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/qoder/adapter.ts +69 -1
  6. package/src/adapters/qoder/scaffold-guard.ts +233 -0
  7. package/src/claude/agents-inject.ts +29 -5
  8. package/src/claude/desktop-3p.ts +31 -3
  9. package/src/claude/gateway-cache.ts +12 -21
  10. package/src/cli/capabilities.ts +28 -0
  11. package/src/cli/claude-agent-startup-sync.ts +26 -1
  12. package/src/cli/claude.ts +138 -20
  13. package/src/cli/config-command.ts +67 -1
  14. package/src/cli/connect.ts +181 -14
  15. package/src/cli/dispatch.ts +53 -9
  16. package/src/cli/doctor.ts +9 -2
  17. package/src/cli/ensure-desired-integrations.ts +10 -0
  18. package/src/cli/gui-pair-client.ts +1 -12
  19. package/src/cli/help.ts +4 -1
  20. package/src/cli/hub.ts +367 -0
  21. package/src/cli/index.ts +94 -30
  22. package/src/cli/launcher-context.ts +1 -1
  23. package/src/cli/registry.ts +43 -3
  24. package/src/cli/status.ts +325 -5
  25. package/src/cli/version-skew.ts +4 -1
  26. package/src/cli.ts +2 -2
  27. package/src/client/catalog-compatibility.ts +192 -0
  28. package/src/client/connect.ts +31 -0
  29. package/src/client/hub-client.ts +52 -0
  30. package/src/client/hub-state.ts +214 -0
  31. package/src/codex/account-usability.ts +48 -12
  32. package/src/codex/auth-api.ts +49 -5
  33. package/src/codex/catalog/effort.ts +67 -8
  34. package/src/codex/catalog/sync.ts +85 -0
  35. package/src/codex/codex-write-lock.ts +11 -2
  36. package/src/codex/desired-state.ts +47 -1
  37. package/src/codex/inject-coordination.ts +10 -5
  38. package/src/codex/inject.ts +26 -10
  39. package/src/codex/loopback-target.ts +45 -0
  40. package/src/codex/routing.ts +48 -1
  41. package/src/codex/runtime.ts +37 -3
  42. package/src/codex/sync.ts +29 -9
  43. package/src/codex/warmup.ts +21 -4
  44. package/src/config/pending-teardown.ts +1 -1
  45. package/src/config.ts +126 -12
  46. package/src/generated/compatibility-version.json +136 -76
  47. package/src/grok/status.ts +9 -1
  48. package/src/integrations/config-io.ts +54 -1
  49. package/src/lib/bun-runtime.ts +1 -1
  50. package/src/lib/gui-pair-capability.ts +27 -0
  51. package/src/lib/local-destinations.ts +162 -0
  52. package/src/lib/package-tree-integrity.ts +1 -1
  53. package/src/lib/process-control.ts +130 -20
  54. package/src/lib/service-secrets.ts +28 -0
  55. package/src/lib/test-home-guard.ts +49 -0
  56. package/src/providers/opencode-go-transport.ts +9 -1
  57. package/src/providers/quota.ts +5 -1
  58. package/src/providers/registry.ts +34 -5
  59. package/src/remote/hub-state.ts +182 -0
  60. package/src/server/auth-cors.ts +5 -0
  61. package/src/server/chat-completions.ts +6 -3
  62. package/src/server/claude-messages.ts +7 -1
  63. package/src/server/hub-state.ts +98 -0
  64. package/src/server/index.ts +124 -6
  65. package/src/server/management/api-access.ts +14 -3
  66. package/src/server/management/config-routes.ts +2 -2
  67. package/src/server/management/cursor-integration-routes.ts +13 -4
  68. package/src/server/proxy-liveness.ts +7 -1
  69. package/src/server/request-log-conversation.ts +41 -1
  70. package/src/server/responses/codex-auth-error.ts +18 -1
  71. package/src/server/responses/codex-ws-exchange.ts +36 -4
  72. package/src/server/responses/codex-ws-wire.ts +75 -4
  73. package/src/server/responses/compact.ts +20 -9
  74. package/src/server/responses/core.ts +57 -10
  75. package/src/server/responses/policy-fallback.ts +7 -1
  76. package/src/server/system-env-shell.ts +14 -2
  77. package/src/server/system-env.ts +106 -14
  78. package/src/service.ts +906 -94
  79. package/src/types/config.ts +57 -4
  80. package/src/update/badge.ts +3 -2
  81. package/src/update/index.ts +317 -64
  82. package/src/update/install-detection.d.mts +6 -0
  83. package/src/update/install-detection.mjs +73 -0
  84. package/src/update/job.ts +101 -49
  85. package/src/update/pnpm-global-install.d.mts +144 -0
  86. package/src/update/pnpm-global-install.mjs +591 -0
  87. package/src/update/pnpm-invocation.d.mts +43 -0
  88. package/src/update/pnpm-invocation.mjs +141 -0
  89. package/src/update/registry-integrity.d.mts +16 -0
  90. package/src/update/registry-integrity.mjs +37 -0
  91. package/src/update/transactional-install.d.mts +1 -1
  92. package/src/update/transactional-install.mjs +101 -7
  93. package/src/update/tray-update-plan.mjs +1 -1
  94. package/src/vision/plan.ts +13 -3
  95. package/src/vision/routed-describe.ts +51 -20
package/bin/ocx.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * opencodex npm bin launcher.
3
+ * opencodex published-package bin launcher.
4
4
  *
5
5
  * The package source is TypeScript that runs on the Bun runtime. To let
6
- * `npm install -g @bitkyc08/opencodex` work without a separately-installed Bun,
6
+ * global npm and pnpm installs of `@bitkyc08/opencodex` work without a separately-installed Bun,
7
7
  * we bundle the runtime via the `bun` npm dependency and exec it from this
8
8
  * Node shim. (Dev still runs `bun run src/cli/index.ts` directly via the shebang on
9
- * src/cli/index.ts — only the published npm `bin` routes through here.)
9
+ * src/cli/index.ts — only the published npm/pnpm `bin` routes through here.)
10
10
  */
11
11
  import { spawn, spawnSync } from "node:child_process";
12
12
  import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "../src/update/stop-contract.mjs";
@@ -20,6 +20,14 @@ import { dirname, join, resolve } from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
21
  import { isRealBunBinary } from "../src/lib/bun-binary-validator.mjs";
22
22
  import { npmInvocation } from "../src/update/npm-invocation.mjs";
23
+ import { pnpmInvocationForPath, resolvePnpmCommands } from "../src/update/pnpm-invocation.mjs";
24
+ import { detectInstallFromPath } from "../src/update/install-detection.mjs";
25
+ import {
26
+ pnpmOwnerInvocation,
27
+ resolvePnpmGlobalOwner,
28
+ runPnpmGlobalUpdate,
29
+ } from "../src/update/pnpm-global-install.mjs";
30
+ import { checkRegistryPackageIntegrity } from "../src/update/registry-integrity.mjs";
23
31
  import { hasPendingTeardownIn } from "../src/config/pending-teardown-names.mjs";
24
32
  import {
25
33
  npmCachePreflightFailureMessage,
@@ -44,6 +52,7 @@ try {
44
52
  }
45
53
  const require = createRequire(import.meta.url);
46
54
  const here = dirname(fileURLToPath(import.meta.url));
55
+ const installMethod = detectInstallFromPath(here, { exists: existsSync });
47
56
  const cliPath = join(here, "..", "src", "cli", "index.ts");
48
57
  const NODE_LAUNCH_CONTEXT_ENV = "OCX_NODE_LAUNCH_CONTEXT";
49
58
  const NODE_LAUNCH_PROOF_PREFIX = "--ocx-internal-launch-proof=";
@@ -53,7 +62,7 @@ function isNodeModulesInstall() {
53
62
  }
54
63
 
55
64
  function isBunGlobalInstall() {
56
- return /[\\/]\.bun[\\/]/.test(here);
65
+ return installMethod === "bun";
57
66
  }
58
67
 
59
68
  function currentPackageVersion() {
@@ -102,10 +111,9 @@ function historyRestoreIncomplete() {
102
111
  }
103
112
  }
104
113
 
105
- function repairCodexShimIfNeeded() {
114
+ function repairCodexShimIfNeeded(launcherPath = fileURLToPath(import.meta.url)) {
106
115
  if (!shouldRepairCodexShim()) return;
107
- const launcher = fileURLToPath(import.meta.url);
108
- const res = spawnSync(process.execPath, [launcher, "codex-shim", "install"], {
116
+ const res = spawnSync(process.execPath, [launcherPath, "codex-shim", "install"], {
109
117
  stdio: "inherit",
110
118
  windowsHide: true,
111
119
  });
@@ -135,34 +143,116 @@ function runTrayLifecycle(launcher, action) {
135
143
  });
136
144
  }
137
145
 
146
+ function shellQuote(value) {
147
+ if (process.platform === "win32") return `"${value.replaceAll("\"", "\\\"")}"`;
148
+ return `'${value.replaceAll("'", "'\\''")}'`;
149
+ }
150
+
151
+ function launcherStartHint(launcher, port) {
152
+ return `${shellQuote(process.execPath)} ${shellQuote(launcher)} start --port ${Math.trunc(port)}`;
153
+ }
154
+
138
155
  function runNpmSelfUpdate() {
156
+ return runPackageManagerSelfUpdate("npm");
157
+ }
158
+
159
+ function runPnpmSelfUpdate() {
160
+ return runPackageManagerSelfUpdate("pnpm");
161
+ }
162
+
163
+ function runningPnpmShimPath() {
164
+ const invoked = process.argv[1];
165
+ if (!invoked) return undefined;
166
+ const name = invoked.replaceAll("\\", "/").split("/").at(-1)?.toLowerCase();
167
+ if (!new Set(["ocx", "opencodex", "ocx.cmd", "opencodex.cmd", "ocx.ps1", "opencodex.ps1"]).has(name ?? "")) {
168
+ return undefined;
169
+ }
170
+ return resolve(invoked);
171
+ }
172
+
173
+ function runPackageManagerSelfUpdate(manager) {
139
174
  const current = currentPackageVersion();
140
175
  const tag = updateTag(current);
141
- const latestInvocation = npmInvocation(["view", `${PKG}@${tag}`, "version"]);
142
- const installInvocation = npmInvocation(["install", "-g", `${PKG}@${tag}`]);
176
+ let owner;
177
+ if (manager === "pnpm") {
178
+ const ownerResult = resolvePnpmGlobalOwner({
179
+ packageName: PKG,
180
+ packagePath: resolve(here, ".."),
181
+ commandPaths: resolvePnpmCommands(),
182
+ runningShimPath: runningPnpmShimPath(),
183
+ runPnpm: (commandPath, args, capture = false) => {
184
+ const invocation = pnpmInvocationForPath(commandPath, args);
185
+ if (!invocation) return { status: 1 };
186
+ return spawnSync(invocation.file, invocation.args, {
187
+ stdio: capture ? "pipe" : "ignore",
188
+ encoding: "utf8",
189
+ timeout: 20_000,
190
+ windowsHide: true,
191
+ ...invocation.options,
192
+ });
193
+ },
194
+ });
195
+ if (!ownerResult.ok) {
196
+ console.error(`opencodex: ${ownerResult.reason}; aborting before stopping the proxy.`);
197
+ process.exit(1);
198
+ }
199
+ owner = ownerResult.owner;
200
+ }
201
+ const managerInvocation = args => manager === "pnpm"
202
+ ? pnpmOwnerInvocation(owner, args)
203
+ : npmInvocation(args);
204
+ const latestInvocation = managerInvocation(["view", `${PKG}@${tag}`, "version"]);
205
+ const installArgs = manager === "pnpm"
206
+ ? ["add", "-g", "--allow-build=bun", `${PKG}@${tag}`]
207
+ : ["install", "-g", `${PKG}@${tag}`];
208
+ const installInvocation = managerInvocation(installArgs);
143
209
  if (!latestInvocation || !installInvocation) {
144
- console.error("opencodex: could not resolve npm from a trusted absolute PATH entry; aborting before stopping the proxy.");
210
+ console.error(`opencodex: could not resolve ${manager} from a trusted absolute PATH entry; aborting before stopping the proxy.`);
145
211
  process.exit(1);
146
212
  }
147
213
  const latestResult = spawnSync(latestInvocation.file, latestInvocation.args, {
148
214
  encoding: "utf8",
149
215
  timeout: 12000,
150
216
  windowsHide: true,
217
+ ...(latestInvocation.env ? { env: latestInvocation.env } : {}),
151
218
  ...latestInvocation.options,
152
219
  });
153
- const latest = latestResult.status === 0 ? latestResult.stdout.trim() : "";
220
+ const latest = latestResult.status === 0 && typeof latestResult.stdout === "string" ? latestResult.stdout.trim() : "";
154
221
 
155
- console.log(`opencodex v${current} (installed via npm, tag ${tag})`);
222
+ console.log(`opencodex v${current} (installed via ${manager}, tag ${tag})`);
156
223
  if (latest && latest === current) {
157
224
  console.log(`Already on the latest ${tag} version (v${latest}).`);
158
225
  process.exit(0);
159
226
  }
160
227
 
161
- const cachePreflight = runNpmCachePreflight();
162
- if (!cachePreflight.ok) {
163
- console.error(`opencodex: ${npmCachePreflightFailureMessage(cachePreflight.reason)}. Aborting before stopping the proxy.`);
228
+ const integrity = checkRegistryPackageIntegrity(PKG, latest || null, args => {
229
+ const invocation = managerInvocation(args);
230
+ if (!invocation) return { status: 1 };
231
+ return spawnSync(invocation.file, invocation.args, {
232
+ encoding: "utf8",
233
+ timeout: 12000,
234
+ windowsHide: true,
235
+ ...(invocation.env ? { env: invocation.env } : {}),
236
+ ...invocation.options,
237
+ });
238
+ });
239
+ if (integrity.ok === false) {
240
+ console.error(`opencodex: ${integrity.reason}; aborting before stopping the proxy.`);
164
241
  process.exit(1);
165
242
  }
243
+ if (integrity.ok === "skipped") {
244
+ console.warn(`opencodex: integrity pre-flight skipped: ${integrity.reason}. Proceeding best-effort.`);
245
+ } else {
246
+ console.log(`Verified ${PKG}@${latest} integrity metadata ${integrity.integrity.slice(0, 24)}…`);
247
+ }
248
+
249
+ if (manager === "npm") {
250
+ const cachePreflight = runNpmCachePreflight();
251
+ if (!cachePreflight.ok) {
252
+ console.error(`opencodex: ${npmCachePreflightFailureMessage(cachePreflight.reason)}. Aborting before stopping the proxy.`);
253
+ process.exit(1);
254
+ }
255
+ }
166
256
 
167
257
  // Remember whether a background service manages the proxy BEFORE stopping — `ocx stop`
168
258
  // unloads it, so a successful update must refresh and restart it afterwards.
@@ -177,15 +267,15 @@ function runNpmSelfUpdate() {
177
267
  * may be re-registered and require elevation.
178
268
  */
179
269
  function serviceRefreshArgs() {
180
- return [launcher, "service", "repair"];
270
+ return [postUpdateLauncher, "service", "repair"];
181
271
  }
182
272
  /** Register from scratch, preserving the recorded backend. Only for a genuinely absent service. */
183
273
  function serviceInstallArgs() {
184
274
  try {
185
275
  const state = JSON.parse(readFileSync(serviceStatePath, "utf8"));
186
- if (state.backend === "native") return [launcher, "service", "install", "--native"];
276
+ if (state.backend === "native") return [postUpdateLauncher, "service", "install", "--native"];
187
277
  } catch { /* missing or corrupt — fall through to default */ }
188
- return [launcher, "service", "install"];
278
+ return [postUpdateLauncher, "service", "install"];
189
279
  }
190
280
  /**
191
281
  * Structured "is a service actually registered?" answer.
@@ -262,16 +352,23 @@ function runNpmSelfUpdate() {
262
352
  // get it from one place.
263
353
 
264
354
  const launcher = fileURLToPath(import.meta.url);
355
+ // The pnpm owner preflight has verified this package tree and global group. Keep that exact
356
+ // package path as the recovery starting point; a path returned by the pnpm transaction
357
+ // replaces it only after the new tree and shims have been verified.
358
+ let postUpdateLauncher = manager === "pnpm" && owner
359
+ ? join(owner.packagePath, "bin", "ocx.mjs")
360
+ : launcher;
361
+ let postUpdateLauncherUsable = true;
265
362
 
266
363
  function startProxyDirectly() {
267
- if (!existsSync(launcher)) {
364
+ if (!postUpdateLauncherUsable || !existsSync(postUpdateLauncher)) {
268
365
  console.error("opencodex: cannot restart the proxy because the launcher is missing; reinstall opencodex manually.");
269
366
  return;
270
367
  }
271
368
  const env = { ...process.env };
272
369
  delete env.OCX_SERVICE;
273
370
  console.log(`Attempting to restart the proxy on port ${bakePort}.`);
274
- const child = spawn(process.execPath, [launcher, "start", "--port", String(bakePort)], {
371
+ const child = spawn(process.execPath, [postUpdateLauncher, "start", "--port", String(bakePort)], {
275
372
  detached: true,
276
373
  stdio: "ignore",
277
374
  windowsHide: true,
@@ -296,7 +393,7 @@ function runNpmSelfUpdate() {
296
393
  // diagnostic says the service is genuinely absent. Installing after ANY repair
297
394
  // failure would resurrect the elevation prompt this change exists to avoid, and
298
395
  // could re-register a service the user just uninstalled.
299
- if (svc.status !== 0 && readServiceInstalledFromStatus(launcher) === false) {
396
+ if (svc.status !== 0 && readServiceInstalledFromStatus(postUpdateLauncher) === false) {
300
397
  console.log("No registered service found — installing it instead.");
301
398
  svc = spawnSync(process.execPath, serviceInstallArgs(), { stdio: "inherit", windowsHide: true });
302
399
  }
@@ -305,7 +402,7 @@ function runNpmSelfUpdate() {
305
402
  // Exit 0 can still leave stale/missing assets that never bring the proxy
306
403
  // back — match the GUI/CLI fallthrough so /healthz is not left dead.
307
404
  try {
308
- const st = spawnSync(process.execPath, [launcher, "status", "--json"], {
405
+ const st = spawnSync(process.execPath, [postUpdateLauncher, "status", "--json"], {
309
406
  encoding: "utf8",
310
407
  timeout: 20_000,
311
408
  windowsHide: true,
@@ -365,6 +462,10 @@ function runNpmSelfUpdate() {
365
462
  existsSync(join(configDir(), "ocx.pid")) || existsSync(join(configDir(), "runtime-port.json"));
366
463
 
367
464
  function recoverStoppedRuntimeAfterFailure() {
465
+ if (!postUpdateLauncherUsable) {
466
+ console.error("opencodex: no verified active launcher remains for automatic recovery; reinstall opencodex manually.");
467
+ return;
468
+ }
368
469
  if (serviceWasInstalled) {
369
470
  console.warn("opencodex: update failed after stopping the proxy — restoring the previous background service.");
370
471
  refreshBackgroundServiceOrStartDirect();
@@ -423,62 +524,105 @@ function runNpmSelfUpdate() {
423
524
  }
424
525
  }
425
526
 
426
- // #1942/#1849: stage -> verify -> swap -> rollback instead of installing straight
427
- // into the live tree. A failure at any point leaves either the old or the new tree
428
- // complete — never a file-less skeleton. Falls back to the legacy in-place install
429
- // only when the transactional module cannot run at all.
430
- const packageDir = resolve(here, "..");
431
- console.log(`Updating${latest ? ` to v${latest}` : ""} (transactional)...`);
527
+ // npm keeps the existing stage -> verify -> swap -> rollback flow. pnpm owns a
528
+ // content-addressable store and generated global shims, so its path uses pnpm's own
529
+ // global update operation and verifies the active group instead of renaming files.
530
+ console.log(`Updating${latest ? ` to v${latest}` : ""} (${manager === "npm" ? "transactional" : "pnpm-managed"})...`);
432
531
  let res;
433
532
  try {
434
- const tx = transactionalNpmUpdate({
435
- packageDir,
436
- pkgName: PKG,
437
- targetVersion: latest || undefined,
438
- tag,
439
- runNpm: (args) => {
440
- const invocation = npmInvocation(args);
441
- if (!invocation) return { status: 1 };
442
- return spawnSync(invocation.file, invocation.args, {
443
- stdio: "inherit",
444
- timeout: 180000,
445
- windowsHide: true,
446
- ...invocation.options,
447
- });
448
- },
449
- log: (line) => console.log(line),
450
- });
451
- if (tx.ok) {
452
- res = { status: 0 };
453
- } else if (tx.phase === "stage" || tx.phase === "verify") {
454
- // Live tree untouched: report and stop. Nothing to roll back.
455
- console.error(`opencodex: update aborted before touching the live install (${tx.phase}): ${tx.error}`);
456
- res = { status: 1 };
533
+ if (manager === "npm") {
534
+ const packageDir = resolve(here, "..");
535
+ const tx = transactionalNpmUpdate({
536
+ packageDir,
537
+ pkgName: PKG,
538
+ targetVersion: latest || undefined,
539
+ tag,
540
+ runNpm: (args) => {
541
+ const invocation = npmInvocation(args);
542
+ if (!invocation) return { status: 1 };
543
+ return spawnSync(invocation.file, invocation.args, {
544
+ stdio: "inherit",
545
+ timeout: 180000,
546
+ windowsHide: true,
547
+ ...invocation.options,
548
+ });
549
+ },
550
+ log: (line) => console.log(line),
551
+ });
552
+ postUpdateLauncherUsable = tx.ok
553
+ || tx.rolledBack === true
554
+ || ["stage", "verify", "swap-backup"].includes(tx.phase);
555
+ if (tx.ok) {
556
+ res = { status: 0 };
557
+ } else if (tx.phase === "stage" || tx.phase === "verify") {
558
+ // Live tree untouched: report and stop. Nothing to roll back.
559
+ console.error(`opencodex: update aborted before touching the live install (${tx.phase}): ${tx.error}`);
560
+ res = { status: 1 };
561
+ } else {
562
+ console.error(`opencodex: update failed (${tx.phase}): ${tx.error}${tx.rolledBack ? " — previous version restored." : ""}`);
563
+ res = { status: 1 };
564
+ }
457
565
  } else {
458
- console.error(`opencodex: update failed (${tx.phase}): ${tx.error}${tx.rolledBack ? " — previous version restored." : ""}`);
459
- res = { status: 1 };
566
+ const update = runPnpmGlobalUpdate({
567
+ packageName: PKG,
568
+ currentVersion: current,
569
+ targetVersion: latest || undefined,
570
+ tag,
571
+ owner,
572
+ runningPackagePath: resolve(here, ".."),
573
+ runPnpm: (args, capture = false) => {
574
+ const invocation = pnpmOwnerInvocation(owner, args);
575
+ if (!invocation) return { status: 1 };
576
+ return spawnSync(invocation.file, invocation.args, {
577
+ stdio: capture ? "pipe" : "inherit",
578
+ encoding: "utf8",
579
+ timeout: 180000,
580
+ windowsHide: true,
581
+ env: invocation.env,
582
+ ...invocation.options,
583
+ });
584
+ },
585
+ log: line => console.log(line),
586
+ });
587
+ if (update.ok) {
588
+ // pnpm switches the active global group and updates its shim. Continue recovery
589
+ // through that fresh package tree, not the old group whose launcher is still
590
+ // executing this update.
591
+ postUpdateLauncher = join(update.path, "bin", "ocx.mjs");
592
+ res = { status: 0 };
593
+ } else {
594
+ console.error(`opencodex: ${update.error}${update.rolledBack ? "." : " Manual recovery may be required."}`);
595
+ postUpdateLauncherUsable = Boolean(update.activePath);
596
+ if (update.activePath) postUpdateLauncher = join(update.activePath, "bin", "ocx.mjs");
597
+ res = { status: 1 };
598
+ }
460
599
  }
461
600
  } catch (error) {
462
601
  // An unexpected throw means we cannot prove the live tree is untouched, so the
463
602
  // legacy in-place install (which deletes live first) is exactly the wrong rescue —
464
603
  // it recreates the #1849 destruction path. Report and stop; the boot probe and the
465
604
  // recovery marker cover the swap-window states.
466
- console.error(`opencodex: transactional update failed unexpectedly (${error?.message ?? error}). ` +
467
- `The live install was not knowingly modified; run 'ocx update' again or reinstall with ` +
468
- `npm install -g --allow-scripts=bun ${PKG}@${tag}.`);
605
+ const manual = manager === "pnpm"
606
+ ? `pnpm add -g --allow-build=bun ${PKG}@${tag}`
607
+ : `npm install -g --allow-scripts=bun ${PKG}@${tag}`;
608
+ // An unexpected exception leaves the active package path unproven for either manager.
609
+ // Do not run service/tray/proxy recovery through a possibly half-swapped tree.
610
+ postUpdateLauncherUsable = false;
611
+ console.error(`opencodex: ${manager} update failed unexpectedly (${error?.message ?? error}). ` +
612
+ `The live install was not knowingly modified; run 'ocx update' again or reinstall with ${manual}.`);
469
613
  res = { status: 1 };
470
614
  }
471
615
  if (res.status === 0) {
472
616
  console.log(`\nUpdated${latest ? ` to v${latest}` : ""}.`);
473
- repairCodexShimIfNeeded();
617
+ repairCodexShimIfNeeded(postUpdateLauncher);
474
618
  if (trayBeforeUpdate.refreshAfterReplacement) {
475
- const tray = spawnSync(process.execPath, [launcher, ...trayBeforeUpdate.installArgs], {
619
+ const tray = spawnSync(process.execPath, [postUpdateLauncher, ...trayBeforeUpdate.installArgs], {
476
620
  stdio: "inherit",
477
621
  windowsHide: true,
478
622
  });
479
623
  if (tray.status !== 0) {
480
624
  console.warn("opencodex: Windows tray refresh failed. Run: ocx tray install");
481
- if (trayBeforeUpdate.restoreOnFailure) runTrayLifecycle(launcher, "start");
625
+ if (trayBeforeUpdate.restoreOnFailure && postUpdateLauncherUsable) runTrayLifecycle(postUpdateLauncher, "start");
482
626
  }
483
627
  }
484
628
  // The stop above unloaded any managed service; refresh via the freshly-installed
@@ -487,19 +631,22 @@ function runNpmSelfUpdate() {
487
631
  console.log("Refreshing the background service with the updated files...");
488
632
  refreshBackgroundServiceOrStartDirect();
489
633
  } else {
490
- console.log("Restart the proxy: ocx start");
634
+ console.log(`Restart the proxy: ${launcherStartHint(postUpdateLauncher, bakePort)}`);
491
635
  }
492
636
  process.exit(0);
493
637
  }
494
- if (trayBeforeUpdate.restoreOnFailure) runTrayLifecycle(launcher, "start");
638
+ if (trayBeforeUpdate.restoreOnFailure && postUpdateLauncherUsable) runTrayLifecycle(postUpdateLauncher, "start");
495
639
  recoverStoppedRuntimeAfterFailure();
496
- console.error(`\nUpdate failed (npm exit ${res.status ?? "?"}). Try manually: npm install -g --allow-scripts=bun ${PKG}@${tag}`);
640
+ const manual = manager === "pnpm"
641
+ ? `pnpm add -g --allow-build=bun ${PKG}@${tag}`
642
+ : `npm install -g --allow-scripts=bun ${PKG}@${tag}`;
643
+ console.error(`\nUpdate failed (${manager} exit ${res.status ?? "?"}). Try manually: ${manual}`);
497
644
  process.exit(1);
498
645
  }
499
646
 
500
647
  function bunBinDir() {
501
648
  // Resolve the `bun` dependency's directory without hardcoding the platform
502
- // package — npm's os/cpu/libc resolution already picked the right @oven/bun-*.
649
+ // package — the package manager's os/cpu/libc resolution already picked the right @oven/bun-*.
503
650
  return dirname(require.resolve("bun/package.json"));
504
651
  }
505
652
 
@@ -511,7 +658,7 @@ const BUN_RUNTIME_SOURCE_ENV = "OCX_BUN_RUNTIME_SOURCE";
511
658
  const BUN_RUNTIME_PATH_ENV = "OCX_BUN_RUNTIME_PATH";
512
659
 
513
660
  function findBunBinary(bunDir) {
514
- // The npm `bun` package ships the binary as bin/bun.exe on every platform;
661
+ // The bundled `bun` package ships the binary as bin/bun.exe on every platform;
515
662
  // probe bin/bun too for forward compatibility.
516
663
  for (const name of ["bun.exe", "bun"]) {
517
664
  const p = join(bunDir, "bin", name);
@@ -521,12 +668,15 @@ function findBunBinary(bunDir) {
521
668
  }
522
669
 
523
670
  function fail(msg) {
671
+ const reinstall = installMethod === "pnpm"
672
+ ? "pnpm add -g --allow-build=bun @bitkyc08/opencodex"
673
+ : "npm install -g --allow-scripts=bun @bitkyc08/opencodex";
524
674
  console.error(
525
675
  `opencodex: ${msg}\n` +
526
676
  "The bundled Bun runtime could not be prepared. This usually means the\n" +
527
- "install skipped lifecycle scripts (e.g. npm blocked bun's postinstall\n" +
528
- "under allowScripts) or optional dependencies. Reinstall with:\n" +
529
- " npm install -g --allow-scripts=bun @bitkyc08/opencodex\n" +
677
+ "install skipped lifecycle scripts (for example npm blocked bun's postinstall\n" +
678
+ "or pnpm did not approve bun's build) or optional dependencies. Reinstall with:\n" +
679
+ ` ${reinstall}\n` +
530
680
  "(use sudo if the original install used sudo; without --ignore-scripts\n" +
531
681
  "and without --omit=optional / optional=false)"
532
682
  );
@@ -534,7 +684,7 @@ function fail(msg) {
534
684
  }
535
685
 
536
686
  function resolveBun({ allowInstall = true } = {}) {
537
- // Keep direct npm-launcher starts aligned with durable service/shim installs:
687
+ // Keep direct package-launcher starts aligned with durable service/shim installs:
538
688
  // a valid explicit runtime must win even when the bundled dependency exists.
539
689
  const override = process.env[BUN_OVERRIDE_ENV]?.trim();
540
690
  if (override) {
@@ -566,7 +716,7 @@ function resolveBun({ allowInstall = true } = {}) {
566
716
  return { path: bin, source: "bundled" };
567
717
  }
568
718
 
569
- // `ocx update --help` prints usage and exits WITHOUT side effects. The npm launcher
719
+ // `ocx update --help` prints usage and exits WITHOUT side effects. The Node launcher
570
720
  // intercepts `update` before the Bun CLI starts, so the help short-circuit must live
571
721
  // here too — otherwise --help runs the real self-update, stops the proxy, and drops
572
722
  // in-flight routed streams (issue #168).
@@ -584,13 +734,14 @@ if (codexCliUpdateInspection && typeof process.versions.bun === "string") {
584
734
  }
585
735
 
586
736
  if (process.argv[2] === "update" && isNodeModulesInstall() && !isBunGlobalInstall()) {
587
- runNpmSelfUpdate();
737
+ if (installMethod === "npm") runNpmSelfUpdate();
738
+ if (installMethod === "pnpm") runPnpmSelfUpdate();
588
739
  }
589
740
 
590
741
  // #1849 boot probe: a prior update that lost power (or double-faulted) mid-swap leaves a
591
742
  // backup sibling and a broken live tree. Restore before anything tries to run from the
592
743
  // broken tree; reap stale backups once the live tree verifies healthy.
593
- if (!codexCliUpdateInspection && isNodeModulesInstall() && !isBunGlobalInstall()) {
744
+ if (!codexCliUpdateInspection && installMethod === "npm" && isNodeModulesInstall() && !isBunGlobalInstall()) {
594
745
  try {
595
746
  const probe = bootRestoreProbe(resolve(here, ".."));
596
747
  if (probe.action === "restored") {