@@ -59,6 +59,7 @@ mod pager_overlay;
5959pub mod public_widgets;
6060mod render;
6161mod resume_picker;
62+ mod selection_list;
6263mod session_log;
6364mod shimmer;
6465mod slash_command;
@@ -70,7 +71,38 @@ mod terminal_palette;
7071mod text_formatting;
7172mod tui;
7273mod ui_consts;
74+ mod update_prompt;
7375mod version;
76+
77+ /// Update action the CLI should perform after the TUI exits.
78+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
79+ pub enum UpdateAction {
80+ /// Update via `npm install -g @openai/codex@latest`.
81+ NpmGlobalLatest ,
82+ /// Update via `bun install -g @openai/codex@latest`.
83+ BunGlobalLatest ,
84+ /// Update via `brew upgrade codex`.
85+ BrewUpgrade ,
86+ }
87+
88+ impl UpdateAction {
89+ /// Returns the list of command-line arguments for invoking the update.
90+ pub fn command_args ( & self ) -> ( & ' static str , & ' static [ & ' static str ] ) {
91+ match self {
92+ UpdateAction :: NpmGlobalLatest => ( "npm" , & [ "install" , "-g" , "@openai/codex@latest" ] ) ,
93+ UpdateAction :: BunGlobalLatest => ( "bun" , & [ "install" , "-g" , "@openai/codex@latest" ] ) ,
94+ UpdateAction :: BrewUpgrade => ( "brew" , & [ "upgrade" , "codex" ] ) ,
95+ }
96+ }
97+
98+ /// Returns string representation of the command-line arguments for invoking the update.
99+ pub fn command_str ( & self ) -> String {
100+ let ( command, args) = self . command_args ( ) ;
101+ let args_str = args. join ( " " ) ;
102+ format ! ( "{command} {args_str}" )
103+ }
104+ }
105+
74106mod wrapping;
75107
76108#[ cfg( test) ]
@@ -299,6 +331,26 @@ async fn run_ratatui_app(
299331
300332 let mut tui = Tui :: new ( terminal) ;
301333
334+ #[ cfg( not( debug_assertions) ) ]
335+ {
336+ use crate :: update_prompt:: UpdatePromptOutcome ;
337+
338+ let skip_update_prompt = cli. prompt . as_ref ( ) . is_some_and ( |prompt| !prompt. is_empty ( ) ) ;
339+ if !skip_update_prompt {
340+ match update_prompt:: run_update_prompt_if_needed ( & mut tui, & config) . await ? {
341+ UpdatePromptOutcome :: Continue => { }
342+ UpdatePromptOutcome :: RunUpdate ( action) => {
343+ crate :: tui:: restore ( ) ?;
344+ return Ok ( AppExitInfo {
345+ token_usage : codex_core:: protocol:: TokenUsage :: default ( ) ,
346+ conversation_id : None ,
347+ update_action : Some ( action) ,
348+ } ) ;
349+ }
350+ }
351+ }
352+ }
353+
302354 // Show update banner in terminal history (instead of stderr) so it is visible
303355 // within the TUI scrollback. Building spans keeps styling consistent.
304356 #[ cfg( not( debug_assertions) ) ]
@@ -309,9 +361,6 @@ async fn run_ratatui_app(
309361 use ratatui:: text:: Line ;
310362
311363 let current_version = env ! ( "CARGO_PKG_VERSION" ) ;
312- let exe = std:: env:: current_exe ( ) ?;
313- let managed_by_bun = std:: env:: var_os ( "CODEX_MANAGED_BY_BUN" ) . is_some ( ) ;
314- let managed_by_npm = std:: env:: var_os ( "CODEX_MANAGED_BY_NPM" ) . is_some ( ) ;
315364
316365 let mut content_lines: Vec < Line < ' static > > = vec ! [
317366 Line :: from( vec![
@@ -331,27 +380,10 @@ async fn run_ratatui_app(
331380 Line :: from( "" ) ,
332381 ] ;
333382
334- if managed_by_bun {
335- let bun_cmd = "bun install -g @openai/codex@latest" ;
336- content_lines. push ( Line :: from ( vec ! [
337- "Run " . into( ) ,
338- bun_cmd. cyan( ) ,
339- " to update." . into( ) ,
340- ] ) ) ;
341- } else if managed_by_npm {
342- let npm_cmd = "npm install -g @openai/codex@latest" ;
383+ if let Some ( update_action) = get_update_action ( ) {
343384 content_lines. push ( Line :: from ( vec ! [
344385 "Run " . into( ) ,
345- npm_cmd. cyan( ) ,
346- " to update." . into( ) ,
347- ] ) ) ;
348- } else if cfg ! ( target_os = "macos" )
349- && ( exe. starts_with ( "/opt/homebrew" ) || exe. starts_with ( "/usr/local" ) )
350- {
351- let brew_cmd = "brew upgrade codex" ;
352- content_lines. push ( Line :: from ( vec ! [
353- "Run " . into( ) ,
354- brew_cmd. cyan( ) ,
386+ update_action. command_str( ) . cyan( ) ,
355387 " to update." . into( ) ,
356388 ] ) ) ;
357389 } else {
@@ -405,6 +437,7 @@ async fn run_ratatui_app(
405437 return Ok ( AppExitInfo {
406438 token_usage : codex_core:: protocol:: TokenUsage :: default ( ) ,
407439 conversation_id : None ,
440+ update_action : None ,
408441 } ) ;
409442 }
410443 if should_show_windows_wsl_screen {
@@ -449,6 +482,7 @@ async fn run_ratatui_app(
449482 return Ok ( AppExitInfo {
450483 token_usage : codex_core:: protocol:: TokenUsage :: default ( ) ,
451484 conversation_id : None ,
485+ update_action : None ,
452486 } ) ;
453487 }
454488 other => other,
@@ -477,6 +511,47 @@ async fn run_ratatui_app(
477511 app_result
478512}
479513
514+ /// Get the update action from the environment.
515+ /// Returns `None` if not managed by npm, bun, or brew.
516+ #[ cfg( not( debug_assertions) ) ]
517+ pub ( crate ) fn get_update_action ( ) -> Option < UpdateAction > {
518+ let exe = std:: env:: current_exe ( ) . unwrap_or_default ( ) ;
519+ let managed_by_npm = std:: env:: var_os ( "CODEX_MANAGED_BY_NPM" ) . is_some ( ) ;
520+ let managed_by_bun = std:: env:: var_os ( "CODEX_MANAGED_BY_BUN" ) . is_some ( ) ;
521+ if managed_by_npm {
522+ Some ( UpdateAction :: NpmGlobalLatest )
523+ } else if managed_by_bun {
524+ Some ( UpdateAction :: BunGlobalLatest )
525+ } else if cfg ! ( target_os = "macos" )
526+ && ( exe. starts_with ( "/opt/homebrew" ) || exe. starts_with ( "/usr/local" ) )
527+ {
528+ Some ( UpdateAction :: BrewUpgrade )
529+ } else {
530+ None
531+ }
532+ }
533+
534+ #[ test]
535+ #[ cfg( not( debug_assertions) ) ]
536+ fn test_get_update_action ( ) {
537+ let prev = std:: env:: var_os ( "CODEX_MANAGED_BY_NPM" ) ;
538+
539+ // First: no npm var -> expect None (we do not run from brew in CI)
540+ unsafe { std:: env:: remove_var ( "CODEX_MANAGED_BY_NPM" ) } ;
541+ assert_eq ! ( get_update_action( ) , None ) ;
542+
543+ // Then: with npm var -> expect NpmGlobalLatest
544+ unsafe { std:: env:: set_var ( "CODEX_MANAGED_BY_NPM" , "1" ) } ;
545+ assert_eq ! ( get_update_action( ) , Some ( UpdateAction :: NpmGlobalLatest ) ) ;
546+
547+ // Restore prior value to avoid leaking state
548+ if let Some ( v) = prev {
549+ unsafe { std:: env:: set_var ( "CODEX_MANAGED_BY_NPM" , v) } ;
550+ } else {
551+ unsafe { std:: env:: remove_var ( "CODEX_MANAGED_BY_NPM" ) } ;
552+ }
553+ }
554+
480555#[ expect(
481556 clippy:: print_stderr,
482557 reason = "TUI should no longer be displayed, so we can write to stderr."
0 commit comments