Private/DevVm.Helpers.ps1
|
<#
Private helper functions for DevVm #> # Initialize configuration paths function Initialize-DevVmConfigPath { $script:DevVmConfig.ConfigPaths = @{ session = 'Memory' project = Resolve-DevVmProjectConfigPath -StartPath (Get-Location).Path global = (Get-ConfigValue 'globalDevVmPath') } } function Resolve-DevVmProjectConfigPath { param([string]$StartPath = (Get-Location).Path) if ([string]::IsNullOrWhiteSpace($StartPath)) { $StartPath = (Get-Location).Path } $startItem = Get-Item -LiteralPath $StartPath -ErrorAction SilentlyContinue if ($startItem) { if (-not $startItem.PSIsContainer) { $StartPath = $startItem.Directory.FullName } else { $StartPath = $startItem.FullName } } $projectPath = Find-NearestDevVmFile -StartPath $StartPath -FileName '.devvm' if ($projectPath) { return $projectPath } return Join-Path -Path $StartPath -ChildPath '.devvm' } # Get runtime info from definitions function Get-DevVmRuntimeInfo { param([string]$Runtime) if (-not $script:DevVmConfig.RuntimeDefinitions) { return $null } if ($script:DevVmConfig.RuntimeDefinitions.runtimes.PSObject.Properties.Name -contains $Runtime) { return $script:DevVmConfig.RuntimeDefinitions.runtimes.$Runtime } return $null } # Test if runtime exists in configuration function Test-DevVmRuntime { param([string]$Runtime) return $null -ne (Get-DevVmRuntimeInfo -Runtime $Runtime) } # Get currently activated version from PATH function Get-DevVmActivatedVersion { param( [string]$Runtime, [string]$BasePath, [string]$RuntimeCommand ) try { $cmdObj = Get-Command $RuntimeCommand -ErrorAction SilentlyContinue if ($cmdObj) { $cmdFullPath = [System.IO.Path]::GetFullPath($cmdObj.Source).ToLower() # Get all installed versions $versions = @(Get-ChildItem -Path $BasePath -Directory -ErrorAction SilentlyContinue) foreach ($v in $versions) { $versionPath = [System.IO.Path]::GetFullPath("$BasePath\$($v.Name)").ToLower() if ($cmdFullPath.StartsWith($versionPath)) { return $v.Name } } } } catch { Write-Verbose "Failed to resolve activated version for ${Runtime}: $_" } return $null } # Convert versions section to a hashtable (supports PSCustomObject or hashtable) function Convert-DevVmVersionsToHashtable { param([object]$Versions) $result = @{} if ($null -eq $Versions) { return $result } if ($Versions -is [hashtable]) { foreach ($key in $Versions.Keys) { $result[$key] = $Versions[$key] } return $result } if ($Versions -is [pscustomobject]) { foreach ($prop in $Versions.PSObject.Properties) { $result[$prop.Name] = $prop.Value } } return $result } # Get merged configuration from all levels function Get-DevVmMergedConfig { param([string]$StartPath = (Get-Location).Path) if ([string]::IsNullOrWhiteSpace($StartPath)) { $StartPath = (Get-Location).Path } Write-Verbose "Resolving project config from: $StartPath" $merged = @{} # Load global config $globalPath = $script:DevVmConfig.ConfigPaths.global if ($globalPath -and (Test-Path $globalPath)) { try { $globalRaw = Get-Content -Path $globalPath -Raw $global = ConvertFrom-DevVmJsonSafe -Raw $globalRaw if ($global) { $globalVersions = Convert-DevVmVersionsToHashtable -Versions $global.versions foreach ($key in $globalVersions.Keys) { $merged[$key] = $globalVersions[$key] } } } catch { Write-Verbose "Failed to load global config: $_" } } # Load nearest project config (overrides global) $projectPath = Resolve-DevVmProjectConfigPath -StartPath $StartPath Write-Verbose "Resolved project config path: $projectPath" if ($projectPath -and (Test-Path $projectPath)) { try { $projectRaw = Get-Content -Path $projectPath -Raw $project = ConvertFrom-DevVmJsonSafe -Raw $projectRaw if ($project) { $projectVersions = Convert-DevVmVersionsToHashtable -Versions $project.versions foreach ($key in $projectVersions.Keys) { $merged[$key] = $projectVersions[$key] } } } catch { Write-Verbose "Failed to load project config: $_" } } else { Write-Verbose "No project config found at resolved path." } # Load session overrides (highest priority) $sessionSelections = $script:DevVmConfig.SessionSelections if ($sessionSelections -and $sessionSelections.Count -gt 0) { foreach ($key in $sessionSelections.Keys) { $merged[$key] = $sessionSelections[$key] } } return $merged } # Find nearest .devvm file function Find-NearestDevVmFile { param( [string]$StartPath, [string]$FileName = '.devvm' ) if ([string]::IsNullOrWhiteSpace($StartPath)) { $StartPath = (Get-Location).Path } $startItem = Get-Item -LiteralPath $StartPath -ErrorAction SilentlyContinue if (-not $startItem) { return $null } $dirPath = if ($startItem.PSIsContainer) { $startItem.FullName } else { $startItem.Directory.FullName } while (-not [string]::IsNullOrWhiteSpace($dirPath)) { $candidate = Join-Path -Path $dirPath -ChildPath $FileName Write-Verbose "Checking for project config: $candidate" if (Test-Path -LiteralPath $candidate) { return $candidate } $parentPath = Split-Path -Parent $dirPath if ([string]::IsNullOrWhiteSpace($parentPath) -or $parentPath -eq $dirPath) { break } $dirPath = $parentPath } return $null } # Save version selection to config file function Save-DevVmVersionSelection { param( [string]$Runtime, [string]$Version, [ValidateSet('project', 'global')] [string]$Target = 'project' ) $configPath = $script:DevVmConfig.ConfigPaths[$Target] if ($Target -eq 'project') { $configPath = Find-NearestDevVmFile -StartPath (Get-Location).Path if (-not $configPath) { $configPath = Join-Path -Path (Get-Location).Path -ChildPath '.devvm' } } # Ensure directory exists $dir = Split-Path -Parent $configPath if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force -ErrorAction SilentlyContinue | Out-Null } # Load existing or create new $data = @{ versions = @{} runtimes = @{} } if (Test-Path $configPath) { try { $rawContent = Get-Content -Path $configPath -Raw if ($rawContent -and $rawContent.Trim()) { $parsed = $rawContent | ConvertFrom-Json # Convert PSCustomObject to hashtable structure if ($parsed.versions) { $data.versions = @{} foreach ($property in $parsed.versions.PSObject.Properties) { $data.versions[$property.Name] = $property.Value } } if ($parsed.runtimes) { $data.runtimes = @{} foreach ($property in $parsed.runtimes.PSObject.Properties) { $data.runtimes[$property.Name] = $property.Value } } } } catch { Write-Verbose "Failed to load existing config, creating new: $_" } } # Update the version inside 'versions' section $data.versions[$Runtime] = $Version # Save $json = $data | ConvertTo-Json -Depth 5 $json | Set-Content -Path $configPath -Encoding UTF8 -ErrorAction Stop Write-Verbose "Saved $Runtime=$Version to $Target config at $configPath" } # Update PATH for current session function Update-DevVmSessionPath { [CmdletBinding(SupportsShouldProcess = $true)] param( [string]$Runtime, [string]$Version, [string]$BasePath, [string]$BinPath ) if (-not $PSCmdlet.ShouldProcess('PATH', "Update $Runtime to $Version")) { Write-Verbose "Skipped PATH update for $Runtime $Version" return } # Split current PATH into array and remove empty entries $pathArray = @($env:Path -split ';' | Where-Object { $_ -and $_.Trim() }) # Remove all existing paths that belong to this runtime's base path # Convert to lowercase for case-insensitive comparison $baseLower = $BasePath.ToLower() $pathArray = @($pathArray | Where-Object { $_.ToLower() -notlike "$baseLower*" }) # Remove duplicates to clean up accumulated PATH entries $pathArray = @($pathArray | Select-Object -Unique) # Construct new path for this runtime $versionPath = Join-Path -Path $BasePath -ChildPath $Version $newPath = if ($BinPath -and (Test-Path "$versionPath\bin")) { "$versionPath\bin" } else { $versionPath } # Only add if the path exists and isn't already in the array if ((Test-Path $newPath) -and $newPath.ToLower() -notin $pathArray.ToLower()) { $pathArray = @($newPath) + $pathArray } # Rebuild PATH $env:Path = $pathArray -join ';' Write-Verbose "Updated PATH for $Runtime $Version" } # Helper to get config value function Get-ConfigValue { param([string]$Key) if ($Key -ne 'globalDevVmPath') { return $null } $globalPath = $env:DEVVM_GLOBAL_CONFIG if (-not $globalPath) { $globalPath = Join-Path $env:USERPROFILE 'AppData\Local\DevVm\.devvm' } $globalDir = Split-Path -Parent $globalPath if (-not (Test-Path $globalDir)) { New-Item -ItemType Directory -Path $globalDir -Force | Out-Null } if (-not (Test-Path $globalPath)) { $defaults = @{ versions = @{ node = '' java = '' maven = '' lein = '' } runtimes = @{} } $defaults | ConvertTo-Json -Depth 5 | Set-Content -Path $globalPath -Encoding UTF8 } return $globalPath } # Get cache directory for DevVm function Get-DevVmCacheDirectory { $cacheDir = Join-Path -Path $env:LOCALAPPDATA -ChildPath 'DevVm\Cache' if (-not (Test-Path $cacheDir)) { New-Item -ItemType Directory -Path $cacheDir -Force -ErrorAction SilentlyContinue | Out-Null } return $cacheDir } # Get install path for a runtime (configurable with precedence) function Get-DevVmInstallPath { param( [string]$Runtime ) # Precedence: env var > .devvm > global config > defaults $basePath = $null # 1. Check environment variable $envVar = "DEVVM_${Runtime.ToUpper()}_PATH" if (Test-Path env:$envVar) { $basePath = (Get-Item env:$envVar).Value Write-Verbose "Using basePath from env var $envVar : $basePath" return $basePath } # 2. Check project .devvm file $projectDevVm = Join-Path -Path (Get-Location) -ChildPath '.devvm' if (Test-Path $projectDevVm) { try { $configRaw = Get-Content -Path $projectDevVm -Raw $config = ConvertFrom-DevVmJsonSafe -Raw $configRaw if ($config -and $config.runtimes -and $config.runtimes.$Runtime -and $config.runtimes.$Runtime.basePath) { Write-Verbose "Using basePath from project .devvm: $($config.runtimes.$Runtime.basePath)" return $config.runtimes.$Runtime.basePath } } catch { Write-Verbose "Could not read project .devvm: $_" } } # 3. Check global config (~/.devvm) $globalDevVm = $script:DevVmConfig.ConfigPaths.global if ($globalDevVm -and (Test-Path $globalDevVm)) { try { $configRaw = Get-Content -Path $globalDevVm -Raw $config = ConvertFrom-DevVmJsonSafe -Raw $configRaw if ($config -and $config.runtimes -and $config.runtimes.$Runtime -and $config.runtimes.$Runtime.basePath) { Write-Verbose "Using basePath from global .devvm: $($config.runtimes.$Runtime.basePath)" return $config.runtimes.$Runtime.basePath } } catch { Write-Verbose "Could not read global .devvm: $_" } } # 4. Use defaults from runtime-definitions runtimes section if ($script:DevVmConfig.RuntimeDefinitions -and $script:DevVmConfig.RuntimeDefinitions.runtimes -and $script:DevVmConfig.RuntimeDefinitions.runtimes.$Runtime) { $runtimeDef = $script:DevVmConfig.RuntimeDefinitions.runtimes.$Runtime if ($runtimeDef.basePath) { Write-Verbose "Using basePath from runtime-definitions: $($runtimeDef.basePath)" return $runtimeDef.basePath } } # 5. Fallback to hardcoded default $defaultBase = "C:\$Runtime" Write-Verbose "Using fallback basePath: $defaultBase" return $defaultBase } # Get temporary download path for a runtime function Get-DevVmDownloadPath { param( [string]$Runtime ) # Always use $env:TEMP/DevVm/<runtime>/<version> $downloadDir = Join-Path -Path $env:TEMP -ChildPath "DevVm\$Runtime" if (-not (Test-Path $downloadDir)) { New-Item -ItemType Directory -Path $downloadDir -Force -ErrorAction SilentlyContinue | Out-Null } return $downloadDir } # Get or download cached content function Get-DevVmCachedContent { param( [string]$Uri, [int]$CacheDays = 30, [switch]$Update, [string]$Runtime = '' ) $cacheDir = Get-DevVmCacheDirectory # Create SHA256 hash of URI for cache filename (compatible with PS 5.0) $uriBytes = [System.Text.Encoding]::UTF8.GetBytes($Uri) $sha256 = [System.Security.Cryptography.SHA256]::Create() $hashBytes = $sha256.ComputeHash($uriBytes) $cacheFileName = ($hashBytes | ForEach-Object { $_.ToString('x2') }) -join '' $cachePath = Join-Path -Path $cacheDir -ChildPath "$cacheFileName.cache" $metadataPath = Join-Path -Path $cacheDir -ChildPath "$cacheFileName.metadata" # Check if valid cache exists if (-not $Update -and (Test-Path $cachePath)) { $cacheAge = (Get-Date) - (Get-Item $cachePath).LastWriteTime if ($cacheAge.TotalDays -lt $CacheDays) { Write-Verbose "Cache hit: Using cached content for: $Uri (age: $([math]::Round($cacheAge.TotalHours, 1)) hours)" Write-Output "[CACHE]" return Get-Content -Path $cachePath -Raw -ErrorAction SilentlyContinue } } # Download content Write-Verbose "Cache miss or update requested: Downloading: $Uri" if ($Update) { Write-Output "[UPDATED]" } else { Write-Output "[DOWNLOADED]" } try { $response = Invoke-WebRequest -Uri $Uri -UseBasicParsing -ErrorAction Stop $content = $response.Content # Save to cache $content | Set-Content -Path $cachePath -Encoding UTF8 -ErrorAction SilentlyContinue # Save metadata with runtime and expiration info $metadata = @{ Runtime = $Runtime Uri = $Uri DownloadDate = (Get-Date) ExpiryDate = (Get-Date).AddDays($CacheDays) } $metadata | ConvertTo-Json | Set-Content -Path $metadataPath -Encoding UTF8 -ErrorAction SilentlyContinue return $content } catch { # If download fails but cache exists, use expired cache if (Test-Path $cachePath) { Write-Verbose "Download failed, using expired cache: $_" Write-Output "[CACHE EXPIRED]" return Get-Content -Path $cachePath -Raw -ErrorAction SilentlyContinue } throw $_ } } # Download and install a runtime version function Invoke-DevVmDownloadAndInstall { param( [string]$Runtime, [string]$Version, [PSObject]$RuntimeInfo, [string]$InstallPath ) # If InstallPath not provided, resolve with precedence if ([string]::IsNullOrWhiteSpace($InstallPath)) { $InstallPath = Get-DevVmInstallPath -Runtime $Runtime } # Special handling for Leiningen (download JAR + create wrapper) if ($Runtime -eq 'lein') { try { New-Item -ItemType Directory -Path $InstallPath -Force -ErrorAction SilentlyContinue | Out-Null # Download the JAR file $downloadUrl = $RuntimeInfo.downloadUrl -replace '{version}', $Version $jarPath = Join-Path -Path $InstallPath -ChildPath "leiningen-$Version-standalone.jar" Write-Output "Downloading Leiningen JAR from: $downloadUrl" Invoke-WebRequest -Uri $downloadUrl -OutFile $jarPath -ErrorAction Stop # Create wrapper batch script based on original lein.bat but with local JAR $wrapperPath = Join-Path -Path $InstallPath -ChildPath "lein.bat" $wrapperContent = @" @echo off setLocal EnableExtensions EnableDelayedExpansion set LEIN_VERSION=$Version set LEIN_JAR=%~dp0leiningen-$Version-standalone.jar if "x!JAVA_CMD!" == "x" set JAVA_CMD=java if "x!LEIN_JAVA_CMD!" == "x" set LEIN_JAVA_CMD=!JAVA_CMD! if "x%JVM_OPTS%" == "x" set JVM_OPTS=%JAVA_OPTS% if not exist "!LEIN_JAR!" ( echo. echo Error: !LEIN_JAR! not found. echo. exit /b 1 ) set "TRAMPOLINE_FILE=%TEMP%\lein-trampoline-!RANDOM!.bat" del "!TRAMPOLINE_FILE!" >nul 2>&1 set ERRORLEVEL= set RC=0 "!LEIN_JAVA_CMD!" -client !LEIN_JVM_OPTS! ^ -Dfile.encoding=UTF-8 ^ -Dleiningen.original.pwd="%CD%" ^ -cp "!LEIN_JAR!" clojure.main -m leiningen.core.main %* SET RC=!ERRORLEVEL! if not !RC! == 0 goto EXITRC if not exist "!TRAMPOLINE_FILE!" goto EOF call "!TRAMPOLINE_FILE!" del "!TRAMPOLINE_FILE!" >nul 2>&1 goto EOF :EXITRC exit /B !RC! :EOF "@ Set-Content -Path $wrapperPath -Value $wrapperContent -Encoding ASCII Write-Output "$Runtime version $Version installed successfully at $InstallPath" Write-Output " JAR: $jarPath" Write-Output " Wrapper: $wrapperPath" Write-Output "" Write-Output "Requirements: Java (OpenJDK) must be installed." Write-Output " Use: devvm use java <version>" } catch { Write-Error "Failed to install $Runtime $Version : $_" Remove-Item -Path $InstallPath -Recurse -Force -ErrorAction SilentlyContinue } return } # Standard handling for archive-based runtimes $tempZip = Join-Path -Path $env:TEMP -ChildPath "$Runtime-$Version-install.zip" try { # Download $downloadUrl = $RuntimeInfo.downloadUrl -replace '{version}', $Version Write-Output "Downloading from: $downloadUrl" Invoke-WebRequest -Uri $downloadUrl -OutFile $tempZip -ErrorAction Stop # Extract Write-Output "Extracting..." $tempExtract = Join-Path -Path $env:TEMP -ChildPath "$Runtime-$Version-extract" Expand-Archive -Path $tempZip -DestinationPath $tempExtract -Force -ErrorAction Stop # Move to final location New-Item -ItemType Directory -Path $InstallPath -Force -ErrorAction SilentlyContinue | Out-Null # Handle extraction pattern if ($RuntimeInfo.extractPattern) { $extractedName = $RuntimeInfo.extractPattern -replace '{version}', $Version $extractedPath = Join-Path -Path $tempExtract -ChildPath $extractedName if (Test-Path $extractedPath) { Copy-Item -Path "$extractedPath\*" -Destination $InstallPath -Recurse -Force Remove-Item -Path $extractedPath -Recurse -Force -ErrorAction SilentlyContinue } } else { Copy-Item -Path "$tempExtract\*" -Destination $InstallPath -Recurse -Force } Write-Output "$Runtime version $Version installed successfully at $InstallPath" } catch { Write-Error "Failed to install $Runtime $Version : $_" Remove-Item -Path $InstallPath -Recurse -Force -ErrorAction SilentlyContinue } finally { # Cleanup Remove-Item -Path $tempZip -ErrorAction SilentlyContinue Remove-Item -Path $tempExtract -Recurse -ErrorAction SilentlyContinue } } # Fetch available versions from remote source function Get-DevVmAvailableVersion { param( [string]$Runtime, [PSObject]$RuntimeInfo, [bool]$ShowAll = $false, [switch]$Update ) try { if ($Runtime -eq 'node') { Get-DevVmNodeVersion -RuntimeInfo $RuntimeInfo -ShowAll $ShowAll -Update:$Update } elseif ($Runtime -eq 'java') { Get-DevVmJavaVersion -RuntimeInfo $RuntimeInfo -ShowAll $ShowAll -Update:$Update } elseif ($Runtime -eq 'maven') { Get-DevVmMavenVersion -RuntimeInfo $RuntimeInfo -ShowAll $ShowAll -Update:$Update } elseif ($Runtime -eq 'lein') { Get-DevVmLeinVersion -RuntimeInfo $RuntimeInfo -ShowAll $ShowAll -Update:$Update } else { Write-Output "Version fetching not implemented for $Runtime" } } catch { Write-Error "Failed to fetch available versions: $_" } } function Get-DevVmNodeVersion { param( [PSObject]$RuntimeInfo, [bool]$ShowAll, [switch]$Update ) $content = Get-DevVmCachedContent -Uri $RuntimeInfo.indexUrl -Update:$Update -Runtime 'node' -ErrorAction Stop $versions = $content | ConvertFrom-Json # Filter v10+ $filtered = $versions | Where-Object { [int]($_.version.TrimStart('v').Split('.')[0]) -ge 10 } if ($ShowAll) { Write-Output "Available Node.js versions (v10+):" $filtered | ForEach-Object { Write-Output " $($_.version)" } } else { Write-Output "Latest Node.js versions (one per major version):" $filtered | Group-Object { $_.version.Split('.')[0] } | ForEach-Object { $latest = $_.Group | Sort-Object -Property version -Descending | Select-Object -First 1 Write-Output " $($latest.version)" } } } function Get-DevVmJavaVersion { param( [PSObject]$RuntimeInfo, [bool]$ShowAll, [switch]$Update ) try { # Fetch Microsoft OpenJDK versions from learn.microsoft.com $indexUrl = $RuntimeInfo.indexUrl Write-Verbose "Fetching Microsoft OpenJDK versions from: $indexUrl" $content = Get-DevVmCachedContent -Uri $indexUrl -Update:$Update -Runtime 'java' -ErrorAction Stop # Parse version URLs from the page # Supports versions like: 21.0.2, 21.0, 21 $pattern = 'https://aka\.ms/download-jdk/microsoft-jdk-([0-9]+(?:\.[0-9]+){0,2})-windows-x64\.zip' $regexMatches = [regex]::Matches($content, $pattern) if ($regexMatches.Count -eq 0) { throw "No Microsoft OpenJDK versions found on the page" } # Extract unique versions and sort them $versions = @() $uniqueVersions = @{} foreach ($match in $regexMatches) { $version = $match.Groups[1].Value if (-not $uniqueVersions.ContainsKey($version)) { $uniqueVersions[$version] = $true $versions += $version } } # Normalize version strings for sorting (handles major-only entries like "25") $normalizeVersion = { param([string]$raw) if ($raw -match '^\d+$') { return "$raw.0.0" } if ($raw -match '^\d+\.\d+$') { return "$raw.0" } return $raw } # Sort versions (highest first) $versions = $versions | Sort-Object -Property { [version](& $normalizeVersion $_) } -Descending if ($versions.Count -eq 0) { throw "Failed to extract valid versions from Microsoft page" } # Categorize versions as LTS or Feature releases # LTS versions: 8, 11, 17, 21 (every 3 years) # Feature releases: others $categorized = @() foreach ($ver in $versions) { $majorVersion = [int]($ver.Split('.')[0]) $isLTS = @(8, 11, 17, 21) -contains $majorVersion $categorized += @{ Version = $ver LTS = $isLTS Major = $majorVersion SortKey = & $normalizeVersion $ver } } if ($ShowAll) { Write-Output "Available Microsoft OpenJDK versions (from learn.microsoft.com):" Write-Output "" $ltsVersions = $categorized | Where-Object { $_.LTS } | Sort-Object -Property Major -Descending $featureVersions = $categorized | Where-Object { -not $_.LTS } | Sort-Object -Property { [version]$_.SortKey } -Descending if ($ltsVersions.Count -gt 0) { Write-Output "LTS (Long Term Support) Versions:" $ltsVersions | ForEach-Object { Write-Output " $($_.Version) [LTS - Java $($_.Major)]" } } if ($featureVersions.Count -gt 0) { Write-Output "" Write-Output "Latest Feature Releases:" $featureVersions | ForEach-Object { Write-Output " $($_.Version)" } } } else { Write-Output "Latest Microsoft OpenJDK versions (LTS recommended):" Write-Output "" # Get latest version of each major release $grouped = $categorized | Group-Object -Property Major | Sort-Object -Property Name -Descending $grouped | ForEach-Object { $major = $_.Name $latest = $_.Group | Sort-Object -Property { [version]$_.SortKey } -Descending | Select-Object -First 1 $ltsLabel = if ($latest.LTS) { " [LTS]" } else { "" } Write-Output " Java ${major}: $($latest.Version)$ltsLabel" } } } catch { Write-Verbose "Failed to fetch Microsoft OpenJDK versions from web: $_" Write-Error "Failed to fetch Microsoft OpenJDK versions: $_" Write-Output "" Write-Output "Commonly available Microsoft OpenJDK versions you can try:" Write-Output " LTS versions: 21, 17, 11" Write-Output " Latest: 20" Write-Output "" Write-Output "Get latest versions from: https://learn.microsoft.com/en-us/java/openjdk/download-major-urls" } } function Get-DevVmMavenVersion { param( [PSObject]$RuntimeInfo, [bool]$ShowAll, [switch]$Update ) try { $content = Get-DevVmCachedContent -Uri $RuntimeInfo.indexUrl -Update:$Update -Runtime 'maven' -ErrorAction Stop $releases = $content | ConvertFrom-Json # Filter Maven releases (tag format: maven-3.9.0, maven-3.8.8, etc.) $mavenReleases = $releases | Where-Object { $_.tag_name -match '^maven-\d+\.\d+\.\d+$' } | ForEach-Object { [PSCustomObject]@{ Tag = $_.tag_name Version = $_.tag_name -replace '^maven-', '' Published = $_.published_at } } | Sort-Object -Property @{Expression={[version]$_.Version}} -Descending if ($ShowAll) { Write-Output "Available Maven versions:" $mavenReleases | ForEach-Object { Write-Output " $($_.Version) (published: $($_.Published.Substring(0,10)))" } } else { # Group by major.minor and show latest patch version for each Write-Output "Latest Maven versions (one per minor version):" $mavenReleases | Group-Object { $parts = $_.Version.Split('.') "$($parts[0]).$($parts[1])" } | ForEach-Object { $latest = $_.Group | Select-Object -First 1 Write-Output " $($latest.Version) (published: $($latest.Published.Substring(0,10)))" } } } catch { Write-Error "Failed to fetch Maven versions from GitHub API: $_" Write-Output "" Write-Output "Common Maven versions you can try:" Write-Output " 3.9.6" Write-Output " 3.9.5" Write-Output " 3.9.4" Write-Output " 3.8.8" Write-Output " 3.8.7" Write-Output " 3.8.6" } } function Get-DevVmLeinVersion { param( [PSObject]$RuntimeInfo, [bool]$ShowAll, [switch]$Update ) try { $content = Get-DevVmCachedContent -Uri $RuntimeInfo.indexUrl -Update:$Update -Runtime 'lein' -ErrorAction Stop $releases = $content | ConvertFrom-Json # Filter Leiningen releases (tags like: 2.10.0, 2.9.10, etc.) $leinReleases = $releases | Where-Object { $_.tag_name -match '^\d+\.\d+\.\d+$' } | ForEach-Object { [PSCustomObject]@{ Version = $_.tag_name Published = $_.published_at } } | Sort-Object -Property @{Expression={[version]$_.Version}} -Descending if ($ShowAll) { Write-Output "Available Leiningen versions:" $leinReleases | ForEach-Object { Write-Output " $($_.Version) (published: $($_.Published.Substring(0,10)))" } } else { # Group by major.minor and show latest patch version for each Write-Output "Latest Leiningen versions (one per minor version):" $leinReleases | Group-Object { $parts = $_.Version.Split('.') "$($parts[0]).$($parts[1])" } | ForEach-Object { $latest = $_.Group | Select-Object -First 1 Write-Output " $($latest.Version) (published: $($latest.Published.Substring(0,10)))" } } } catch { Write-Error "Failed to fetch Leiningen versions from GitHub API: $_" Write-Output "" Write-Output "Common Leiningen versions you can try:" Write-Output " 2.10.0" Write-Output " 2.9.10" Write-Output " 2.9.9" Write-Output " 2.9.8" Write-Output " 2.9.7" } } # Safe JSON parser for .devvm (handles BOM and trailing commas) function ConvertFrom-DevVmJsonSafe { param([string]$Raw) if ([string]::IsNullOrWhiteSpace($Raw)) { return $null } $normalized = $Raw.TrimStart([char]0xFEFF) $normalized = [regex]::Replace($normalized, ',\s*([}\]])', '$1') try { return $normalized | ConvertFrom-Json } catch { Write-Verbose "Failed to parse .devvm JSON: $_" return $null } } # Verify runtime command executes after activation function Test-DevVmRuntimeCommand { param( [string]$Runtime, [string]$Command, [string]$VersionFlag ) if ([string]::IsNullOrWhiteSpace($Command)) { Write-Verbose "No command configured for runtime ${Runtime}" return $null } $cmd = Get-Command $Command -ErrorAction SilentlyContinue if (-not $cmd) { Write-Warning "Could not find command for ${Runtime} after activation: $Command" return $null } $versionArgs = @() if (-not [string]::IsNullOrWhiteSpace($VersionFlag)) { $versionArgs += $VersionFlag } try { # Capture the first line of output from the command # Commands like 'java -version' write to stderr, so we redirect with 2>&1 # We use a script block with &() to properly handle the stderr output $ErrorActionPreference = 'Continue' $output = & { $result = & $Command @versionArgs 2>&1 return $result } | Select-Object -First 1 | ForEach-Object { "$_" } if ($output -and "$output".Trim() -ne "") { # Format: [RUNTIME_NAME] version_output Write-Information " [OK] ${Runtime}: $output" -InformationAction Continue return $output } Write-Verbose "No output from $Runtime command" return $null } catch { Write-Warning "Failed to execute $Runtime command: $_" return $null } } |