@@ -2195,8 +2195,10 @@ def _system_diagnostics(self):
21952195 " [cyan]3[/cyan] Reset progress" ,
21962196 " [cyan]4[/cyan] Show configuration" ,
21972197 " [cyan]5[/cyan] Install dependencies" ,
2198- " [cyan]6[/cyan] Reset all data (progress + workspaces)" ,
2199- " [cyan]7[/cyan] Uninstall LEARN CLI" ,
2198+ " [cyan]6[/cyan] Check for updates" ,
2199+ " [cyan]7[/cyan] Update to latest version" ,
2200+ " [cyan]8[/cyan] Reset all data (progress + workspaces)" ,
2201+ " [cyan]9[/cyan] Uninstall LEARN CLI" ,
22002202 " [cyan]b[/cyan] Back"
22012203 ]
22022204 for item in diag_items :
@@ -2209,8 +2211,10 @@ def _system_diagnostics(self):
22092211 print (" 3 Reset progress" )
22102212 print (" 4 Show configuration" )
22112213 print (" 5 Install dependencies" )
2212- print (" 6 Reset all data (progress + workspaces)" )
2213- print (" 7 Uninstall LEARN CLI" )
2214+ print (" 6 Check for updates" )
2215+ print (" 7 Update to latest version" )
2216+ print (" 8 Reset all data (progress + workspaces)" )
2217+ print (" 9 Uninstall LEARN CLI" )
22142218 print (" b Back" )
22152219
22162220 choice = input ("\n → Select option: " ).strip ().lower ()
@@ -2257,12 +2261,49 @@ def _system_diagnostics(self):
22572261 self ._install_dependencies ()
22582262
22592263 elif choice == "6" :
2264+ # Check for updates
2265+ self ._clear_screen ()
2266+ update_info = check_for_updates (self .learn_dir )
2267+
2268+ if "error" in update_info :
2269+ if console :
2270+ console .print (f"[bold red]Error:[/bold red] { update_info ['error' ]} " )
2271+ else :
2272+ print (f"Error: { update_info ['error' ]} " )
2273+ elif update_info ["has_update" ]:
2274+ if console :
2275+ console .print (f"[bold yellow]Update available![/bold yellow]" )
2276+ console .print (f" Current: [cyan]{ update_info ['current' ]} [/cyan]" )
2277+ console .print (f" Latest: [green]{ update_info ['latest' ]} [/green]" )
2278+ console .print (f" Commits behind: [yellow]{ update_info ['commits_behind' ]} [/yellow]" )
2279+ else :
2280+ print (f"Update available!" )
2281+ print (f" Current: { update_info ['current' ]} " )
2282+ print (f" Latest: { update_info ['latest' ]} " )
2283+ print (f" Commits behind: { update_info ['commits_behind' ]} " )
2284+ else :
2285+ if console :
2286+ console .print (f"[bold green]✓ You're on the latest version[/bold green]" )
2287+ console .print (f"[dim]Version: { update_info ['current' ]} [/dim]" )
2288+ else :
2289+ print (f"✓ You're on the latest version" )
2290+ print (f"Version: { update_info ['current' ]} " )
2291+
2292+ input ("\n Press Enter to continue..." )
2293+
2294+ elif choice == "7" :
2295+ # Update to latest version
2296+ self ._clear_screen ()
2297+ update_learn (self .learn_dir )
2298+ input ("\n Press Enter to continue..." )
2299+
2300+ elif choice == "8" :
22602301 # Reset all data
22612302 self ._clear_screen ()
22622303 reset_user_data (self .learn_dir )
22632304 input ("\n Press Enter to continue..." )
22642305
2265- elif choice == "7 " :
2306+ elif choice == "9 " :
22662307 # Uninstall
22672308 self ._clear_screen ()
22682309 uninstall_learn (self .learn_dir )
@@ -2377,6 +2418,179 @@ def _install_dependencies(self):
23772418 input ("Press Enter to continue..." )
23782419
23792420
2421+ def check_for_updates (learn_dir : Path ) -> dict :
2422+ """Check if updates are available from GitHub
2423+
2424+ Returns:
2425+ dict with keys: has_update (bool), current (str), latest (str), commits_behind (int)
2426+ """
2427+ try :
2428+ import subprocess
2429+
2430+ # Get current commit hash
2431+ result = subprocess .run (
2432+ ["git" , "-C" , str (learn_dir ), "rev-parse" , "HEAD" ],
2433+ capture_output = True ,
2434+ text = True ,
2435+ timeout = 5
2436+ )
2437+ current_hash = result .stdout .strip () if result .returncode == 0 else None
2438+
2439+ # Fetch latest from remote
2440+ subprocess .run (
2441+ ["git" , "-C" , str (learn_dir ), "fetch" , "origin" , "main" ],
2442+ capture_output = True ,
2443+ timeout = 10
2444+ )
2445+
2446+ # Get remote commit hash
2447+ result = subprocess .run (
2448+ ["git" , "-C" , str (learn_dir ), "rev-parse" , "origin/main" ],
2449+ capture_output = True ,
2450+ text = True ,
2451+ timeout = 5
2452+ )
2453+ remote_hash = result .stdout .strip () if result .returncode == 0 else None
2454+
2455+ if not current_hash or not remote_hash :
2456+ return {"has_update" : False , "current" : "unknown" , "latest" : "unknown" , "commits_behind" : 0 }
2457+
2458+ # Count commits behind
2459+ result = subprocess .run (
2460+ ["git" , "-C" , str (learn_dir ), "rev-list" , "--count" , f"{ current_hash } ..{ remote_hash } " ],
2461+ capture_output = True ,
2462+ text = True ,
2463+ timeout = 5
2464+ )
2465+ commits_behind = int (result .stdout .strip ()) if result .returncode == 0 else 0
2466+
2467+ return {
2468+ "has_update" : current_hash != remote_hash ,
2469+ "current" : current_hash [:7 ],
2470+ "latest" : remote_hash [:7 ],
2471+ "commits_behind" : commits_behind
2472+ }
2473+
2474+ except Exception as e :
2475+ return {"has_update" : False , "current" : "error" , "latest" : "error" , "commits_behind" : 0 , "error" : str (e )}
2476+
2477+
2478+ def update_learn (learn_dir : Path ):
2479+ """Update LEARN to the latest version"""
2480+ if console :
2481+ console .print ("[bold cyan]╔══════════════════════════════════════════════════════════════╗[/bold cyan]" )
2482+ console .print ("[bold cyan]║ UPDATE LEARN CLI ║[/bold cyan]" )
2483+ console .print ("[bold cyan]╚══════════════════════════════════════════════════════════════╝[/bold cyan]" )
2484+ console .print ("\n [bold]Checking for updates...[/bold]" )
2485+ else :
2486+ print ("=" * 70 )
2487+ print (" UPDATE LEARN CLI" )
2488+ print ("=" * 70 )
2489+ print ("\n Checking for updates..." )
2490+
2491+ update_info = check_for_updates (learn_dir )
2492+
2493+ if "error" in update_info :
2494+ if console :
2495+ console .print (f"\n [bold red]Error checking for updates:[/bold red] { update_info ['error' ]} " )
2496+ else :
2497+ print (f"\n Error checking for updates: { update_info ['error' ]} " )
2498+ return
2499+
2500+ if not update_info ["has_update" ]:
2501+ if console :
2502+ console .print ("\n [bold green]✓ You're already on the latest version![/bold green]" )
2503+ console .print (f"[dim]Current version: { update_info ['current' ]} [/dim]" )
2504+ else :
2505+ print ("\n ✓ You're already on the latest version!" )
2506+ print (f"Current version: { update_info ['current' ]} " )
2507+ return
2508+
2509+ # Show update info
2510+ if console :
2511+ console .print (f"\n [bold yellow]Update available![/bold yellow]" )
2512+ console .print (f" Current version: [cyan]{ update_info ['current' ]} [/cyan]" )
2513+ console .print (f" Latest version: [green]{ update_info ['latest' ]} [/green]" )
2514+ console .print (f" Commits behind: [yellow]{ update_info ['commits_behind' ]} [/yellow]" )
2515+ else :
2516+ print (f"\n Update available!" )
2517+ print (f" Current version: { update_info ['current' ]} " )
2518+ print (f" Latest version: { update_info ['latest' ]} " )
2519+ print (f" Commits behind: { update_info ['commits_behind' ]} " )
2520+
2521+ confirm = input ("\n Do you want to update now? (Y/n): " ).strip ().lower ()
2522+
2523+ if confirm == 'n' :
2524+ if console :
2525+ console .print ("\n [bold]Update cancelled.[/bold]" )
2526+ else :
2527+ print ("\n Update cancelled." )
2528+ return
2529+
2530+ # Perform update
2531+ if console :
2532+ console .print ("\n [bold cyan]Updating...[/bold cyan]" )
2533+ else :
2534+ print ("\n Updating..." )
2535+
2536+ try :
2537+ # Pull latest changes
2538+ result = subprocess .run (
2539+ ["git" , "-C" , str (learn_dir ), "pull" , "origin" , "main" ],
2540+ capture_output = True ,
2541+ text = True ,
2542+ timeout = 30
2543+ )
2544+
2545+ if result .returncode != 0 :
2546+ if console :
2547+ console .print (f"\n [bold red]Update failed:[/bold red]" )
2548+ console .print (f"[dim]{ result .stderr } [/dim]" )
2549+ else :
2550+ print (f"\n Update failed:" )
2551+ print (result .stderr )
2552+ return
2553+
2554+ # Show what was updated
2555+ if console :
2556+ console .print ("\n [bold green]✓ Update complete![/bold green]" )
2557+ console .print ("\n [bold]Changes:[/bold]" )
2558+ if result .stdout :
2559+ for line in result .stdout .split ('\n ' )[:10 ]: # Show first 10 lines
2560+ if line .strip ():
2561+ console .print (f" [dim]{ line } [/dim]" )
2562+ else :
2563+ print ("\n ✓ Update complete!" )
2564+ print ("\n Changes:" )
2565+ if result .stdout :
2566+ for line in result .stdout .split ('\n ' )[:10 ]:
2567+ if line .strip ():
2568+ print (f" { line } " )
2569+
2570+ # Reinstall CLI if needed
2571+ if console :
2572+ console .print ("\n [bold cyan]Updating CLI...[/bold cyan]" )
2573+ else :
2574+ print ("\n Updating CLI..." )
2575+
2576+ cli_install = learn_dir / "CLI" / "install.sh"
2577+ if cli_install .exists ():
2578+ subprocess .run (["bash" , str (cli_install )], capture_output = True )
2579+
2580+ if console :
2581+ console .print ("\n [bold green]✓ All updates applied successfully![/bold green]" )
2582+ console .print ("\n [bold yellow]Note:[/bold yellow] You may need to restart the CLI for all changes to take effect." )
2583+ else :
2584+ print ("\n ✓ All updates applied successfully!" )
2585+ print ("\n Note: You may need to restart the CLI for all changes to take effect." )
2586+
2587+ except Exception as e :
2588+ if console :
2589+ console .print (f"\n [bold red]Update error:[/bold red] { str (e )} " )
2590+ else :
2591+ print (f"\n Update error: { str (e )} " )
2592+
2593+
23802594def reset_user_data (learn_dir : Path ):
23812595 """Reset all user data including progress and workspaces"""
23822596 if console :
@@ -2661,6 +2875,8 @@ def main():
26612875 parser .add_argument ("--run" , nargs = 3 , metavar = ("LANG" , "STAGE" , "LEVEL" ), help = "Compile and run workspace code" )
26622876 parser .add_argument ("--reset-data" , action = "store_true" , help = "Reset all user data (progress and workspaces)" )
26632877 parser .add_argument ("--uninstall" , action = "store_true" , help = "Uninstall LEARN CLI" )
2878+ parser .add_argument ("--update" , action = "store_true" , help = "Update LEARN to the latest version" )
2879+ parser .add_argument ("--check-updates" , action = "store_true" , help = "Check if updates are available" )
26642880
26652881 args = parser .parse_args ()
26662882
@@ -2681,6 +2897,41 @@ def main():
26812897 print ("Run 'learn init' to install missing components." )
26822898 return
26832899
2900+ # Check for updates
2901+ if args .check_updates :
2902+ update_info = check_for_updates (learn_dir )
2903+
2904+ if "error" in update_info :
2905+ print (f"Error checking for updates: { update_info ['error' ]} " )
2906+ return
2907+
2908+ if console :
2909+ if update_info ["has_update" ]:
2910+ console .print (f"[bold yellow]Update available![/bold yellow]" )
2911+ console .print (f" Current: [cyan]{ update_info ['current' ]} [/cyan]" )
2912+ console .print (f" Latest: [green]{ update_info ['latest' ]} [/green]" )
2913+ console .print (f" Commits behind: [yellow]{ update_info ['commits_behind' ]} [/yellow]" )
2914+ console .print (f"\n Run [cyan]learn --update[/cyan] to update" )
2915+ else :
2916+ console .print (f"[bold green]✓ You're on the latest version[/bold green]" )
2917+ console .print (f"[dim]Version: { update_info ['current' ]} [/dim]" )
2918+ else :
2919+ if update_info ["has_update" ]:
2920+ print (f"Update available!" )
2921+ print (f" Current: { update_info ['current' ]} " )
2922+ print (f" Latest: { update_info ['latest' ]} " )
2923+ print (f" Commits behind: { update_info ['commits_behind' ]} " )
2924+ print (f"\n Run 'learn --update' to update" )
2925+ else :
2926+ print (f"✓ You're on the latest version" )
2927+ print (f"Version: { update_info ['current' ]} " )
2928+ return
2929+
2930+ # Update
2931+ if args .update :
2932+ update_learn (learn_dir )
2933+ return
2934+
26842935 # Reset data
26852936 if args .reset_data :
26862937 reset_user_data (learn_dir )
0 commit comments