Untitled

 avatar
unknown
plain_text
10 months ago
45 kB
13
Indexable
# UEPluginManager.ps1 - Multi-functional UE Plugin Packager & Bootstrap Manager
# Usage: .\UEPluginManager.ps1 [command] [args]
# Commands: package, init, add, install, list, clean
# No args = package (backward compatibility with your existing workflow)

[CmdletBinding()] param(
    [Parameter(Position=0)]
    [string]$Command = "package"
)

$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null

# === UTILITY FUNCTIONS (keeping your existing ones) ===

function Pick-File($title,$filter="All (*.*)|*.*"){
  Write-Host "Attempting to open file dialog: $title" -ForegroundColor Gray
  try {
    $dlg=New-Object System.Windows.Forms.OpenFileDialog
    $dlg.Title=$title
    $dlg.Filter=$filter
    $dlg.Multiselect=$false
    $dlg.CheckFileExists=$true
    $dlg.CheckPathExists=$true
    
    Write-Host "Showing file dialog..." -ForegroundColor Gray
    $result = $dlg.ShowDialog()
    Write-Host "File dialog result: $result" -ForegroundColor Gray
    
    if($result -eq [System.Windows.Forms.DialogResult]::OK){ 
      Write-Host "Selected file: $($dlg.FileName)" -ForegroundColor Green
      return $dlg.FileName 
    } else { 
      Write-Host "File dialog cancelled or closed" -ForegroundColor Yellow
      return $null 
    }
  } catch {
    Write-Host "Error with file dialog: $($_.Exception.Message)" -ForegroundColor Red
    Write-Host "Falling back to text input..." -ForegroundColor Yellow
    $path = Read-Host "Please enter the full path to your .uplugin file"
    if(Test-Path $path) {
      return $path
    } else {
      Write-Host "Path does not exist: $path" -ForegroundColor Red
      return $null
    }
  }
}

function Pick-Folder($desc){
  Write-Host "Attempting to open folder dialog: $desc" -ForegroundColor Gray
  try {
    $dlg=New-Object System.Windows.Forms.FolderBrowserDialog
    $dlg.Description=$desc
    $dlg.ShowNewFolderButton=$true
    
    Write-Host "Showing folder dialog..." -ForegroundColor Gray
    $result = $dlg.ShowDialog()
    Write-Host "Folder dialog result: $result" -ForegroundColor Gray
    
    if($result -eq [System.Windows.Forms.DialogResult]::OK){ 
      Write-Host "Selected folder: $($dlg.SelectedPath)" -ForegroundColor Green
      return $dlg.SelectedPath 
    } else { 
      Write-Host "Folder dialog cancelled or closed" -ForegroundColor Yellow
      return $null 
    }
  } catch {
    Write-Host "Error with folder dialog: $($_.Exception.Message)" -ForegroundColor Red
    Write-Host "Falling back to text input..." -ForegroundColor Yellow
    $path = Read-Host "Please enter the full path to the folder"
    if(Test-Path $path) {
      return $path
    } else {
      Write-Host "Path does not exist: $path" -ForegroundColor Red
      return $null
    }
  }
}

function Ask($prompt,$def=""){
  if([string]::IsNullOrWhiteSpace($def)){
    $v = Read-Host $prompt
  } else {
    $v = Read-Host "$prompt [$def]"
    if([string]::IsNullOrWhiteSpace($v)){ $v = $def }
  }
  return $v
}

function Ask-YesNo($prompt, $default=$true){
  $suffix = if($default) { "(Y/n)" } else { "(y/N)" }
  $v = Read-Host "$prompt $suffix"
  if([string]::IsNullOrWhiteSpace($v)){ return $default }
  return ($v -match '^(?i)y|yes$')
}

function Ensure-Dir($p){ 
  if(-not(Test-Path -LiteralPath $p)){ 
    New-Item -ItemType Directory -Force -Path $p | Out-Null 
  } 
}

function Read-Json($p){ 
  if(-not (Test-Path $p)) { return $null }
  try {
    return (Get-Content -Raw -LiteralPath $p) | ConvertFrom-Json 
  } catch {
    Write-Warning "Could not parse JSON: $p"
    return $null
  }
}

function Write-Json($obj,$p){ 
  ($obj|ConvertTo-Json -Depth 20) | Out-File -LiteralPath $p -Encoding UTF8 
}

function Parse-Platforms($s){
  if([string]::IsNullOrWhiteSpace($s)){ return @('Win64') }
  return ($s -split '[,\s]+' | Where-Object { $_.Trim() } | ForEach-Object { $_.Trim() })
}

# === SEMVER FUNCTIONS (new for package manager) ===

function Parse-SemVer($version) {
  if($version -match '^(\d+)\.(\d+)\.(\d+)(-.*)?$') {
    return @{
      Major = [int]$matches[1]
      Minor = [int]$matches[2] 
      Patch = [int]$matches[3]
      PreRelease = $matches[4]
      Original = $version
    }
  }
  return $null
}

function Compare-SemVer($v1, $v2) {
  $ver1 = Parse-SemVer $v1
  $ver2 = Parse-SemVer $v2
  
  if(-not $ver1 -or -not $ver2) { return [string]::Compare($v1, $v2) }
  
  if($ver1.Major -ne $ver2.Major) { return $ver1.Major - $ver2.Major }
  if($ver1.Minor -ne $ver2.Minor) { return $ver1.Minor - $ver2.Minor }
  if($ver1.Patch -ne $ver2.Patch) { return $ver1.Patch - $ver2.Patch }
  
  return 0
}

function Test-SemVerRange($version, $range) {
  if([string]::IsNullOrWhiteSpace($range)) { return $true }
  
  $ver = Parse-SemVer $version
  if(-not $ver) { return $false }
  
  # Simple range parsing
  if($range -match '^\^(\d+\.\d+\.\d+)$') {
    $target = Parse-SemVer $matches[1]
    return ($ver.Major -eq $target.Major -and 
            (($ver.Minor -gt $target.Minor) -or 
             ($ver.Minor -eq $target.Minor -and $ver.Patch -ge $target.Patch)))
  }
  
  if($range -match '^~(\d+\.\d+\.\d+)$') {
    $target = Parse-SemVer $matches[1]
    return ($ver.Major -eq $target.Major -and 
            $ver.Minor -eq $target.Minor -and 
            $ver.Patch -ge $target.Patch)
  }
  
  # Exact match
  return ($version -eq $range)
}

