#!/usr/bin/env nu # Nushell Auto-Updater - Nushell version # Downloads latest nushell release from GitHub, extracts, and manages symlinks # Usage: nu update_nushell.nu [command] # Configuration let GITHUB_API_URL = "https://api.github.com/repos/nushell/nushell/releases/latest" let GITHUB_RELEASES_URL = "https://github.com/nushell/nushell/releases/download" let NUSHELL_SYMLINK = "nushell" # Function to show usage def show_usage [] { print $"(ansi blue)Nushell Auto-Updater(ansi reset)" print "" print "Usage: nu update_nushell.nu [command]" print "" print "Commands:" print $" (ansi green)check(ansi reset) Check for updates without installing" print $" (ansi green)update(ansi reset) Download and install latest version" print $" (ansi green)status(ansi reset) Show current installation status" print $" (ansi green)help, -h(ansi reset) Show this help message" print "" print "If no command is specified, 'update' is assumed." print "" } # Function to get latest version from GitHub API def get_latest_version [] { try { http get $GITHUB_API_URL | get tag_name } catch { null } } # Function to check if nushell directory is a git repository def is_git_repository [nushell_dir: string] { ($nushell_dir | path join ".git" "config" | path exists) } # Function to get git repository status with origin information def get_git_status [nushell_dir: string] { if not (is_git_repository $nushell_dir) { return null } try { cd $nushell_dir let current_commit = (^git rev-parse --short HEAD | str trim) let current_branch = (^git branch --show-current | str trim) let remote_status = (^git status --porcelain=v1 | lines | length) let origin_url = (^git remote get-url origin | str trim) cd .. { branch: $current_branch, commit: $current_commit, changes: $remote_status, origin: $origin_url } } catch { null } } # Function to get currently installed version def get_current_version [] { try { if ($NUSHELL_SYMLINK | path exists) and (($NUSHELL_SYMLINK | path type) == "symlink") { let target = $NUSHELL_SYMLINK | path expand | path basename # Check if target is a git repository if (is_git_repository $target) { "git-repo" } else { let version_match = ($target | parse "nushell-{version}") if ($version_match | length) > 0 { $version_match.0.version } else { null } } } else if ($NUSHELL_SYMLINK | path exists) and (($NUSHELL_SYMLINK | path type) == "dir") and (is_git_repository $NUSHELL_SYMLINK) { # Handle case where nushell is a directory (git repository) "git-repo" } else { null } } catch { null } } # Function to get current directory name def get_current_directory [] { try { if ($NUSHELL_SYMLINK | path exists) and (($NUSHELL_SYMLINK | path type) == "symlink") { $NUSHELL_SYMLINK | path expand | path basename } else if ($NUSHELL_SYMLINK | path exists) and (($NUSHELL_SYMLINK | path type) == "dir") { # Return the directory name itself if it's a directory $NUSHELL_SYMLINK } else { null } } catch { null } } # Function to check installation status def check_status [] { print $"(ansi blue)Nushell Installation Status(ansi reset)" print "" let current_version = get_current_version let current_dir = get_current_directory # Handle git repository case if $current_version == "git-repo" { print $"(ansi cyan)Git Repository Detected(ansi reset)" print $"Currently installed: (ansi cyan)Git repository(ansi reset)" let git_status = get_git_status $current_dir if ($git_status | is-not-empty) { print $"Repository: (ansi cyan)($current_dir)(ansi reset)" print $"Origin: (ansi cyan)($git_status.origin)(ansi reset)" print $"Branch: (ansi cyan)($git_status.branch)(ansi reset)" print $"Commit: (ansi cyan)($git_status.commit)(ansi reset)" if $git_status.changes > 0 { print $"Status: (ansi yellow)($git_status.changes) uncommitted changes(ansi reset)" } else { print $"Status: (ansi green)Clean working directory(ansi reset)" } } print "" print $"(ansi yellow)⚠️ Git Repository Management(ansi reset)" print "This nushell installation is a git repository." print $"Use (ansi cyan)git pull(ansi reset) to update instead of this script." print $"To switch to release versions, remove the current directory and use (ansi cyan)nu update_nushell.nu update(ansi reset)" return 3 } let latest_version = get_latest_version if ($latest_version | is-empty) { print $"(ansi red)Error: Could not fetch latest version from GitHub(ansi reset)" print "Please check your internet connection and try again." return 1 } print $"Latest available version: (ansi green)($latest_version)(ansi reset)" if ($current_version | is-empty) { print $"Currently installed: (ansi yellow)None(ansi reset)" print $"Status: (ansi yellow)Fresh installation needed(ansi reset)" return 2 } else { print $"Currently installed: (ansi cyan)($current_version)(ansi reset)" if $current_version == $latest_version { print $"Status: (ansi green)Up to date(ansi reset)" return 0 } else { print $"Status: (ansi yellow)Update available(ansi reset)" return 2 } } } # Function to download file def download_file [url: string, filename: string] { try { print $"(ansi blue)Downloading from: ($url)(ansi reset)" http get $url | save $filename true } catch { |e| print $"(ansi red)Error downloading file: ($e.msg)(ansi reset)" false } } # Function to download and extract nushell def download_and_extract [version: string] { let filename = $"nushell-($version)-source.tar.gz" let url = $"($GITHUB_RELEASES_URL)/($version)/($filename)" let target_dir = $"nushell-($version)" print $"(ansi blue)Downloading nushell ($version)...(ansi reset)" # Download the file if not (download_file $url $filename) { return false } if not ($filename | path exists) { print $"(ansi red)Error: Download failed - file not found(ansi reset)" return false } print $"(ansi blue)Extracting ($filename)...(ansi reset)" # Extract the tar.gz file try { ^tar -xzf $filename } catch { |e| print $"(ansi red)Error: Failed to extract ($filename): ($e.msg)(ansi reset)" try { rm $filename } catch { } return false } # Check if extraction created the expected directory if not ($target_dir | path exists) { print $"(ansi red)Error: Expected directory ($target_dir) not found after extraction(ansi reset)" try { rm $filename } catch { } return false } print $"(ansi green)Successfully downloaded and extracted nushell ($version)(ansi reset)" true } # Function to clean up old installations def cleanup_old_installation [old_dir: string, old_version: string] { if not ($old_dir | is-empty) and ($old_dir | path exists) { print $"(ansi blue)Removing old installation: ($old_dir)(ansi reset)" try { rm -rf $old_dir } catch { |e| print $"(ansi yellow)Warning: Could not remove old directory ($old_dir): ($e.msg)(ansi reset)" } } # Remove old tar.gz files let old_tarball = $"nushell-($old_version)-source.tar.gz" if ($old_tarball | path exists) { print $"(ansi blue)Removing old tarball: ($old_tarball)(ansi reset)" try { rm $old_tarball } catch { |e| print $"(ansi yellow)Warning: Could not remove old tarball ($old_tarball): ($e.msg)(ansi reset)" } } } # Function to create symlink def create_symlink [target_dir: string] { # Remove existing symlink if ($NUSHELL_SYMLINK | path exists) { if (($NUSHELL_SYMLINK | path type) == "symlink") { try { rm $NUSHELL_SYMLINK } catch { } } else { print $"(ansi red)Error: ($NUSHELL_SYMLINK) exists but is not a symlink(ansi reset)" return false } } # Create new symlink try { ^ln -s $target_dir $NUSHELL_SYMLINK print $"(ansi green)Created symlink: ($NUSHELL_SYMLINK) -> ($target_dir)(ansi reset)" true } catch { |e| print $"(ansi red)Error: Failed to create symlink: ($e.msg)(ansi reset)" false } } # Function to update git repository def update_git_repository [] { let current_dir = get_current_directory if ($current_dir | is-empty) or not (is_git_repository $current_dir) { print $"(ansi red)Error: Git repository not found or invalid(ansi reset)" return 1 } print $"(ansi blue)Updating Git Repository(ansi reset)" print $"Repository: (ansi cyan)($current_dir)(ansi reset)" # Show origin and branch info let git_status = get_git_status $current_dir if ($git_status | is-not-empty) { print $"Origin: (ansi cyan)($git_status.origin)(ansi reset)" print $"Branch: (ansi cyan)($git_status.branch)(ansi reset)" } print "" try { cd $current_dir # Check if there are uncommitted changes let changes = (^git status --porcelain=v1 | lines | length) if $changes > 0 { print $"(ansi yellow)Warning: ($changes) uncommitted changes detected(ansi reset)" print "Stashing changes before update..." let stash_message = $"Auto-stash before nushell update (date now | format date '%Y-%m-%d %H:%M:%S')" ^git stash push -m $stash_message } # Update repository print $"(ansi blue)Fetching latest changes...(ansi reset)" ^git fetch origin print $"(ansi blue)Pulling latest changes...(ansi reset)" ^git pull origin # Restore stashed changes if any if $changes > 0 { print $"(ansi blue)Restoring stashed changes...(ansi reset)" try { ^git stash pop } catch { print $"(ansi yellow)Warning: Failed to restore stashed changes(ansi reset)" print "Your changes are saved in the stash. Use 'git stash list' to see them." } } cd .. print "" print $"(ansi green)✓ Git repository updated successfully!(ansi reset)" let new_commit = (^git -C $current_dir rev-parse --short HEAD | str trim) print $"Current commit: (ansi cyan)($new_commit)(ansi reset)" 0 } catch { |e| print $"(ansi red)Error updating git repository: ($e.msg)(ansi reset)" try { cd .. } catch { } 1 } } # Function to perform update def perform_update [force_update: bool = false] { print $"(ansi blue)Nushell Auto-Updater(ansi reset)" print "" # Check status first let status_result = check_status match $status_result { 0 => { if not $force_update { print "" print $"(ansi green)Nushell is already up to date!(ansi reset)" return 0 } print "" print $"(ansi yellow)Forcing update even though current version is latest...(ansi reset)" } 1 => { return 1 } 2 => { print "" print $"(ansi yellow)Proceeding with installation/update...(ansi reset)" } 3 => { # Git repository detected print "" print $"(ansi blue)Git repository detected. Running git update...(ansi reset)" print "" return (update_git_repository) } } print "" # Get versions let latest_version = get_latest_version let current_version = get_current_version let current_dir = get_current_directory # Download and extract if not (download_and_extract $latest_version) { return 1 } let new_dir = $"nushell-($latest_version)" let new_tarball = $"nushell-($latest_version)-source.tar.gz" # Create symlink if not (create_symlink $new_dir) { print $"(ansi red)Installation failed at symlink creation(ansi reset)" return 1 } # Clean up old installation (only if different from new one) if (not ($current_version | is-empty)) and ($current_version != $latest_version) { cleanup_old_installation $current_dir $current_version } # Remove the new tarball (keep the extracted directory) if ($new_tarball | path exists) { try { rm $new_tarball } catch { } } print "" print $"(ansi green)✓ Nushell installation completed successfully!(ansi reset)" print $"Version: (ansi cyan)($latest_version)(ansi reset)" print $"Location: (ansi cyan)(pwd)/($new_dir)(ansi reset)" print $"Symlink: (ansi cyan)(pwd)/($NUSHELL_SYMLINK)(ansi reset)" 0 } # Main function def main [command?: string] { let cmd = ($command | default "update") match $cmd { "check" => { check_status } "update" => { perform_update } "force" => { perform_update true } "status" => { check_status } "help" | "-h" | "--help" => { show_usage } _ => { print $"(ansi red)Error: Unknown command '($cmd)'(ansi reset)" print "" show_usage } } } # Export functions for use export def "nushell-updater check" [] { check_status } export def "nushell-updater update" [] { perform_update } export def "nushell-updater force" [] { perform_update true } export def "nushell-updater status" [] { check_status } export def "nushell-updater help" [] { show_usage } # Main entry point function export def "nushell-updater" [command?: string] { main ($command | default "help") } # When run as script, don't execute main automatically # Use the exported functions instead