diff --git a/README.md b/README.md index 8e17eb8..f9d638f 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,14 @@ A command-line interface for interacting with JuliaHub, a platform for Julia com ## Installation -### Quick Install (Recommended) +### Quick Install + +#### Linux and macOS Install the latest release automatically: ```bash -curl -sSfL https://raw.githubusercontent.com/JuliaComputing/jh/main/install.sh | bash +curl -sSfL https://raw.githubusercontent.com/JuliaComputing/jh/main/install.sh | sh ``` Or download and run the script manually: @@ -35,9 +37,50 @@ chmod +x install.sh **Custom installation directory example:** ```bash -curl -sSfL https://raw.githubusercontent.com/JuliaComputing/jh/main/install.sh | bash -s -- --install-dir /usr/local/bin +curl -sSfL https://raw.githubusercontent.com/JuliaComputing/jh/main/install.sh | sh -s -- --install-dir /usr/local/bin +``` + +#### Windows + +**Option 1: PowerShell (Recommended)** + +```powershell +# Download and run the PowerShell installer +Invoke-WebRequest -Uri "https://raw.githubusercontent.com/JuliaComputing/jh/main/install.ps1" -OutFile "install.ps1"; .\install.ps1; Remove-Item install.ps1 +``` + +Or download and run manually: +```powershell +# Download the installer +Invoke-WebRequest -Uri "https://raw.githubusercontent.com/JuliaComputing/jh/main/install.ps1" -OutFile "install.ps1" + +# Run the installer +.\install.ps1 + +# Clean up +Remove-Item install.ps1 + +**Option 2: Command Prompt (CMD)** + +```cmd +curl -L "https://raw.githubusercontent.com/JuliaComputing/jh/main/install.bat" -o install.bat && install.bat && del install.bat +``` + +Or download and run manually: +```cmd +curl -L "https://raw.githubusercontent.com/JuliaComputing/jh/main/install.bat" -o install.bat +install.bat +del install.bat ``` +**Windows Installation Notes:** +- PowerShell script supports custom install directory: `.\install.ps1 -InstallDir "C:\tools\bin"` +- PowerShell script can automatically add to PATH: will prompt unless you use `-NoPrompt` +- For automated installs: `.\install.ps1 -NoPrompt` (won't add to PATH automatically) +- Default install location: `%USERPROFILE%\.local\bin` +- CMD script requires curl (available in Windows 10 1803+ and Windows 11) +- After installation, restart your terminal or run `refreshenv` to use `jh` command + ### Download Binary Manually Download the latest release from the [GitHub releases page](https://github.com/JuliaComputing/jh/releases). @@ -132,6 +175,11 @@ go build -o jh . - `jh user info` - Show detailed user information +### Update (`jh update`) + +- `jh update` - Check for updates and automatically install the latest version +- `jh update --force` - Force update even if current version is newer than latest release + ## Configuration Configuration is stored in `~/.juliahub` with 0600 permissions. The file contains: diff --git a/install.bat b/install.bat new file mode 100644 index 0000000..4b26e92 --- /dev/null +++ b/install.bat @@ -0,0 +1,124 @@ +@echo off +setlocal enabledelayedexpansion + +:: JuliaHub CLI (jh) installer script for Windows +:: This script downloads and installs the latest release of jh from GitHub + +:: Configuration +set REPO_OWNER=JuliaComputing +set REPO_NAME=jh +set BINARY_NAME=jh +if "%INSTALL_DIR%"=="" set INSTALL_DIR=%USERPROFILE%\.local\bin + +:: Create install directory if it doesn't exist +if not exist "%INSTALL_DIR%" mkdir "%INSTALL_DIR%" + +echo JuliaHub CLI (%BINARY_NAME%) Installer for Windows +echo ================================================ + +:: Check if curl is available (Windows 10 1803+ has curl built-in) +curl --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo ERROR: curl is required but not found. Please install curl or use PowerShell. + echo You can install curl from: https://curl.se/download.html + echo Or use the PowerShell install script instead. + exit /b 1 +) + +echo INFO: Fetching latest release information... + +:: Get latest version from GitHub API +for /f "tokens=*" %%i in ('curl -s "https://api.github.com/repos/%REPO_OWNER%/%REPO_NAME%/releases/latest" ^| findstr "tag_name" ^| for /f "tokens=2 delims=:," %%j in ('findstr "tag_name"') do @echo %%~j') do set VERSION=%%i +set VERSION=%VERSION:"=% +set VERSION=%VERSION: =% + +if "%VERSION%"=="" ( + echo ERROR: Failed to get latest version information + exit /b 1 +) + +echo INFO: Latest version: %VERSION% + +:: Detect architecture +set ARCH=amd64 +if "%PROCESSOR_ARCHITECTURE%"=="ARM64" set ARCH=arm64 + +:: Construct download URL and filenames +set BINARY_FILE=%BINARY_NAME%-windows-%ARCH%.exe +set DOWNLOAD_URL=https://github.com/%REPO_OWNER%/%REPO_NAME%/releases/download/%VERSION%/%BINARY_FILE% +set TEMP_FILE=%INSTALL_DIR%\%BINARY_FILE%.tmp +set FINAL_FILE=%INSTALL_DIR%\%BINARY_NAME%.exe + +echo INFO: Downloading %BINARY_NAME% %VERSION% for windows-%ARCH%... +echo INFO: Download URL: %DOWNLOAD_URL% + +:: Check if binary already exists +if exist "%FINAL_FILE%" ( + echo INFO: Checking current installation... + for /f "tokens=*" %%i in ('"%FINAL_FILE%" --version 2^>nul') do set CURRENT_VERSION=%%i + if not "%CURRENT_VERSION%"=="" ( + echo INFO: Current installation: !CURRENT_VERSION! + echo !CURRENT_VERSION! | findstr "%VERSION%" >nul + if !errorlevel! equ 0 ( + echo INFO: Latest version is already installed + exit /b 0 + ) + ) + echo WARNING: Existing installation found. It will be replaced. +) + +:: Download binary +curl -L -o "%TEMP_FILE%" "%DOWNLOAD_URL%" +if %errorlevel% neq 0 ( + echo ERROR: Failed to download binary from %DOWNLOAD_URL% + if exist "%TEMP_FILE%" del "%TEMP_FILE%" + exit /b 1 +) + +:: Verify download was successful +if not exist "%TEMP_FILE%" ( + echo ERROR: Downloaded file not found + exit /b 1 +) + +:: Move to final location +move "%TEMP_FILE%" "%FINAL_FILE%" >nul +if %errorlevel% neq 0 ( + echo ERROR: Failed to install binary to %FINAL_FILE% + exit /b 1 +) + +echo SUCCESS: Installed %BINARY_NAME% to %FINAL_FILE% + +:: Check if install directory is in PATH +echo %PATH% | findstr /C:"%INSTALL_DIR%" >nul +if %errorlevel% neq 0 ( + echo WARNING: %INSTALL_DIR% is not in your PATH. + echo To add it permanently, run: + echo setx PATH "%%PATH%%;%INSTALL_DIR%" + echo Or add it to your current session: + echo set PATH=%%PATH%%;%INSTALL_DIR% + echo. +) + +:: Verify installation +if exist "%FINAL_FILE%" ( + echo INFO: Verifying installation... + for /f "tokens=*" %%i in ('"%FINAL_FILE%" --version 2^>nul') do set VERSION_OUTPUT=%%i + if not "!VERSION_OUTPUT!"=="" ( + echo SUCCESS: Installation verified: !VERSION_OUTPUT! + echo INFO: Run '%BINARY_NAME% --help' to get started + ) else ( + echo WARNING: Binary installed but version check failed + ) +) else ( + echo ERROR: Installation failed: binary not found + exit /b 1 +) + +echo. +echo SUCCESS: Installation complete! +echo INFO: You can now use '%BINARY_NAME%' to interact with JuliaHub +echo INFO: Start with: %BINARY_NAME% auth login + +endlocal diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..108e7b4 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,215 @@ +#!/usr/bin/env pwsh + +# JuliaHub CLI (jh) installer script for Windows PowerShell +# This script downloads and installs the latest release of jh from GitHub + +param( + [string]$InstallDir = "$env:USERPROFILE\.local\bin", + [switch]$Help, + [switch]$NoPrompt +) + +# Configuration +$RepoOwner = "JuliaComputing" +$RepoName = "jh" +$BinaryName = "jh" + +# Colors for output +$Colors = @{ + Red = "Red" + Green = "Green" + Yellow = "Yellow" + Blue = "Cyan" +} + +# Logging functions +function Write-Info { + param([string]$Message) + Write-Host "INFO: $Message" -ForegroundColor $Colors.Blue +} + +function Write-Success { + param([string]$Message) + Write-Host "SUCCESS: $Message" -ForegroundColor $Colors.Green +} + +function Write-Warning { + param([string]$Message) + Write-Host "WARNING: $Message" -ForegroundColor $Colors.Yellow +} + +function Write-Error { + param([string]$Message) + Write-Host "ERROR: $Message" -ForegroundColor $Colors.Red + exit 1 +} + +# Show help +if ($Help) { + Write-Host "JuliaHub CLI Installer for PowerShell" + Write-Host "" + Write-Host "Usage: .\install.ps1 [options]" + Write-Host "" + Write-Host "Parameters:" + Write-Host " -InstallDir DIR Install directory (default: `$env:USERPROFILE\.local\bin)" + Write-Host " -NoPrompt Don't prompt to add to PATH (for automated installs)" + Write-Host " -Help Show this help message" + Write-Host "" + Write-Host "Environment variables:" + Write-Host " INSTALL_DIR Same as -InstallDir" + exit 0 +} + +# Override with environment variable if set +if ($env:INSTALL_DIR) { + $InstallDir = $env:INSTALL_DIR +} + +Write-Host "JuliaHub CLI ($BinaryName) Installer for PowerShell" +Write-Host "==================================================" + +# Detect architecture +$Arch = "amd64" +if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { + $Arch = "arm64" +} + +Write-Info "Detected platform: windows-$Arch" + +# Get latest version from GitHub API +Write-Info "Fetching latest release information..." +try { + $ApiUrl = "https://api.github.com/repos/$RepoOwner/$RepoName/releases/latest" + $Response = Invoke-RestMethod -Uri $ApiUrl -Method Get + $Version = $Response.tag_name + + if (-not $Version) { + Write-Error "Failed to get latest version information" + } + + Write-Info "Latest version: $Version" +} +catch { + Write-Error "Failed to fetch release information: $($_.Exception.Message)" +} + +# Construct download URL and filenames +$BinaryFile = "$BinaryName-windows-$Arch.exe" +$DownloadUrl = "https://github.com/$RepoOwner/$RepoName/releases/download/$Version/$BinaryFile" +$TempFile = Join-Path $InstallDir "$BinaryFile.tmp" +$FinalFile = Join-Path $InstallDir "$BinaryName.exe" + +# Create install directory if it doesn't exist +if (-not (Test-Path $InstallDir)) { + New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null +} + +# Check if binary already exists +if (Test-Path $FinalFile) { + Write-Info "Checking current installation..." + try { + $CurrentVersion = & $FinalFile --version 2>$null + if ($CurrentVersion -and $CurrentVersion -match $Version) { + Write-Info "Current installation: $CurrentVersion" + Write-Info "Latest version is already installed" + exit 0 + } + if ($CurrentVersion) { + Write-Info "Current installation: $CurrentVersion" + } + } + catch { + # Ignore version check errors + } + Write-Warning "Existing installation found. It will be replaced." +} + +Write-Info "Downloading $BinaryName $Version for windows-$Arch..." +Write-Info "Download URL: $DownloadUrl" + +# Download binary +try { + Invoke-WebRequest -Uri $DownloadUrl -OutFile $TempFile -UseBasicParsing + + # Verify download was successful + if (-not (Test-Path $TempFile) -or (Get-Item $TempFile).Length -eq 0) { + Remove-Item $TempFile -ErrorAction SilentlyContinue + Write-Error "Failed to download binary from $DownloadUrl" + } + + # Move to final location + Move-Item $TempFile $FinalFile -Force + + Write-Success "Installed $BinaryName to $FinalFile" +} +catch { + Remove-Item $TempFile -ErrorAction SilentlyContinue + Write-Error "Failed to download or install binary: $($_.Exception.Message)" +} + +# Check if install directory is in PATH +$PathDirs = $env:PATH -split ";" +if ($InstallDir -notin $PathDirs) { + Write-Warning "$InstallDir is not in your PATH." + + # Ask user if they want to add it automatically (unless -NoPrompt is used) + $AddToPath = "n" + if (-not $NoPrompt) { + $AddToPath = Read-Host "Add $InstallDir to your PATH permanently? (y/N)" + } + + if ($AddToPath -match '^[Yy]') { + try { + # Add to current session + $env:PATH += ";$InstallDir" + + # Add permanently for current user + $UserPath = [Environment]::GetEnvironmentVariable('PATH', 'User') + if ($UserPath -and (-not $UserPath.Contains($InstallDir))) { + $NewUserPath = "$UserPath;$InstallDir" + [Environment]::SetEnvironmentVariable('PATH', $NewUserPath, 'User') + Write-Success "Added $InstallDir to your PATH permanently" + } elseif (-not $UserPath) { + [Environment]::SetEnvironmentVariable('PATH', $InstallDir, 'User') + Write-Success "Added $InstallDir to your PATH permanently" + } else { + Write-Info "$InstallDir already in permanent PATH" + } + } + catch { + Write-Warning "Failed to update PATH automatically: $($_.Exception.Message)" + Write-Host "To add it manually:" + Write-Host " For current session: `$env:PATH += ';$InstallDir'" -ForegroundColor Yellow + Write-Host " Permanently: [Environment]::SetEnvironmentVariable('PATH', `$env:PATH + ';$InstallDir', 'User')" -ForegroundColor Yellow + } + } else { + Write-Host "To add it manually:" + Write-Host " For current session: `$env:PATH += ';$InstallDir'" -ForegroundColor Yellow + Write-Host " Permanently: [Environment]::SetEnvironmentVariable('PATH', `$env:PATH + ';$InstallDir', 'User')" -ForegroundColor Yellow + } + Write-Host "" +} + +# Verify installation +if (Test-Path $FinalFile) { + Write-Info "Verifying installation..." + try { + $VersionOutput = & $FinalFile --version 2>$null + if ($VersionOutput) { + Write-Success "Installation verified: $VersionOutput" + Write-Info "Run '$BinaryName --help' to get started" + } else { + Write-Warning "Binary installed but version check failed" + } + } + catch { + Write-Warning "Binary installed but version check failed" + } +} else { + Write-Error "Installation failed: binary not found" +} + +Write-Host "" +Write-Success "Installation complete!" +Write-Info "You can now use '$BinaryName' to interact with JuliaHub" +Write-Info "Start with: $BinaryName auth login" diff --git a/install.sh b/install.sh index 2873233..afd775e 100755 --- a/install.sh +++ b/install.sh @@ -7,7 +7,7 @@ set -e # Configuration REPO_OWNER="JuliaComputing" -REPO_NAME="gojuliahub" +REPO_NAME="jh" BINARY_NAME="jh" INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}" @@ -206,7 +206,7 @@ main() { } # Parse command line arguments -while [[ $# -gt 0 ]]; do +while [ $# -gt 0 ]; do case $1 in --install-dir) INSTALL_DIR="$2" diff --git a/main.go b/main.go index eaf58e5..48ddecb 100644 --- a/main.go +++ b/main.go @@ -912,6 +912,26 @@ without needing to use the 'jh' wrapper commands.`, }, } +var updateCmd = &cobra.Command{ + Use: "update", + Short: "Update jh to the latest version", + Long: `Check for updates and automatically download and install the latest version of jh. + +This command fetches the latest release information from GitHub and compares +it with the current version. If an update is available, it downloads and runs +the appropriate install script for your platform. + +The update process will replace the current installation with the latest version.`, + Example: " jh update\n jh update --force", + Run: func(cmd *cobra.Command, args []string) { + force, _ := cmd.Flags().GetBool("force") + if err := runUpdate(force); err != nil { + fmt.Printf("Update failed: %v\n", err) + os.Exit(1) + } + }, +} + func init() { authLoginCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") jobListCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") @@ -928,6 +948,7 @@ func init() { pushCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") fetchCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") pullCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") + updateCmd.Flags().Bool("force", false, "Force update even if current version is newer than latest release") authCmd.AddCommand(authLoginCmd, authRefreshCmd, authStatusCmd, authEnvCmd) jobCmd.AddCommand(jobListCmd, jobStartCmd) @@ -937,7 +958,7 @@ func init() { juliaCmd.AddCommand(juliaInstallCmd) gitCredentialCmd.AddCommand(gitCredentialHelperCmd, gitCredentialGetCmd, gitCredentialStoreCmd, gitCredentialEraseCmd, gitCredentialSetupCmd) - rootCmd.AddCommand(authCmd, jobCmd, datasetCmd, projectCmd, userCmd, juliaCmd, cloneCmd, pushCmd, fetchCmd, pullCmd, runCmd, gitCredentialCmd) + rootCmd.AddCommand(authCmd, jobCmd, datasetCmd, projectCmd, userCmd, juliaCmd, cloneCmd, pushCmd, fetchCmd, pullCmd, runCmd, gitCredentialCmd, updateCmd) } func main() { diff --git a/update.go b/update.go new file mode 100644 index 0000000..aee372a --- /dev/null +++ b/update.go @@ -0,0 +1,143 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "os/exec" + "runtime" + "strings" +) + +// GitHubRelease represents a GitHub release response +type GitHubRelease struct { + TagName string `json:"tag_name"` + Name string `json:"name"` + Body string `json:"body"` +} + +// getLatestRelease fetches the latest release info from GitHub +func getLatestRelease() (*GitHubRelease, error) { + url := "https://api.github.com/repos/JuliaComputing/jh/releases/latest" + + resp, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("failed to fetch release info: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GitHub API returned status %d", resp.StatusCode) + } + + var release GitHubRelease + if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { + return nil, fmt.Errorf("failed to parse release info: %w", err) + } + + return &release, nil +} + +// compareVersions compares two version strings (e.g., "v1.2.3") +// Returns: -1 if current < latest, 0 if equal, 1 if current > latest +func compareVersions(current, latest string) int { + // Remove 'v' prefix if present + current = strings.TrimPrefix(current, "v") + latest = strings.TrimPrefix(latest, "v") + + // Handle "dev" version + if current == "dev" { + return -1 // Always consider dev as older + } + + // Simple string comparison for now (works for semantic versions) + if current == latest { + return 0 + } else if current < latest { + return -1 + } + return 1 +} + +// getInstallScript returns the appropriate install script URL and command for the current platform +func getInstallScript() (string, []string, error) { + switch runtime.GOOS { + case "windows": + // Prefer PowerShell on Windows + powershellCmd, err := exec.LookPath("powershell") + if err == nil { + return "https://raw.githubusercontent.com/JuliaComputing/jh/main/install.ps1", + []string{powershellCmd, "-ExecutionPolicy", "Bypass", "-Command", + "Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/JuliaComputing/jh/main/install.ps1' -OutFile 'install.ps1'; ./install.ps1 -NoPrompt; Remove-Item install.ps1"}, nil + } + // Fallback to cmd + return "https://raw.githubusercontent.com/JuliaComputing/jh/main/install.bat", + []string{"cmd", "/c", "curl -L https://raw.githubusercontent.com/JuliaComputing/jh/main/install.bat -o install.bat && install.bat && del install.bat"}, nil + case "darwin", "linux": + // Prefer bash if available, fallback to sh + shell := "bash" + if _, err := exec.LookPath("bash"); err != nil { + shell = "sh" + } + return "https://raw.githubusercontent.com/JuliaComputing/jh/main/install.sh", + []string{shell, "-c", fmt.Sprintf("curl -sSfL https://raw.githubusercontent.com/JuliaComputing/jh/main/install.sh -o /tmp/jh_install.sh && %s /tmp/jh_install.sh && rm -f /tmp/jh_install.sh", shell)}, nil + default: + return "", nil, fmt.Errorf("unsupported platform: %s", runtime.GOOS) + } +} + +// runUpdate performs the actual update by executing the install script +func runUpdate(force bool) error { + // Check current version vs latest + fmt.Printf("Current version: %s\n", version) + + latest, err := getLatestRelease() + if err != nil { + return fmt.Errorf("failed to check for updates: %w", err) + } + + fmt.Printf("Latest version: %s\n", latest.TagName) + + // Compare versions + comparison := compareVersions(version, latest.TagName) + + if comparison == 0 && !force { + fmt.Println("You are already running the latest version!") + return nil + } else if comparison > 0 && !force { + fmt.Printf("Your version (%s) is newer than the latest release (%s)\n", version, latest.TagName) + fmt.Println("Use --force to downgrade to the latest release") + return nil + } + + if comparison < 0 { + fmt.Printf("Update available: %s -> %s\n", version, latest.TagName) + } else if force { + fmt.Printf("Force updating: %s -> %s\n", version, latest.TagName) + } + + // Get install script for current platform + scriptURL, command, err := getInstallScript() + if err != nil { + return fmt.Errorf("failed to determine install script: %w", err) + } + + fmt.Printf("Downloading and running install script from: %s\n", scriptURL) + fmt.Println("This will replace the current installation...") + + // Execute the install command + cmd := exec.Command(command[0], command[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + + if err := cmd.Run(); err != nil { + return fmt.Errorf("update failed: %w", err) + } + + fmt.Println("\nUpdate completed successfully!") + fmt.Println("You may need to restart your terminal for the changes to take effect.") + + return nil +}