# === YOUR EXISTING PACKAGING FUNCTIONS (unchanged) ===

function Find-UE-Installations {
  $candidates = @()
  
  # Common Epic Games locations
  $epicPaths = @(
    "$env:ProgramFiles\Epic Games",
    "${env:ProgramFiles(x86)}\Epic Games",
    "$env:ProgramW6432\Epic Games"
  )
  
  foreach($path in $epicPaths) {
    if(Test-Path $path) {
      $ueVersions = Get-ChildItem -Path $path -Directory | Where-Object { $_.Name -match '^UE_\d+\.\d+' }
      foreach($ue in $ueVersions) {
        $runUAT = Join-Path $ue.FullName "Engine\Build\BatchFiles\RunUAT.bat"
        if(Test-Path $runUAT) {
          $candidates += @{
            Path = $ue.FullName
            Version = ($ue.Name -replace '^UE_', '')
            Display = "$($ue.Name) - $($ue.FullName)"
          }
        }
      }
    }
  }
  
  return $candidates | Sort-Object Version -Descending
}

function Prompt-UE-Selection {
  Write-Host ""
  Write-Host "=== Unreal Engine Selection ===" -ForegroundColor Cyan
  
  $installations = Find-UE-Installations
  
  if($installations.Count -gt 0) {
    Write-Host "Found UE installations:" -ForegroundColor Yellow
    for($i = 0; $i -lt $installations.Count; $i++) {
      Write-Host "  [$($i+1)] $($installations[$i].Display)" -ForegroundColor Green
    }
    Write-Host "  [0] Browse for different location" -ForegroundColor Gray
    
    do {
      $choice = Read-Host "Select UE installation (1-$($installations.Count) or 0 to browse)"
      $choiceNum = $choice -as [int]
    } while($choiceNum -lt 0 -or $choiceNum -gt $installations.Count)
    
    if($choiceNum -eq 0) {
      return Pick-Folder "Select UE root directory (e.g. C:\Program Files\Epic Games\UE_5.5)"
    } else {
      return $installations[$choiceNum - 1].Path
    }
  } else {
    Write-Host "No UE installations found automatically." -ForegroundColor Yellow
    return Pick-Folder "Select UE root directory (e.g. C:\Program Files\Epic Games\UE_5.5)"
  }
}

function Prompt-Plugin-Selection {
  Write-Host ""
  Write-Host "=== Main Plugin Selection ===" -ForegroundColor Cyan
  Write-Host "Select the .uplugin file you want to package..." -ForegroundColor Yellow
  
  $selected = Pick-File "Select the main .uplugin file to package" "Unreal Plugin (*.uplugin)|*.uplugin|All (*.*)|*.*"
  if(-not $selected) {
    Write-Host "No plugin selected." -ForegroundColor Red
    return $null
  }
  
  return $selected
}

function Find-Dependencies-OneLevel-Up($mainPluginPath, $depNames) {
  Write-Host ""
  Write-Host "Looking for dependencies one level up from main plugin..." -ForegroundColor Yellow
  
  # Get the parent directory of the main plugin
  $mainPluginDir = Split-Path -Parent $mainPluginPath
  $oneLevelUp = Split-Path -Parent $mainPluginDir
  
  Write-Host "Main plugin directory: $mainPluginDir" -ForegroundColor Gray
  Write-Host "Searching for dependencies in: $oneLevelUp" -ForegroundColor Gray
  
  $foundDeps = @()
  
  foreach($depName in $depNames) {
    Write-Host "  Looking for: $depName" -ForegroundColor Cyan
    
    # Look for DepName/DepName.uplugin in the parent directory
    $depDir = Join-Path $oneLevelUp $depName
    $depUplugin = Join-Path $depDir ($depName + ".uplugin")
    
    if(Test-Path $depUplugin) {
      Write-Host "    Found: $depUplugin" -ForegroundColor Green
      $foundDeps += $depUplugin
    } else {
      Write-Host "    Not found at: $depUplugin" -ForegroundColor Red
      
      # Also check if there's a subdirectory with the dependency
      $subDirs = Get-ChildItem -Path $oneLevelUp -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq $depName }
      if($subDirs) {
        $subDepUplugin = Join-Path $subDirs[0].FullName ($depName + ".uplugin")
        if(Test-Path $subDepUplugin) {
          Write-Host "    Found in subdirectory: $subDepUplugin" -ForegroundColor Green
          $foundDeps += $subDepUplugin
        }
      }
    }
  }
  
  return $foundDeps
}

function Copy-PluginFolder($pluginUPluginPath, $destPluginsRoot){
  $srcPluginDir = Split-Path -Parent $pluginUPluginPath
  $name = [IO.Path]::GetFileNameWithoutExtension($pluginUPluginPath)
  $destDir = Join-Path $destPluginsRoot $name
  
  if(Test-Path $destDir){ Remove-Item -Recurse -Force -LiteralPath $destDir }
  Write-Host "  Copying $name -> $destDir" -ForegroundColor Green
  
  # Use robocopy for reliable copying
  $robocopyResult = & robocopy "$srcPluginDir" "$destDir" /E /NFL /NDL /NJH /NJS /NP 2>$null
  if($LASTEXITCODE -ge 8){ 
    # Fallback to PowerShell copy
    Copy-Item -Path $srcPluginDir -Destination $destDir -Recurse -Force
  }
  
  return $destDir
}

function Try-BuildPlugin-Method1($runUAT, $pluginPath, $packageDir, $platforms) {
  Write-Host ""
  Write-Host "==> Trying Method 1: Direct BuildPlugin (no HostProject)" -ForegroundColor Yellow
  
  $platArg = ($platforms -join '+')
  $uatArgs = @(
    'BuildPlugin',
    ('-Plugin="{0}"' -f $pluginPath),
    ('-Package="{0}"' -f $packageDir),
    ('-TargetPlatforms={0}' -f $platArg),
    '-Rocket', '-NoP4'
  )
  
  Write-Host "UAT Command: $($uatArgs -join ' ')" -ForegroundColor Gray
  
  try {
    & $runUAT @uatArgs
    return $LASTEXITCODE -eq 0
  } catch {
    Write-Host "Method 1 failed with exception: $($_.Exception.Message)" -ForegroundColor Red
    return $false
  }
}

