@@ -23,6 +23,10 @@ import {
2323} from "../src/update/npm-cache-preflight.mjs" ;
2424import { handoffWindowsTrayForUpdate , planWindowsTrayUpdate } from "../src/update/tray-update-plan.mjs" ;
2525import { bootRestoreProbe , transactionalNpmUpdate } from "../src/update/transactional-install.mjs" ;
26+ import {
27+ CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS ,
28+ isCodexCliUpdateInspectionArgv ,
29+ } from "../src/update/codex-cli-update-launch-policy.mjs" ;
2630
2731const PKG = "@bitkyc08/opencodex" ;
2832const require = createRequire ( import . meta. url ) ;
@@ -155,10 +159,9 @@ function runNpmSelfUpdate() {
155159 process . platform === "win32" ? trayInstallState ( ) : { installed : false , running : false } ,
156160 ) ;
157161 /**
158- * Refresh the existing service without re-registering it. `service repair` discovers
159- * the installed backend itself and, on Windows scheduler installs, rewrites the wrapper
160- * assets and restarts the existing task without `schtasks /create` — the elevation a
161- * non-admin `ocx update` does not have.
162+ * Refresh the existing service in place. `service repair` discovers the installed backend;
163+ * healthy Windows scheduler registrations avoid `schtasks /create`, while stale definitions
164+ * may be re-registered and require elevation.
162165 */
163166 function serviceRefreshArgs ( ) {
164167 return [ launcher , "service" , "repair" ] ;
@@ -290,7 +293,8 @@ function runNpmSelfUpdate() {
290293 }
291294 }
292295 if ( needDirectStart ) {
293- // A repair needs no elevation, but it can still fail — or exit 0 while leaving
296+ // Repair normally avoids elevation for a healthy registration, but a stale Windows
297+ // scheduler definition can require it. It can also fail — or exit 0 while leaving
294298 // a non-viable manager. Fall back to a direct detached proxy start so the
295299 // update never leaves the user without a running proxy.
296300 console . warn (
@@ -468,7 +472,7 @@ function fail(msg) {
468472 process . exit ( 1 ) ;
469473}
470474
471- function resolveBun ( ) {
475+ function resolveBun ( { allowInstall = true } = { } ) {
472476 // Keep direct npm-launcher starts aligned with durable service/shim installs:
473477 // a valid explicit runtime must win even when the bundled dependency exists.
474478 const override = process . env [ BUN_OVERRIDE_ENV ] ?. trim ( ) ;
@@ -493,7 +497,7 @@ function resolveBun() {
493497 // Lazy fallback: --ignore-scripts (or a failed postinstall) leaves the
494498 // ~450-byte placeholder stub. Run the bun package's own installer once.
495499 const installJs = join ( bunDir , "install.js" ) ;
496- if ( existsSync ( installJs ) ) {
500+ if ( allowInstall && existsSync ( installJs ) ) {
497501 const r = spawnSync ( process . execPath , [ installJs ] , { stdio : "inherit" } ) ;
498502 if ( r . status === 0 ) bin = findBunBinary ( bunDir ) ;
499503 }
@@ -512,14 +516,20 @@ if (updateHelpRequested) {
512516 process . exit ( 0 ) ;
513517}
514518
519+ const codexCliUpdateInspection = isCodexCliUpdateInspectionArgv ( process . argv ) ;
520+ if ( codexCliUpdateInspection && typeof process . versions . bun === "string" ) {
521+ console . error ( "opencodex: codex-cli-update inspection must use the published Node launcher." ) ;
522+ process . exit ( 1 ) ;
523+ }
524+
515525if ( process . argv [ 2 ] === "update" && isNodeModulesInstall ( ) && ! isBunGlobalInstall ( ) ) {
516526 runNpmSelfUpdate ( ) ;
517527}
518528
519529// #1849 boot probe: a prior update that lost power (or double-faulted) mid-swap leaves a
520530// backup sibling and a broken live tree. Restore before anything tries to run from the
521531// broken tree; reap stale backups once the live tree verifies healthy.
522- if ( isNodeModulesInstall ( ) && ! isBunGlobalInstall ( ) ) {
532+ if ( ! codexCliUpdateInspection && isNodeModulesInstall ( ) && ! isBunGlobalInstall ( ) ) {
523533 try {
524534 const probe = bootRestoreProbe ( resolve ( here , ".." ) ) ;
525535 if ( probe . action === "restored" ) {
@@ -530,7 +540,7 @@ if (isNodeModulesInstall() && !isBunGlobalInstall()) {
530540 } catch { /* the probe must never block launch */ }
531541}
532542
533- const bunRuntime = resolveBun ( ) ;
543+ const bunRuntime = resolveBun ( { allowInstall : ! codexCliUpdateInspection } ) ;
534544const bun = bunRuntime . path ;
535545
536546// Run the Bun child asynchronously and FORWARD termination signals to it, then wait
@@ -554,20 +564,69 @@ const bun = bunRuntime.path;
554564// interpolation and provider settings legitimately read the project environment.
555565const preBunAnthropicSlots = [ "ANTHROPIC_API_KEY" , "ANTHROPIC_AUTH_TOKEN" , "ANTHROPIC_BASE_URL" ]
556566 . filter ( name => typeof process . env [ name ] === "string" && process . env [ name ] !== "" ) ;
567+ // A configured CODEX_CLI_PATH may legitimately be cwd-relative (`./tools/codex`), which the
568+ // ordinary runtime resolver accepts. Inspection only trusts absolute local paths, so capture
569+ // the absolute form here, in the launcher, while the original cwd is still authoritative;
570+ // resolving it later would silently reinterpret it against a different working directory.
571+ //
572+ // A bare command with no separator (`codex`) is NOT a relative path: the runtime resolver
573+ // deliberately hands those to executable lookup along PATH. Rewriting it to `<cwd>/codex`
574+ // would make the inspector treat it as an explicit path and stop searching PATH entirely.
575+ const configuredCodexCliPath = typeof process . env . CODEX_CLI_PATH === "string" && process . env . CODEX_CLI_PATH !== ""
576+ ? process . env . CODEX_CLI_PATH
577+ : null ;
578+ const preBunCodexCliPath = configuredCodexCliPath !== null
579+ && ( configuredCodexCliPath . includes ( "/" ) || configuredCodexCliPath . includes ( "\\" ) || / ^ [ A - Z a - z ] : / . test ( configuredCodexCliPath ) )
580+ ? resolve ( configuredCodexCliPath )
581+ : configuredCodexCliPath ;
582+ const preBunPath = typeof process . env . PATH === "string" ? process . env . PATH : null ;
583+ const preBunPathExt = typeof process . env . PATHEXT === "string" ? process . env . PATHEXT : null ;
584+ const preBunCodexCliManagerRoots = Object . fromEntries (
585+ CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS . flatMap ( name => {
586+ const value = process . env [ name ] ;
587+ return typeof value === "string" && value !== "" ? [ [ name , value ] ] : [ ] ;
588+ } ) ,
589+ ) ;
557590const launchProof = randomBytes ( 32 ) . toString ( "base64url" ) ;
558591const launchContext = JSON . stringify ( {
559592 version : 1 ,
560593 proof : launchProof ,
561594 anthropicEnvSlots : preBunAnthropicSlots ,
595+ codexCliInspectionEnv : codexCliUpdateInspection ? {
596+ codexCliPath : preBunCodexCliPath ,
597+ path : preBunPath ,
598+ pathExt : preBunPathExt ,
599+ managerRoots : preBunCodexCliManagerRoots ,
600+ configDir : configDir ( ) ,
601+ } : null ,
562602} ) ;
603+ // The inspection snapshot above already carries PATH, PATHEXT, and the manager-root slots as
604+ // proof-bound values, and `inspectCodexCliInstall` reads them from that snapshot rather than
605+ // from the live environment. Inheriting them again would spend the 32,767-character Windows
606+ // environment block twice, so a large-but-valid shell environment could stop the Bun child
607+ // from spawning and fail the command before it reports anything. Drop the duplicates for the
608+ // one-shot inspection launch only; every other launch inherits the environment unchanged.
609+ // Windows environment names are case-insensitive, but this spread produces an ordinary
610+ // case-sensitive object, and a real Windows environment commonly spells the variable `Path`.
611+ // Deleting only the canonical upper-case spelling would silently leave that copy behind and
612+ // reintroduce the duplication this block exists to prevent, so match on the lowercase form.
613+ const inheritedEnv = { ...process . env } ;
614+ if ( codexCliUpdateInspection ) {
615+ const snapshotted = new Set (
616+ [ "PATH" , "PATHEXT" , ...CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS ] . map ( name => name . toLowerCase ( ) ) ,
617+ ) ;
618+ for ( const name of Object . keys ( inheritedEnv ) ) {
619+ if ( snapshotted . has ( name . toLowerCase ( ) ) ) delete inheritedEnv [ name ] ;
620+ }
621+ }
563622const child = spawn ( bun , [ cliPath , `${ NODE_LAUNCH_PROOF_PREFIX } ${ launchProof } ` , ...process . argv . slice ( 2 ) ] , {
564623 stdio : "inherit" ,
565624 // A headless Windows parent (Task Scheduler, dashboard restart, shortcut) has no
566625 // console to inherit. Without this flag Windows allocates a visible console for
567626 // the long-running Bun child, and closing that window kills the proxy (#1236).
568627 windowsHide : true ,
569628 env : {
570- ...process . env ,
629+ ...inheritedEnv ,
571630 [ NODE_LAUNCH_CONTEXT_ENV ] : launchContext ,
572631 [ BUN_RUNTIME_SOURCE_ENV ] : bunRuntime . source ,
573632 [ BUN_RUNTIME_PATH_ENV ] : bunRuntime . path ,
0 commit comments