1+ #![ cfg( not( debug_assertions) ) ]
2+
3+ use crate :: update_action;
4+ use crate :: update_action:: UpdateAction ;
15use chrono:: DateTime ;
26use chrono:: Duration ;
37use chrono:: Utc ;
8+ use codex_core:: config:: Config ;
9+ use codex_core:: default_client:: create_client;
410use serde:: Deserialize ;
511use serde:: Serialize ;
612use std:: path:: Path ;
713use std:: path:: PathBuf ;
814
9- use codex_core:: config:: Config ;
10- use codex_core:: default_client:: create_client;
11-
1215use crate :: version:: CODEX_CLI_VERSION ;
1316
1417pub fn get_upgrade_version ( config : & Config ) -> Option < String > {
@@ -47,14 +50,17 @@ struct VersionInfo {
4750 dismissed_version : Option < String > ,
4851}
4952
53+ const VERSION_FILENAME : & str = "version.json" ;
54+ // We use the latest version from the cask if installation is via homebrew - homebrew does not immediately pick up the latest release and can lag behind.
55+ const HOMEBREW_CASK_URL : & str =
56+ "https://raw.githubusercontent.com/Homebrew/homebrew-cask/HEAD/Casks/c/codex.rb" ;
57+ const LATEST_RELEASE_URL : & str = "https://api.github.com/repos/openai/codex/releases/latest" ;
58+
5059#[ derive( Deserialize , Debug , Clone ) ]
5160struct ReleaseInfo {
5261 tag_name : String ,
5362}
5463
55- const VERSION_FILENAME : & str = "version.json" ;
56- const LATEST_RELEASE_URL : & str = "https://api.github.com/repos/openai/codex/releases/latest" ;
57-
5864fn version_filepath ( config : & Config ) -> PathBuf {
5965 config. codex_home . join ( VERSION_FILENAME )
6066}
@@ -65,23 +71,35 @@ fn read_version_info(version_file: &Path) -> anyhow::Result<VersionInfo> {
6571}
6672
6773async fn check_for_update ( version_file : & Path ) -> anyhow:: Result < ( ) > {
68- let ReleaseInfo {
69- tag_name : latest_tag_name,
70- } = create_client ( )
71- . get ( LATEST_RELEASE_URL )
72- . send ( )
73- . await ?
74- . error_for_status ( ) ?
75- . json :: < ReleaseInfo > ( )
76- . await ?;
74+ let latest_version = match update_action:: get_update_action ( ) {
75+ Some ( UpdateAction :: BrewUpgrade ) => {
76+ let cask_contents = create_client ( )
77+ . get ( HOMEBREW_CASK_URL )
78+ . send ( )
79+ . await ?
80+ . error_for_status ( ) ?
81+ . text ( )
82+ . await ?;
83+ extract_version_from_cask ( & cask_contents) ?
84+ }
85+ _ => {
86+ let ReleaseInfo {
87+ tag_name : latest_tag_name,
88+ } = create_client ( )
89+ . get ( LATEST_RELEASE_URL )
90+ . send ( )
91+ . await ?
92+ . error_for_status ( ) ?
93+ . json :: < ReleaseInfo > ( )
94+ . await ?;
95+ extract_version_from_latest_tag ( & latest_tag_name) ?
96+ }
97+ } ;
7798
7899 // Preserve any previously dismissed version if present.
79100 let prev_info = read_version_info ( version_file) . ok ( ) ;
80101 let info = VersionInfo {
81- latest_version : latest_tag_name
82- . strip_prefix ( "rust-v" )
83- . ok_or_else ( || anyhow:: anyhow!( "Failed to parse latest tag name '{latest_tag_name}'" ) ) ?
84- . into ( ) ,
102+ latest_version,
85103 last_checked_at : Utc :: now ( ) ,
86104 dismissed_version : prev_info. and_then ( |p| p. dismissed_version ) ,
87105 } ;
@@ -101,6 +119,25 @@ fn is_newer(latest: &str, current: &str) -> Option<bool> {
101119 }
102120}
103121
122+ fn extract_version_from_cask ( cask_contents : & str ) -> anyhow:: Result < String > {
123+ cask_contents
124+ . lines ( )
125+ . find_map ( |line| {
126+ let line = line. trim ( ) ;
127+ line. strip_prefix ( "version \" " )
128+ . and_then ( |rest| rest. strip_suffix ( '"' ) )
129+ . map ( ToString :: to_string)
130+ } )
131+ . ok_or_else ( || anyhow:: anyhow!( "Failed to find version in Homebrew cask file" ) )
132+ }
133+
134+ fn extract_version_from_latest_tag ( latest_tag_name : & str ) -> anyhow:: Result < String > {
135+ latest_tag_name
136+ . strip_prefix ( "rust-v" )
137+ . map ( str:: to_owned)
138+ . ok_or_else ( || anyhow:: anyhow!( "Failed to parse latest tag name '{latest_tag_name}'" ) )
139+ }
140+
104141/// Returns the latest version to show in a popup, if it should be shown.
105142/// This respects the user's dismissal choice for the current latest version.
106143pub fn get_upgrade_version_for_popup ( config : & Config ) -> Option < String > {
@@ -140,56 +177,35 @@ fn parse_version(v: &str) -> Option<(u64, u64, u64)> {
140177 Some ( ( maj, min, pat) )
141178}
142179
143- /// Update action the CLI should perform after the TUI exits.
144- #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
145- pub enum UpdateAction {
146- /// Update via `npm install -g @openai/codex@latest`.
147- NpmGlobalLatest ,
148- /// Update via `bun install -g @openai/codex@latest`.
149- BunGlobalLatest ,
150- /// Update via `brew upgrade codex`.
151- BrewUpgrade ,
152- }
180+ #[ cfg( test) ]
181+ mod tests {
182+ use super :: * ;
153183
154- #[ cfg( any( not( debug_assertions) , test) ) ]
155- pub ( crate ) fn get_update_action ( ) -> Option < UpdateAction > {
156- let exe = std:: env:: current_exe ( ) . unwrap_or_default ( ) ;
157- let managed_by_npm = std:: env:: var_os ( "CODEX_MANAGED_BY_NPM" ) . is_some ( ) ;
158- let managed_by_bun = std:: env:: var_os ( "CODEX_MANAGED_BY_BUN" ) . is_some ( ) ;
159- if managed_by_npm {
160- Some ( UpdateAction :: NpmGlobalLatest )
161- } else if managed_by_bun {
162- Some ( UpdateAction :: BunGlobalLatest )
163- } else if cfg ! ( target_os = "macos" )
164- && ( exe. starts_with ( "/opt/homebrew" ) || exe. starts_with ( "/usr/local" ) )
165- {
166- Some ( UpdateAction :: BrewUpgrade )
167- } else {
168- None
184+ #[ test]
185+ fn parses_version_from_cask_contents ( ) {
186+ let cask = r#"
187+ cask "codex" do
188+ version "0.55.0"
189+ end
190+ "# ;
191+ assert_eq ! (
192+ extract_version_from_cask( cask) . expect( "failed to parse version" ) ,
193+ "0.55.0"
194+ ) ;
169195 }
170- }
171196
172- impl UpdateAction {
173- /// Returns the list of command-line arguments for invoking the update.
174- pub fn command_args ( self ) -> ( & ' static str , & ' static [ & ' static str ] ) {
175- match self {
176- UpdateAction :: NpmGlobalLatest => ( "npm" , & [ "install" , "-g" , "@openai/codex@latest" ] ) ,
177- UpdateAction :: BunGlobalLatest => ( "bun" , & [ "install" , "-g" , "@openai/codex@latest" ] ) ,
178- UpdateAction :: BrewUpgrade => ( "brew" , & [ "upgrade" , "--cask" , "codex" ] ) ,
179- }
197+ #[ test]
198+ fn extracts_version_from_latest_tag ( ) {
199+ assert_eq ! (
200+ extract_version_from_latest_tag( "rust-v1.5.0" ) . expect( "failed to parse version" ) ,
201+ "1.5.0"
202+ ) ;
180203 }
181204
182- /// Returns string representation of the command-line arguments for invoking the update.
183- pub fn command_str ( self ) -> String {
184- let ( command, args) = self . command_args ( ) ;
185- let args_str = args. join ( " " ) ;
186- format ! ( "{command} {args_str}" )
205+ #[ test]
206+ fn latest_tag_without_prefix_is_invalid ( ) {
207+ assert ! ( extract_version_from_latest_tag( "v1.5.0" ) . is_err( ) ) ;
187208 }
188- }
189-
190- #[ cfg( test) ]
191- mod tests {
192- use super :: * ;
193209
194210 #[ test]
195211 fn prerelease_version_is_not_considered_newer ( ) {
@@ -210,24 +226,4 @@ mod tests {
210226 assert_eq ! ( parse_version( " 1.2.3 \n " ) , Some ( ( 1 , 2 , 3 ) ) ) ;
211227 assert_eq ! ( is_newer( " 1.2.3 " , "1.2.2" ) , Some ( true ) ) ;
212228 }
213-
214- #[ test]
215- fn test_get_update_action ( ) {
216- let prev = std:: env:: var_os ( "CODEX_MANAGED_BY_NPM" ) ;
217-
218- // First: no npm var -> expect None (we do not run from brew in CI)
219- unsafe { std:: env:: remove_var ( "CODEX_MANAGED_BY_NPM" ) } ;
220- assert_eq ! ( get_update_action( ) , None ) ;
221-
222- // Then: with npm var -> expect NpmGlobalLatest
223- unsafe { std:: env:: set_var ( "CODEX_MANAGED_BY_NPM" , "1" ) } ;
224- assert_eq ! ( get_update_action( ) , Some ( UpdateAction :: NpmGlobalLatest ) ) ;
225-
226- // Restore prior value to avoid leaking state
227- if let Some ( v) = prev {
228- unsafe { std:: env:: set_var ( "CODEX_MANAGED_BY_NPM" , v) } ;
229- } else {
230- unsafe { std:: env:: remove_var ( "CODEX_MANAGED_BY_NPM" ) } ;
231- }
232- }
233229}
0 commit comments