function Try-BuildPlugin-Method2($runUAT, $pluginPath, $packageDir, $platforms, $workDir) {
  Write-Host ""
  Write-Host "==> Trying Method 2: BuildPlugin with minimal HostProject" -ForegroundColor Yellow
  
  # Create minimal host project
  $hostProjDir = Join-Path $workDir "HostProject"
  $hostProjFile = Join-Path $hostProjDir "HostProject.uproject"
  Ensure-Dir $hostProjDir
  
  # Get engine version from plugin or use default
  try {
    $pluginJson = Read-Json $pluginPath
    $engineVer = if($pluginJson.EngineVersion) { $pluginJson.EngineVersion } else { "5.5" }
  } catch {
    $engineVer = "5.5"
  }
  
  # Create simple .uproject
  @"
{
  "FileVersion": 3,
  "EngineAssociation": "$engineVer",
  "Category": "",
  "Description": "Minimal host project for BuildPlugin"
}
"@ | Out-File -LiteralPath $hostProjFile -Encoding UTF8
  
  $platArg = ($platforms -join '+')
  $uatArgs = @(
    'BuildPlugin',
    ('-Plugin="{0}"' -f $pluginPath),
    ('-Package="{0}"' -f $packageDir),
    ('-TargetPlatforms={0}' -f $platArg),
    ('-HostProject="{0}"' -f $hostProjFile),
    '-Rocket', '-NoP4'
  )
  
  Write-Host "UAT Command: $($uatArgs -join ' ')" -ForegroundColor Gray
  
  try {
    & $runUAT @uatArgs
    return $LASTEXITCODE -eq 0
  } catch {
    Write-Host "Method 2 failed with exception: $($_.Exception.Message)" -ForegroundColor Red
    return $false
  }
}

function Try-BuildPlugin-Method3($runUAT, $pluginPath, $depPluginPaths, $packageDir, $platforms, $workDir) {
  Write-Host ""
  Write-Host "==> Trying Method 3: BuildPlugin with dependency copying to HostProject" -ForegroundColor Yellow
  
  # Create host project with copied dependencies
  $hostProjDir = Join-Path $workDir "HostProjectWithDeps"
  $hostProjFile = Join-Path $hostProjDir "HostProject.uproject"
  $hostPluginsDir = Join-Path $hostProjDir "Plugins"
  
  if(Test-Path $hostProjDir) { Remove-Item -Recurse -Force $hostProjDir }
  Ensure-Dir $hostPluginsDir
  
  # Copy all dependencies to host project
  $copiedPaths = @()
  foreach($depPath in $depPluginPaths) {
    $copied = Copy-PluginFolder -pluginUPluginPath $depPath -destPluginsRoot $hostPluginsDir
    $copiedPaths += $copied
  }
  
  # Also copy main plugin
  $mainCopied = Copy-PluginFolder -pluginUPluginPath $pluginPath -destPluginsRoot $hostPluginsDir
  $mainPluginName = [IO.Path]::GetFileNameWithoutExtension($pluginPath)
  $copiedMainPlugin = Join-Path $hostPluginsDir "$mainPluginName\$mainPluginName.uplugin"
  
  # Create enhanced .uproject
  try {
    $pluginJson = Read-Json $pluginPath
    $engineVer = if($pluginJson.EngineVersion) { $pluginJson.EngineVersion } else { "5.5" }
  } catch {
    $engineVer = "5.5"
  }
  
  # Build plugin references
  $pluginRefs = @()
  foreach($depPath in $depPluginPaths) {
    $depName = [IO.Path]::GetFileNameWithoutExtension($depPath)
    $pluginRefs += @{ "Name" = $depName; "Enabled" = $true }
  }
  $pluginRefs += @{ "Name" = $mainPluginName; "Enabled" = $true }
  
  $uprojectContent = @{
    "FileVersion" = 3
    "EngineAssociation" = $engineVer
    "Category" = ""
    "Description" = "Host project with dependencies for BuildPlugin"
    "Plugins" = $pluginRefs
  }
  
  Write-Json $uprojectContent $hostProjFile
  
  $platArg = ($platforms -join '+')
  $uatArgs = @(
    'BuildPlugin',
    ('-Plugin="{0}"' -f $copiedMainPlugin),
    ('-Package="{0}"' -f $packageDir),
    ('-TargetPlatforms={0}' -f $platArg),
    ('-HostProject="{0}"' -f $hostProjFile),
    '-Rocket', '-NoP4'
  )
  
  Write-Host "UAT Command: $($uatArgs -join ' ')" -ForegroundColor Gray
  
  try {
    & $runUAT @uatArgs
    return $LASTEXITCODE -eq 0
  } catch {
    Write-Host "Method 3 failed with exception: $($_.Exception.Message)" -ForegroundColor Red
    return $false
  }
}

function Get-PackagedPluginPath($packageDir, $pluginBase){
  # Look for the packaged plugin directory
  $expectedDir = Join-Path $packageDir $pluginBase
  $expectedUplugin = Join-Path $expectedDir ($pluginBase + ".uplugin")
  if(Test-Path $expectedUplugin){ return $expectedDir }
  
  # Search recursively
  $found = Get-ChildItem -Path $packageDir -Filter ($pluginBase + ".uplugin") -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
  if($found){ return $found.Directory.FullName }
  
  return $null
}

# === NEW BOOTSTRAP PACKAGE MANAGER FUNCTIONS ===

function Initialize-PluginProject {
  $projectDir = Get-Location
  Write-Host "Initializing plugin project in: $projectDir" -ForegroundColor Cyan
  
  # Check if we're in a UE project
  $uprojectFiles = Get-ChildItem -Filter "*.uproject" -ErrorAction SilentlyContinue
  if($uprojectFiles.Count -eq 0) {
    Write-Warning "No .uproject file found. Are you in a UE project directory?"
    if(-not (Ask-YesNo "Continue anyway?" $false)) {
      return
    }
  }
  
  $pluginsDir = Join-Path $projectDir "Plugins"
  Ensure-Dir $pluginsDir
  
  $requirementsFile = Join-Path $pluginsDir "plugins.requirements.json"
  $lockFile = Join-Path $pluginsDir ".ueplugins.lock.json"
  
  if(-not (Test-Path $requirementsFile)) {
    $engineVer = Ask "Engine version" "5.5"
    
    $requirements = [ordered]@{
      engine = $engineVer
      plugins = @{}
    }
    
    Write-Json $requirements $requirementsFile
    Write-Host "Created: $requirementsFile" -ForegroundColor Green
  } else {
    Write-Host "Requirements file already exists: $requirementsFile" -ForegroundColor Yellow
  }
  
  Write-Host ""
  Write-Host "Plugin project initialized!" -ForegroundColor Green
  Write-Host "  - Edit $requirementsFile to add plugin dependencies" -ForegroundColor Gray
  Write-Host "  - Run 'install' to resolve and download plugins" -ForegroundColor Gray
}

