@@ -59,6 +59,12 @@ enum Commands {
5959 #[ command( subcommand) ]
6060 cmd : OverrideCommands ,
6161 } ,
62+ /// Update RAZ to the latest version
63+ SelfUpdate {
64+ /// Force update even if already on latest version
65+ #[ arg( long) ]
66+ force : bool ,
67+ } ,
6268}
6369
6470#[ derive( Subcommand ) ]
@@ -194,6 +200,9 @@ async fn run(cli: Cli) -> anyhow::Result<()> {
194200 Commands :: Override { cmd } => {
195201 return handle_override_command ( & working_dir, cmd) ;
196202 }
203+ Commands :: SelfUpdate { force } => {
204+ return handle_self_update ( force) . await ;
205+ }
197206 }
198207 }
199208
@@ -809,6 +818,130 @@ fn detect_subcommand(file_path: &Path, cursor: Option<Position>) -> String {
809818 }
810819}
811820
821+ /// Handle self-update command
822+ async fn handle_self_update ( force : bool ) -> anyhow:: Result < ( ) > {
823+ use std:: process:: Stdio ;
824+
825+ println ! ( "{} Checking for updates..." , OutputFormatter :: info( "RAZ" ) ) ;
826+
827+ // Get current version
828+ let current_version = env ! ( "CARGO_PKG_VERSION" ) ;
829+ println ! ( "{} Current version: v{}" , OutputFormatter :: label( "Info" ) , current_version) ;
830+
831+ // Check if running from cargo install or system PATH
832+ let current_exe = env:: current_exe ( ) ?;
833+ let exe_dir = current_exe. parent ( ) . unwrap ( ) ;
834+
835+ // Check if we're in a cargo bin directory
836+ let is_cargo_installed = exe_dir. to_string_lossy ( ) . contains ( ".cargo/bin" ) ||
837+ exe_dir. to_string_lossy ( ) . contains ( ".cargo\\ bin" ) ;
838+
839+ if !is_cargo_installed {
840+ // Check if it's in system PATH but not cargo
841+ let which_result = process:: Command :: new ( "which" )
842+ . arg ( "raz" )
843+ . output ( ) ;
844+
845+ if let Ok ( output) = which_result {
846+ let path = String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ;
847+ if !path. is_empty ( ) && !path. contains ( ".cargo" ) {
848+ println ! (
849+ "\n {} RAZ appears to be installed via a package manager or custom installation." ,
850+ OutputFormatter :: warning( "Note" )
851+ ) ;
852+ println ! (
853+ "Please update using the same method you used to install RAZ."
854+ ) ;
855+ return Ok ( ( ) ) ;
856+ }
857+ }
858+ }
859+
860+ // Get latest version from GitHub API
861+ let latest_version = get_latest_version ( ) . await ?;
862+ println ! ( "{} Latest version: {}" , OutputFormatter :: label( "Info" ) , latest_version) ;
863+
864+ // Compare versions
865+ if !force && current_version == latest_version. trim_start_matches ( 'v' ) {
866+ println ! (
867+ "\n {} You are already on the latest version!" ,
868+ OutputFormatter :: success( "✓" )
869+ ) ;
870+ return Ok ( ( ) ) ;
871+ }
872+
873+ // Prompt for confirmation
874+ if !force {
875+ print ! ( "\n Update RAZ from v{} to {}? [Y/n] " , current_version, latest_version) ;
876+ use std:: io:: { self , Write } ;
877+ io:: stdout ( ) . flush ( ) ?;
878+
879+ let mut input = String :: new ( ) ;
880+ io:: stdin ( ) . read_line ( & mut input) ?;
881+ let choice = input. trim ( ) . to_lowercase ( ) ;
882+
883+ if choice != "y" && choice != "" {
884+ println ! ( "Update cancelled." ) ;
885+ return Ok ( ( ) ) ;
886+ }
887+ }
888+
889+ // Perform update using cargo install
890+ println ! ( "\n {} Updating RAZ..." , OutputFormatter :: info( "Installing" ) ) ;
891+ println ! ( "{} This may take a few minutes..." , OutputFormatter :: dim( "Note" ) ) ;
892+
893+ let mut cmd = process:: Command :: new ( "cargo" ) ;
894+ cmd. arg ( "install" )
895+ . arg ( "raz-cli" )
896+ . arg ( "--force" )
897+ . stdout ( Stdio :: inherit ( ) )
898+ . stderr ( Stdio :: inherit ( ) ) ;
899+
900+ let status = cmd. status ( ) ?;
901+
902+ if status. success ( ) {
903+ println ! (
904+ "\n {} RAZ has been successfully updated to {}!" ,
905+ OutputFormatter :: success( "Success" ) ,
906+ latest_version
907+ ) ;
908+ println ! (
909+ "{} Run {} to verify the update." ,
910+ OutputFormatter :: info( "Tip" ) ,
911+ OutputFormatter :: command( "raz --version" )
912+ ) ;
913+ } else {
914+ anyhow:: bail!( "Failed to update RAZ. Please try again or install manually." ) ;
915+ }
916+
917+ Ok ( ( ) )
918+ }
919+
920+ /// Get latest version from GitHub releases
921+ async fn get_latest_version ( ) -> anyhow:: Result < String > {
922+ let url = "https://api.github.com/repos/codeitlikemiley/raz/releases/latest" ;
923+
924+ // Use a simple HTTPS request
925+ let output = process:: Command :: new ( "curl" )
926+ . args ( & [ "-s" , "-H" , "Accept: application/vnd.github.v3+json" , url] )
927+ . output ( ) ?;
928+
929+ if !output. status . success ( ) {
930+ anyhow:: bail!( "Failed to fetch latest version from GitHub" ) ;
931+ }
932+
933+ let response = String :: from_utf8 ( output. stdout ) ?;
934+
935+ // Parse JSON to get tag_name
936+ let json: serde_json:: Value = serde_json:: from_str ( & response) ?;
937+
938+ let tag_name = json[ "tag_name" ]
939+ . as_str ( )
940+ . ok_or_else ( || anyhow:: anyhow!( "Failed to parse version from GitHub response" ) ) ?;
941+
942+ Ok ( tag_name. to_string ( ) )
943+ }
944+
812945/// Initialize configuration for the current project
813946fn init_config ( working_dir : & Path , template : & str , force : bool ) -> anyhow:: Result < ( ) > {
814947 // Auto-detect project type if using default template
0 commit comments