1+ //! Upgrade rnr binaries to the latest version
2+
13use anyhow:: { Context , Result } ;
4+ use std:: fs;
25use std:: path:: PathBuf ;
36
7+ use crate :: platform:: Platform ;
8+ use crate :: rnr_config:: RnrConfig ;
9+
10+ /// GitHub repository for releases
11+ const GITHUB_REPO : & str = "CodingWithCalvin/rnr.cli" ;
12+
413/// Run the upgrade command
514pub fn run ( ) -> Result < ( ) > {
615 let rnr_dir = find_rnr_dir ( ) ?;
@@ -10,16 +19,26 @@ pub fn run() -> Result<()> {
1019 anyhow:: bail!( "rnr is not initialized. Run 'rnr init' first." ) ;
1120 }
1221
13- println ! ( "Upgrading rnr binaries..." ) ;
22+ // Load current config
23+ let config_path = rnr_dir. join ( "config.yaml" ) ;
24+ let mut config = RnrConfig :: load_from ( & config_path) ?;
25+ let platforms = config. get_platforms ( ) ;
26+
27+ if platforms. is_empty ( ) {
28+ anyhow:: bail!( "No platforms configured. Run 'rnr init' to set up platforms." ) ;
29+ }
30+
31+ println ! ( "Checking for updates...\n " ) ;
32+ println ! ( " Current version: v{}" , config. version) ;
1433
1534 #[ cfg( feature = "network" ) ]
1635 {
17- upgrade_binaries ( & bin_dir) ?;
36+ upgrade_binaries ( & bin_dir, & mut config , & config_path , & platforms ) ?;
1837 }
1938
2039 #[ cfg( not( feature = "network" ) ) ]
2140 {
22- println ! ( "Network feature is disabled. Cannot download updates." ) ;
41+ println ! ( "\n Network feature is disabled. Cannot check for updates." ) ;
2342 println ! ( "Please manually update binaries in .rnr/bin/" ) ;
2443 }
2544
@@ -46,22 +65,169 @@ fn find_rnr_dir() -> Result<PathBuf> {
4665 anyhow:: bail!( "No .rnr directory found. Run 'rnr init' first." )
4766}
4867
49- /// Download and replace binaries
68+ /// Upgrade binaries to the latest version
5069#[ cfg( feature = "network" ) ]
51- fn upgrade_binaries ( bin_dir : & std:: path:: Path ) -> Result < ( ) > {
52- // TODO: Implement actual binary downloads
53- // 1. Check current version
54- // 2. Check latest version from server
55- // 3. Download if newer version available
56- // 4. Replace binaries
70+ fn upgrade_binaries (
71+ bin_dir : & std:: path:: Path ,
72+ config : & mut RnrConfig ,
73+ config_path : & std:: path:: Path ,
74+ platforms : & [ Platform ] ,
75+ ) -> Result < ( ) > {
76+ // Get latest release info from GitHub
77+ let latest_version = get_latest_version ( ) ?;
78+ println ! ( " Latest version: v{}" , latest_version) ;
5779
58- println ! ( " Checking for updates..." ) ;
59- println ! ( " TODO: Check https://rnr.dev/bin/latest/" ) ;
60- println ! ( " TODO: Download updated binaries" ) ;
61- println ! ( "\n Upgrade complete!" ) ;
80+ // Compare versions
81+ if !is_newer_version ( & config. version , & latest_version) {
82+ println ! ( "\n You're already on the latest version!" ) ;
83+ return Ok ( ( ) ) ;
84+ }
6285
63- // Placeholder for actual implementation
64- let _ = bin_dir;
86+ println ! ( "\n Upgrading to v{}...\n " , latest_version) ;
87+
88+ // Download new binaries for all configured platforms
89+ for platform in platforms {
90+ print ! ( " Downloading {}..." , platform. binary_name( ) ) ;
91+ let binary_path = bin_dir. join ( platform. binary_name ( ) ) ;
92+ download_binary ( * platform, & latest_version, & binary_path) ?;
93+ println ! ( " done" ) ;
94+ }
95+
96+ // Update config version
97+ config. version = latest_version. clone ( ) ;
98+ config. save_to ( config_path) ?;
99+
100+ println ! ( "\n Upgrade complete! Now running v{}" , latest_version) ;
65101
66102 Ok ( ( ) )
67103}
104+
105+ /// Get the latest release version from GitHub
106+ #[ cfg( feature = "network" ) ]
107+ fn get_latest_version ( ) -> Result < String > {
108+ let url = format ! (
109+ "https://api.github.com/repos/{}/releases/latest" ,
110+ GITHUB_REPO
111+ ) ;
112+
113+ let client = reqwest:: blocking:: Client :: builder ( )
114+ . user_agent ( "rnr-cli" )
115+ . build ( )
116+ . context ( "Failed to create HTTP client" ) ?;
117+
118+ let response = client
119+ . get ( & url)
120+ . send ( )
121+ . context ( "Failed to fetch latest release info" ) ?;
122+
123+ if !response. status ( ) . is_success ( ) {
124+ if response. status ( ) . as_u16 ( ) == 404 {
125+ anyhow:: bail!( "No releases found. This may be the first version." ) ;
126+ }
127+ anyhow:: bail!(
128+ "Failed to fetch release info: HTTP {}" ,
129+ response. status( ) . as_u16( )
130+ ) ;
131+ }
132+
133+ let json: serde_json:: Value = response
134+ . json ( )
135+ . context ( "Failed to parse release info as JSON" ) ?;
136+
137+ let tag = json[ "tag_name" ]
138+ . as_str ( )
139+ . context ( "Release missing tag_name" ) ?;
140+
141+ // Strip 'v' prefix if present
142+ let version = tag. strip_prefix ( 'v' ) . unwrap_or ( tag) ;
143+ Ok ( version. to_string ( ) )
144+ }
145+
146+ /// Download a binary for a specific platform and version
147+ #[ cfg( feature = "network" ) ]
148+ fn download_binary ( platform : Platform , version : & str , dest : & std:: path:: Path ) -> Result < ( ) > {
149+ let url = format ! (
150+ "https://github.com/{}/releases/download/v{}/{}" ,
151+ GITHUB_REPO ,
152+ version,
153+ platform. binary_name( )
154+ ) ;
155+
156+ let client = reqwest:: blocking:: Client :: builder ( )
157+ . user_agent ( "rnr-cli" )
158+ . build ( )
159+ . context ( "Failed to create HTTP client" ) ?;
160+
161+ let response = client
162+ . get ( & url)
163+ . send ( )
164+ . with_context ( || format ! ( "Failed to download {}" , platform. binary_name( ) ) ) ?;
165+
166+ if !response. status ( ) . is_success ( ) {
167+ anyhow:: bail!(
168+ "Failed to download {}: HTTP {}" ,
169+ platform. binary_name( ) ,
170+ response. status( ) . as_u16( )
171+ ) ;
172+ }
173+
174+ let bytes = response
175+ . bytes ( )
176+ . with_context ( || format ! ( "Failed to read response for {}" , platform. binary_name( ) ) ) ?;
177+
178+ // Write to file
179+ fs:: write ( dest, & bytes) . with_context ( || format ! ( "Failed to write {}" , dest. display( ) ) ) ?;
180+
181+ // Make executable on Unix
182+ #[ cfg( unix) ]
183+ {
184+ use std:: os:: unix:: fs:: PermissionsExt ;
185+ let mut perms = fs:: metadata ( dest) ?. permissions ( ) ;
186+ perms. set_mode ( 0o755 ) ;
187+ fs:: set_permissions ( dest, perms) ?;
188+ }
189+
190+ Ok ( ( ) )
191+ }
192+
193+ /// Compare semantic versions, returns true if latest is newer than current
194+ #[ cfg( feature = "network" ) ]
195+ fn is_newer_version ( current : & str , latest : & str ) -> bool {
196+ let parse_version = |v : & str | -> ( u32 , u32 , u32 ) {
197+ let parts: Vec < & str > = v. split ( '.' ) . collect ( ) ;
198+ let major = parts. first ( ) . and_then ( |s| s. parse ( ) . ok ( ) ) . unwrap_or ( 0 ) ;
199+ let minor = parts. get ( 1 ) . and_then ( |s| s. parse ( ) . ok ( ) ) . unwrap_or ( 0 ) ;
200+ let patch = parts. get ( 2 ) . and_then ( |s| s. parse ( ) . ok ( ) ) . unwrap_or ( 0 ) ;
201+ ( major, minor, patch)
202+ } ;
203+
204+ let ( cur_major, cur_minor, cur_patch) = parse_version ( current) ;
205+ let ( lat_major, lat_minor, lat_patch) = parse_version ( latest) ;
206+
207+ if lat_major > cur_major {
208+ return true ;
209+ }
210+ if lat_major == cur_major && lat_minor > cur_minor {
211+ return true ;
212+ }
213+ if lat_major == cur_major && lat_minor == cur_minor && lat_patch > cur_patch {
214+ return true ;
215+ }
216+ false
217+ }
218+
219+ #[ cfg( test) ]
220+ mod tests {
221+ use super :: * ;
222+
223+ #[ test]
224+ #[ cfg( feature = "network" ) ]
225+ fn test_version_comparison ( ) {
226+ assert ! ( is_newer_version( "0.1.0" , "0.2.0" ) ) ;
227+ assert ! ( is_newer_version( "0.1.0" , "1.0.0" ) ) ;
228+ assert ! ( is_newer_version( "0.1.0" , "0.1.1" ) ) ;
229+ assert ! ( !is_newer_version( "0.2.0" , "0.1.0" ) ) ;
230+ assert ! ( !is_newer_version( "1.0.0" , "0.9.0" ) ) ;
231+ assert ! ( !is_newer_version( "0.1.0" , "0.1.0" ) ) ;
232+ }
233+ }
0 commit comments