function Add-Plugin-Requirement($pluginSpec) {
  $pluginsDir = Join-Path (Get-Location) "Plugins"
  $requirementsFile = Join-Path $pluginsDir "plugins.requirements.json"
  
  if(-not (Test-Path $requirementsFile)) {
    Write-Host "No requirements file found. Run 'init' first." -ForegroundColor Red
    return
  }
  
  if([string]::IsNullOrWhiteSpace($pluginSpec)) {
    $pluginSpec = Read-Host "Enter plugin specification (name@version, e.g. MyPlugin@^1.0.0)"
  }
  
  if(-not ($pluginSpec -match '^(.+)@(.+)$')) {
    Write-Host "Invalid format. Use: PluginName@VersionRange (e.g. MyPlugin@^1.0.0)" -ForegroundColor Red
    return
  }
  
  $pluginName = $matches[1].Trim()
  $versionRange = $matches[2].Trim()
  
  $requirements = Read-Json $requirementsFile
  if(-not $requirements) {
    Write-Host "Could not read requirements file." -ForegroundColor Red
    return
  }
  
  if(-not $requirements.plugins) {
    $requirements.plugins = @{}
  }
  
  $requirements.plugins.$pluginName = $versionRange
  
  Write-Json $requirements $requirementsFile
  Write-Host "Added $pluginName@$versionRange to requirements" -ForegroundColor Green
}

function Get-Local-Nexus-Path {
  $nexusPath = Join-Path (Get-Location) "Nexus"
  if(-not (Test-Path $nexusPath)) {
    Write-Host "No local Nexus directory found at: $nexusPath" -ForegroundColor Red
    $create = Ask-YesNo "Create local Nexus directory?" $true
    if($create) {
      Ensure-Dir $nexusPath
      Write-Host "Created local Nexus directory: $nexusPath" -ForegroundColor Green
      Write-Host "Place your .zip and .manifest.json files in this directory" -ForegroundColor Yellow
    } else {
      return $null
    }
  }
  return $nexusPath
}

function Scan-Local-Nexus($nexusPath) {
  Write-Host "Scanning local Nexus: $nexusPath" -ForegroundColor Yellow
  
  $manifests = @{}
  $manifestFiles = Get-ChildItem -Path $nexusPath -Filter "*.manifest.json" -ErrorAction SilentlyContinue
  
  foreach($manifestFile in $manifestFiles) {
    $manifest = Read-Json $manifestFile.FullName
    if($manifest -and $manifest.name) {
      # Find corresponding ZIP file
      $expectedZip = $manifestFile.FullName -replace '\.manifest\.json$', '.zip'
      if(Test-Path $expectedZip) {
        $manifest.zipPath = $expectedZip
        
        if(-not $manifests[$manifest.name]) {
          $manifests[$manifest.name] = @()
        }
        $manifests[$manifest.name] += $manifest
        Write-Host "  Found: $($manifest.name)@$($manifest.version) (engine: $($manifest.engine))" -ForegroundColor Green
      } else {
        Write-Warning "Manifest found but no corresponding ZIP: $($manifestFile.Name)"
      }
    }
  }
  
  # Sort versions for each plugin (newest first)
  foreach($pluginName in $manifests.Keys) {
    $manifests[$pluginName] = $manifests[$pluginName] | Sort-Object { 
      $ver = Parse-SemVer $_.version
      if($ver) { 
        -$ver.Major * 10000 - $ver.Minor * 100 - $ver.Patch 
      } else { 
        $_.version 
      }
    }
  }
  
  return $manifests
}

function Resolve-Plugin-Dependencies($requirements, $availableManifests, $targetEngine) {
  Write-Host ""
  Write-Host "=== Resolving Dependencies ===" -ForegroundColor Cyan
  Write-Host "Target engine: $targetEngine" -ForegroundColor Gray
  
  $resolved = @{}
  $toProcess = @()
  
  # Add root requirements
  foreach($pluginName in $requirements.plugins.Keys) {
    $versionRange = $requirements.plugins.$pluginName
    $toProcess += @{ Name = $pluginName; Range = $versionRange; IsRoot = $true }
    Write-Host "Root requirement: $pluginName@$versionRange" -ForegroundColor Cyan
  }
  
  while($toProcess.Count -gt 0) {
    $current = $toProcess[0]
    $toProcess = $toProcess[1..($toProcess.Count-1)]
    
    $pluginName = $current.Name
    $versionRange = $current.Range
    
    Write-Host "Processing: $pluginName@$versionRange" -ForegroundColor Gray
    
    if($resolved.ContainsKey($pluginName)) {
      Write-Host "  Already resolved: $pluginName@$($resolved[$pluginName].version)" -ForegroundColor Gray
      continue
    }
    
    if(-not $availableManifests.ContainsKey($pluginName)) {
      Write-Host "  Plugin not found in Nexus: $pluginName" -ForegroundColor Red
      throw "Plugin not found: $pluginName"
    }
    
    # Find compatible version
    $compatibleVersions = $availableManifests[$pluginName] | Where-Object { 
      $_.engine -eq $targetEngine -and (Test-SemVerRange $_.version $versionRange)
    }
    
    if($compatibleVersions.Count -eq 0) {
      Write-Host "  No compatible version found: $pluginName@$versionRange (engine: $targetEngine)" -ForegroundColor Red
      Write-Host "  Available versions for $pluginName" -ForegroundColor Yellow
      $availableManifests[$pluginName] | ForEach-Object {
        Write-Host "    $($_.version) (engine: $($_.engine))" -ForegroundColor Gray
      }
      throw "No compatible version: $pluginName@$versionRange for engine $targetEngine"
    }
    
    $selectedVersion = $compatibleVersions[0]
    $resolved[$pluginName] = $selectedVersion
    
    Write-Host "  Selected: $pluginName@$($selectedVersion.version)" -ForegroundColor Green
    
    # Add dependencies
    if($selectedVersion.dependencies) {
      foreach($dep in $selectedVersion.dependencies) {
        if($dep.name -and -not $resolved.ContainsKey($dep.name)) {
          $alreadyQueued = $toProcess | Where-Object { $_.Name -eq $dep.name }
          if(-not $alreadyQueued) {
            $depRange = if($dep.range) { $dep.range } else { "*" }
            $toProcess += @{ Name = $dep.name; Range = $depRange; IsRoot = $false }
            Write-Host "    Added dependency: $($dep.name)@$depRange" -ForegroundColor Cyan
          }
        }
      }
    }
  }
  
  return $resolved
}

function Install-Resolved-Plugins($resolved, $pluginsDir) {
  Write-Host ""
  Write-Host "=== Installing Plugins ===" -ForegroundColor Cyan
  
  foreach($pluginName in $resolved.Keys) {
    $manifest = $resolved[$pluginName]
    $zipPath = $manifest.zipPath
    $pluginDir = Join-Path $pluginsDir $pluginName
    
    Write-Host "Installing: $pluginName@$($manifest.version)" -ForegroundColor Yellow
    Write-Host "  From: $zipPath" -ForegroundColor Gray
    Write-Host "  To: $pluginDir" -ForegroundColor Gray
    
    if(Test-Path $pluginDir) {
      Write-Host "  Removing existing: $pluginDir" -ForegroundColor Gray
      Remove-Item -Recurse -Force $pluginDir
    }
    
    try {
      # Extract ZIP to temp location first
      $tempDir = Join-Path $env:TEMP "uepm_extract_$(Get-Random)"
      Ensure-Dir $tempDir
      
      Expand-Archive -Path $zipPath -DestinationPath $tempDir -Force
      
      # Find the plugin folder in the extracted content (skip manifest.json)
      $extractedPlugin = Get-ChildItem -Path $tempDir -Directory | Where-Object { 
        Test-Path (Join-Path $_.FullName ($_.Name + ".uplugin"))
      } | Select-Object -First 1
      
      if($extractedPlugin) {
        Move-Item -Path $extractedPlugin.FullName -Destination $pluginDir
        Write-Host "  Installed successfully" -ForegroundColor Green
      } else {
        Write-Host "  Could not find plugin folder in ZIP" -ForegroundColor Red
      }
      
      # Cleanup temp
      Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue
      
    } catch {
      Write-Host "  Installation failed: $($_.Exception.Message)" -ForegroundColor Red
    }
  }
}

function Create-Lockfile($resolved, $requirements, $lockFilePath) {
  $lockData = [ordered]@{
    engine = $requirements.engine
    resolved = @()
  }
  
  foreach($pluginName in ($resolved.Keys | Sort-Object)) {
    $manifest = $resolved[$pluginName]
    $lockData.resolved += [ordered]@{
      name = $manifest.name
      version = $manifest.version
      platform = "Win64"
      url = [IO.Path]::GetFileName($manifest.zipPath)
      sha256 = if($manifest.sha256 -and $manifest.sha256.Win64) { $manifest.sha256.Win64 } else { "" }
    }
  }
  
  Write-Json $lockData $lockFilePath
  Write-Host "Created lockfile: $lockFilePath" -ForegroundColor Green
}

# === COMMAND HANDLERS ===

function Handle-Package-Command {
  # This is your existing packaging workflow - unchanged
  Write-Host "=== UE Plugin Builder with Dependency Resolution ===" -ForegroundColor Cyan

  # 1. Select UE Installation
  $ueRoot = Prompt-UE-Selection
  if(-not $ueRoot) { throw "No UE installation selected" }

  $runUAT = Join-Path $ueRoot "Engine\Build\BatchFiles\RunUAT.bat"
  if(-not (Test-Path $runUAT)) { throw "RunUAT not found at: $runUAT" }

  Write-Host ""
  Write-Host "Selected UE: $ueRoot" -ForegroundColor Green

  # 2. Select Main Plugin
  $mainPluginPath = Prompt-Plugin-Selection
  if(-not $mainPluginPath) { throw "No plugin selected" }

  Write-Host ""
  Write-Host "Selected Plugin: $mainPluginPath" -ForegroundColor Green

  # 3. Get plugin info
  try {
    $pluginJson = Read-Json $mainPluginPath
    $pluginBase = [IO.Path]::GetFileNameWithoutExtension($mainPluginPath)
    $pluginName = if($pluginJson.FriendlyName) { $pluginJson.FriendlyName } else { $pluginBase }
    $pluginVer = if($pluginJson.VersionName) { $pluginJson.VersionName } elseif($pluginJson.Version) { $pluginJson.Version.ToString() } else { "1.0.0" }
  } catch {
    throw "Could not read plugin file: $mainPluginPath"
  }

  # 4. Get build settings
  Write-Host ""
  Write-Host "=== Build Configuration ===" -ForegroundColor Cyan
  $engineVer = Ask "Engine version" "5.5"
  $platformsStr = Ask "Target platforms (comma-separated)" "Win64"
  $platforms = Parse-Platforms $platformsStr

  # 5. Select output directory
  Write-Host ""
  $artifactsDir = Pick-Folder "Select output directory for build artifacts"
  if(-not $artifactsDir) { throw "No output directory selected" }

  Write-Host ""
  Write-Host "=== Plugin Information ===" -ForegroundColor Cyan
  Write-Host "  Name: $pluginName" -ForegroundColor Green
  Write-Host "  Base Name: $pluginBase" -ForegroundColor Green
  Write-Host "  Version: $pluginVer" -ForegroundColor Green
  Write-Host "  Engine: $engineVer" -ForegroundColor Green
  Write-Host "  Platforms: $($platforms -join ', ')" -ForegroundColor Green
  Write-Host "  Output: $artifactsDir" -ForegroundColor Green

  # 6. Check for dependencies
  $depNames = @()
  if($pluginJson.Plugins) {
    foreach($dep in $pluginJson.Plugins) {
      if($dep.Name) { $depNames += $dep.Name }
    }
  }
  $depNames = $depNames | Select-Object -Unique

  $depPluginPaths = @()
  if($depNames.Count -gt 0) {
    Write-Host ""
    Write-Host "=== Dependency Resolution ===" -ForegroundColor Cyan
    Write-Host "Dependencies required: $($depNames -join ', ')" -ForegroundColor Yellow
    
    # Look for dependencies one level up from the main plugin
    $foundDeps = Find-Dependencies-OneLevel-Up -mainPluginPath $mainPluginPath -depNames $depNames

    foreach($depName in $depNames) {
      $found = $foundDeps | Where-Object { [IO.Path]::GetFileNameWithoutExtension($_) -eq $depName }
      
      if($found) {
        Write-Host ""
        Write-Host "Found dependency: $depName at $found" -ForegroundColor Green
        $useFound = Ask-YesNo "Use this dependency?" $true
        if($useFound) {
          $depPluginPaths += $found
          Write-Host "  Added: $found" -ForegroundColor Green
        } else {
          $manual = Pick-File "Locate $depName.uplugin manually" "Unreal Plugin (*.uplugin)|*.uplugin"
          if($manual) {
            $depPluginPaths += $manual
            Write-Host "  Added: $manual" -ForegroundColor Green
          } else {
            Write-Warning "Dependency $depName not provided - build may fail"
          }
        }
      } else {
        Write-Host ""
        Write-Host "Could not find dependency: $depName" -ForegroundColor Red
        $manual = Pick-File "Locate $depName.uplugin manually" "Unreal Plugin (*.uplugin)|*.uplugin"
        if($manual) {
          $depPluginPaths += $manual
          Write-Host "  Added: $manual" -ForegroundColor Green
        } else {
          Write-Warning "Dependency $depName not provided - build may fail"
        }
      }
    }
  } else {
    Write-Host ""
    Write-Host "No dependencies declared in plugin." -ForegroundColor Green
  }

  # 7. Create work directory and package directory
  $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
  $buildStamp = "${pluginBase}_${pluginVer}_UE${engineVer}_$($platforms -join '+')_${timestamp}"
  $workDir = Join-Path $artifactsDir "_Work_$buildStamp"
  $packageDir = Join-Path $artifactsDir "Package_$buildStamp"

  if(Test-Path $workDir) { Remove-Item -Recurse -Force $workDir }
  if(Test-Path $packageDir) { Remove-Item -Recurse -Force $packageDir }
  Ensure-Dir $workDir
  Ensure-Dir $packageDir

  Write-Host ""
  Write-Host "=== Starting Build Process ===" -ForegroundColor Cyan
  Write-Host "Work directory: $workDir" -ForegroundColor Gray
  Write-Host "Package directory: $packageDir" -ForegroundColor Gray

  # 8. Try multiple build methods
  $buildSuccess = $false
  $buildMethod = ""

  try {
    # Method 1: Direct build (works for plugins without dependencies)
    if(-not $buildSuccess -and $depPluginPaths.Count -eq 0) {
      if(Try-BuildPlugin-Method1 -runUAT $runUAT -pluginPath $mainPluginPath -packageDir $packageDir -platforms $platforms) {
        $buildSuccess = $true
        $buildMethod = "Direct BuildPlugin (no dependencies)"
      }
    }
    
    # Method 2: Minimal host project
    if(-not $buildSuccess) {
      if(Try-BuildPlugin-Method2 -runUAT $runUAT -pluginPath $mainPluginPath -packageDir $packageDir -platforms $platforms -workDir $workDir) {
        $buildSuccess = $true
        $buildMethod = "BuildPlugin with minimal HostProject"
      }
    }
    
    # Method 3: Host project with dependencies
    if(-not $buildSuccess -and $depPluginPaths.Count -gt 0) {
      if(Try-BuildPlugin-Method3 -runUAT $runUAT -pluginPath $mainPluginPath -depPluginPaths $depPluginPaths -packageDir $packageDir -platforms $platforms -workDir $workDir) {
        $buildSuccess = $true
        $buildMethod = "BuildPlugin with HostProject and dependencies"
      }
    }
    
  } finally {
    # Cleanup work directory
    if(Test-Path $workDir) {
      try { Remove-Item -Recurse -Force $workDir } catch { Write-Warning "Could not clean up work directory" }
    }
  }

  if(-not $buildSuccess) {
    Write-Host ""
    Write-Host "=== BUILD FAILED ===" -ForegroundColor Red
    Write-Host "All build methods failed. Check the UAT logs above for details." -ForegroundColor Red
    throw "Plugin build failed with all attempted methods"
  }

  Write-Host ""
  Write-Host "=== BUILD SUCCESSFUL ===" -ForegroundColor Green
  Write-Host "Method used: $buildMethod" -ForegroundColor Yellow

  # 9. Package the result WITH MANIFEST INSIDE ZIP
  $packagedDir = Get-PackagedPluginPath -packageDir $packageDir -pluginBase $pluginBase
  if(-not $packagedDir) {
    throw "Could not locate packaged plugin in: $packageDir"
  }

  $zipPath = Join-Path $artifactsDir "${buildStamp}.zip"
  if(Test-Path $zipPath) { Remove-Item $zipPath -Force }

  Write-Host ""
  Write-Host "Creating ZIP package with embedded manifest..." -ForegroundColor Yellow
  
  # Create temp directory for packaging with manifest
  $tempPackageDir = Join-Path $env:TEMP "uepm_package_$(Get-Random)"
  Ensure-Dir $tempPackageDir
  
  try {
    # Copy plugin to temp directory
    $tempPluginDir = Join-Path $tempPackageDir $pluginBase
    Copy-Item -Path $packagedDir -Destination $tempPluginDir -Recurse -Force
    
    # Create manifest for inside the ZIP
    $zipFileName = [IO.Path]::GetFileName($zipPath)
    
    # Create artifacts and sha256 hashtables with string keys
    $artifactsHash = @{}
    $sha256Hash = @{}
    $artifactsHash[$platforms[0].ToString()] = $zipFileName
    
    $depsArray = @()
    if($depNames.Count -gt 0) {
      $depsArray = $depNames | ForEach-Object { 
        @{ 
          name = [string]$_
          range = $null 
        } 
      }
    }
    
    $manifest = [ordered]@{
      name = [string]$pluginName
      id = [string]$pluginBase
      version = [string]$pluginVer
      engine = [string]$engineVer
      platforms = @([string]$platforms[0])
      artifacts = $artifactsHash
      url = [string]$zipFileName
      sha256 = $sha256Hash
      buildMethod = [string]$buildMethod
      timestamp = (Get-Date).ToString("o")
    }
    
    if($depsArray.Count -gt 0) {
      $manifest.dependencies = $depsArray
    }
    
    # Write manifest INSIDE the package (next to plugin folder)
    $packageManifestPath = Join-Path $tempPackageDir "manifest.json"
    Write-Json $manifest $packageManifestPath
    Write-Host "  Added manifest.json to package" -ForegroundColor Green
    
    # Create ZIP from temp directory
    Compress-Archive -Path "$tempPackageDir\*" -DestinationPath $zipPath -Force
    
    # Calculate hash and update external manifest
    $hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $zipPath).Hash.ToLower()
    $sha256Hash[$platforms[0].ToString()] = $hash
    $manifest.sha256 = $sha256Hash
    
    # Create external manifest file (for Nexus)
    $manifestPath = Join-Path $artifactsDir "${buildStamp}.manifest.json"
    Write-Json $manifest $manifestPath
    
  } finally {
    # Cleanup temp directory
    if(Test-Path $tempPackageDir) {
      Remove-Item -Recurse -Force $tempPackageDir -ErrorAction SilentlyContinue
    }
  }

  Write-Host ""
  Write-Host "=== PACKAGING COMPLETE ===" -ForegroundColor Green
  Write-Host "Build Method: $buildMethod" -ForegroundColor Cyan
  Write-Host "Package ZIP : $zipPath" -ForegroundColor Cyan  
  Write-Host "SHA256      : $hash" -ForegroundColor Cyan
  Write-Host "Manifest    : $manifestPath" -ForegroundColor Cyan
  Write-Host "Package Dir : $packageDir" -ForegroundColor Cyan
  Write-Host ""
  Write-Host "Plugin successfully built and packaged!" -ForegroundColor Green
  Write-Host "ZIP contains both the plugin and manifest.json" -ForegroundColor Yellow
  Write-Host ""
  Write-Host "To test locally:" -ForegroundColor Yellow
  Write-Host "  1. Copy both .zip and .manifest.json to a 'Nexus' folder" -ForegroundColor Gray
  Write-Host "  2. Use 'install' command to test dependency resolution" -ForegroundColor Gray
}

function Handle-Install-Command {
  Write-Host "=== UE Plugin Installer ===" -ForegroundColor Cyan
  
  $pluginsDir = Join-Path (Get-Location) "Plugins"
  $requirementsFile = Join-Path $pluginsDir "plugins.requirements.json"
  $lockFile = Join-Path $pluginsDir ".ueplugins.lock.json"
  
  if(-not (Test-Path $requirementsFile)) {
    Write-Host "No requirements file found." -ForegroundColor Red
    Write-Host "Run: .\UEPluginManager.ps1 init" -ForegroundColor Yellow
    return
  }
  
  $requirements = Read-Json $requirementsFile
  if(-not $requirements) {
    Write-Host "Could not read requirements file." -ForegroundColor Red
    return
  }
  
  if(-not $requirements.plugins -or $requirements.plugins.PSObject.Properties.Count -eq 0) {
    Write-Host "No plugins specified in requirements." -ForegroundColor Yellow
    Write-Host "Use: .\UEPluginManager.ps1 add PluginName@^1.0.0" -ForegroundColor Gray
    return
  }
  
  # Get local Nexus path
  $nexusPath = Get-Local-Nexus-Path
  if(-not $nexusPath) { return }
  
  # Scan available plugins
  Write-Host ""
  $availableManifests = Scan-Local-Nexus $nexusPath
  Write-Host "Found $($availableManifests.Keys.Count) plugins in local Nexus" -ForegroundColor Green
  
  # Resolve dependencies
  try {
    $resolved = Resolve-Plugin-Dependencies -requirements $requirements -availableManifests $availableManifests -targetEngine $requirements.engine
    
    Write-Host ""
    Write-Host "=== Resolution Summary ===" -ForegroundColor Cyan
    foreach($pluginName in ($resolved.Keys | Sort-Object)) {
      $manifest = $resolved[$pluginName]
      Write-Host "  $pluginName@$($manifest.version)" -ForegroundColor Green
    }
    
    # Install plugins
    Install-Resolved-Plugins -resolved $resolved -pluginsDir $pluginsDir
    
    # Create lockfile
    Create-Lockfile -resolved $resolved -requirements $requirements -lockFilePath $lockFile
    
    Write-Host ""
    Write-Host "=== INSTALLATION COMPLETE ===" -ForegroundColor Green
    Write-Host "Installed $($resolved.Keys.Count) plugins" -ForegroundColor Yellow
    
  } catch {
    Write-Host ""
    Write-Host "=== RESOLUTION FAILED ===" -ForegroundColor Red
    Write-Host $_.Exception.Message -ForegroundColor Red
  }
}

function Handle-List-Command {
  Write-Host "=== Plugin Status ===" -ForegroundColor Cyan
  
  $pluginsDir = Join-Path (Get-Location) "Plugins"
  $requirementsFile = Join-Path $pluginsDir "plugins.requirements.json"
  $lockFile = Join-Path $pluginsDir ".ueplugins.lock.json"
  
  # Show requirements
  if(Test-Path $requirementsFile) {
    $requirements = Read-Json $requirementsFile
    Write-Host ""
    Write-Host "Requirements (plugins.requirements.json):" -ForegroundColor Yellow
    Write-Host "  Engine: $($requirements.engine)" -ForegroundColor Gray
    if($requirements.plugins) {
      foreach($pluginName in $requirements.plugins.Keys) {
        Write-Host "  $pluginName@$($requirements.plugins.$pluginName)" -ForegroundColor Green
      }
    }
  } else {
    Write-Host ""
    Write-Host "No requirements file found" -ForegroundColor Red
  }
  
  # Show lockfile
  if(Test-Path $lockFile) {
    $lockData = Read-Json $lockFile
    Write-Host ""
    Write-Host "Locked versions (.ueplugins.lock.json):" -ForegroundColor Yellow
    if($lockData.resolved) {
      foreach($plugin in $lockData.resolved) {
        Write-Host "  $($plugin.name)@$($plugin.version)" -ForegroundColor Green
      }
    }
  } else {
    Write-Host ""
    Write-Host "No lockfile found" -ForegroundColor Red
  }
  
  # Show installed plugins
  if(Test-Path $pluginsDir) {
    $installedPlugins = Get-ChildItem -Path $pluginsDir -Directory | Where-Object { 
      Test-Path (Join-Path $_.FullName ($_.Name + ".uplugin"))
    }
    
    Write-Host ""
    Write-Host "Installed plugins:" -ForegroundColor Yellow
    if($installedPlugins.Count -gt 0) {
      foreach($plugin in $installedPlugins) {
        $upluginPath = Join-Path $plugin.FullName ($plugin.Name + ".uplugin")
        $pluginJson = Read-Json $upluginPath
        $version = if($pluginJson -and $pluginJson.VersionName) { $pluginJson.VersionName } else { "unknown" }
        Write-Host "  $($plugin.Name)@$version" -ForegroundColor Green
      }
    } else {
      Write-Host "  No plugins installed" -ForegroundColor Gray
    }
  }
  
  # Check local Nexus
  $nexusPath = Join-Path (Get-Location) "Nexus"
  if(Test-Path $nexusPath) {
    $availableManifests = Scan-Local-Nexus $nexusPath
    Write-Host ""
    Write-Host "Local Nexus ($nexusPath):" -ForegroundColor Yellow
    if($availableManifests.Keys.Count -gt 0) {
      foreach($pluginName in ($availableManifests.Keys | Sort-Object)) {
        $versions = $availableManifests[$pluginName] | ForEach-Object { "$($_.version) (UE$($_.engine))" }
        $versionList = $versions -join ', '
        Write-Host "  ${pluginName}: $versionList" -ForegroundColor Green
      }
    } else {
      Write-Host "  No plugins found" -ForegroundColor Gray
    }
  }
}

function Handle-Clean-Command {
  Write-Host "=== Clean Plugin Installation ===" -ForegroundColor Cyan
  
  $pluginsDir = Join-Path (Get-Location) "Plugins"
  $lockFile = Join-Path $pluginsDir ".ueplugins.lock.json"
  
  if(-not (Test-Path $pluginsDir)) {
    Write-Host "No Plugins directory found" -ForegroundColor Yellow
    return
  }
  
  $installedPlugins = Get-ChildItem -Path $pluginsDir -Directory | Where-Object { 
    Test-Path (Join-Path $_.FullName ($_.Name + ".uplugin"))
  }
  
  if($installedPlugins.Count -eq 0) {
    Write-Host "No installed plugins found" -ForegroundColor Yellow
    return
  }
  
  Write-Host ""
  Write-Host "Found $($installedPlugins.Count) installed plugins:" -ForegroundColor Yellow
  foreach($plugin in $installedPlugins) {
    Write-Host "  $($plugin.Name)" -ForegroundColor Gray
  }
  
  if(Ask-YesNo "Remove all installed plugins?" $false) {
    foreach($plugin in $installedPlugins) {
      Write-Host "Removing: $($plugin.Name)" -ForegroundColor Red
      Remove-Item -Recurse -Force $plugin.FullName
    }
    
    if(Test-Path $lockFile) {
      Write-Host "Removing lockfile: $lockFile" -ForegroundColor Red
      Remove-Item $lockFile
    }
    
    Write-Host ""
    Write-Host "Clean complete" -ForegroundColor Green
  } else {
    Write-Host "Clean cancelled" -ForegroundColor Yellow
  }
}

function Show-Help {
  Write-Host ""
  Write-Host "=== UE Plugin Manager ===" -ForegroundColor Cyan
  Write-Host ""
  Write-Host "Commands:" -ForegroundColor Yellow
  Write-Host "  package    - Package a plugin with dependencies (creates ZIP + manifest)" -ForegroundColor Green
  Write-Host "  init       - Initialize plugin project (creates plugins.requirements.json)" -ForegroundColor Green  
  Write-Host "  add        - Add plugin to requirements (e.g. add MyPlugin@^1.0.0)" -ForegroundColor Green
  Write-Host "  install    - Resolve dependencies and install plugins from local Nexus" -ForegroundColor Green
  Write-Host "  list       - Show current requirements, lockfile, and installed plugins" -ForegroundColor Green
  Write-Host "  clean      - Remove all installed plugins and lockfile" -ForegroundColor Green
  Write-Host ""
  Write-Host "Usage Examples:" -ForegroundColor Yellow
  Write-Host "  .\UEPluginManager.ps1            # Package (default - backward compatible)" -ForegroundColor Gray
  Write-Host "  .\UEPluginManager.ps1 package    # Package a plugin" -ForegroundColor Gray
  Write-Host "  .\UEPluginManager.ps1 init       # Initialize project" -ForegroundColor Gray
  Write-Host "  .\UEPluginManager.ps1 add MyPlugin@^1.0.0" -ForegroundColor Gray
  Write-Host "  .\UEPluginManager.ps1 install    # Install from requirements" -ForegroundColor Gray
  Write-Host "  .\UEPluginManager.ps1 list       # Show status" -ForegroundColor Gray
  Write-Host ""
  Write-Host "Local Nexus Setup:" -ForegroundColor Yellow
  Write-Host "  1. Create a 'Nexus' folder in your project" -ForegroundColor Gray
  Write-Host "  2. Place .zip and .manifest.json files from packaging in Nexus/" -ForegroundColor Gray
  Write-Host "  3. Run 'install' to resolve and download from local Nexus" -ForegroundColor Gray
}

# === MAIN EXECUTION ===

try {
  switch($Command.ToLower()) {
    "package" { Handle-Package-Command }
    "init" { Initialize-PluginProject }
    "add" { 
      $pluginSpec = if($args.Count -gt 0) { $args[0] } else { $null }
      Add-Plugin-Requirement $pluginSpec 
    }
    "install" { Handle-Install-Command }
    "list" { Handle-List-Command }
    "clean" { Handle-Clean-Command }
    "help" { Show-Help }
    default { 
      if($Command -eq "package") {
        Handle-Package-Command
      } else {
        Write-Host "Unknown command: $Command" -ForegroundColor Red
        Show-Help
      }
    }
  }
} catch {
  Write-Host ""
  Write-Host "=== ERROR ===" -ForegroundColor Red
  Write-Host $_.Exception.Message -ForegroundColor Red
  Write-Host ""
}
Editor is loading...
Leave a Comment