src/Engine/Entry.ps1

# Entry.ps1 — raw JSON → validated module entry, and file-level parsing.

# Normalise one raw module entry into the rich object the rest of psmm uses.
# Invalid Install/Mode values degrade to the defaults with an issue recorded —
# a broken entry must never take the whole config file down.
function Resolve-PSMMEntry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] $Raw,
        [Parameter(Mandatory)][string]$Source,
        [Parameter(Mandatory)][bool]$Writable
    )

    $validInstall = 'CheckOnly', 'IfMissing', 'Latest'
    $validMode    = 'Load', 'InstallOnly', 'Ignore'
    $issues       = [System.Collections.Generic.List[string]]::new()

    $name = [string]$Raw.Name
    if ([string]::IsNullOrWhiteSpace($name)) { $issues.Add('Missing Name') }

    $install = if ($Raw.PSObject.Properties['Install'] -and $Raw.Install) { [string]$Raw.Install } else { 'IfMissing' }
    if ($install -notin $validInstall) { $issues.Add("Invalid Install '$install' (using IfMissing)"); $install = 'IfMissing' }

    $mode = if ($Raw.PSObject.Properties['Mode'] -and $Raw.Mode) { [string]$Raw.Mode } else { 'Load' }
    if ($mode -notin $validMode) { $issues.Add("Invalid Mode '$mode' (using Load)"); $mode = 'Load' }

    # Optional version pin: exact ("1.2.3") or NuGet range ("[1.0,2.0)").
    $version = $null
    if ($Raw.PSObject.Properties['Version'] -and $Raw.Version) { $version = ([string]$Raw.Version).Trim() }
    $pinnedExact = $false
    if ($version) {
        if ($version -match '^\d+(\.\d+){1,3}$') {
            $pinnedExact = $true
        } elseif ($version -notmatch '^[\[\(][0-9\.\,\s\*\-A-Za-z]*[\]\)]$') {
            $issues.Add("Invalid Version '$version' (ignoring pin)")
            $version = $null
        }
    }

    [pscustomobject]@{
        Name              = $name
        FriendlyName      = if ($Raw.FriendlyName) { [string]$Raw.FriendlyName } else { $name }
        Description       = [string]$Raw.Description
        Install           = $install
        Mode              = $mode
        Version           = $version
        PinnedExact       = $pinnedExact
        Source            = $Source
        Writable          = $Writable
        Issues            = $issues.ToArray()
        Installed         = $false
        InstalledVersion  = $null
        InstalledVersions = @()      # every installed version (duplicate-cleanup feature)
        InstallScope      = $null    # CurrentUser | AllUsers | mixed | $null (unknown)
        Loaded            = $false
        LoadedVersion     = $null
        LatestVersion     = $null
        UpdateAvailable   = $false
        ImportMs          = $null    # measured import duration (startup / UI load)
    }
}

# Parse one config file's JSON into its file-level shape. Throws on bad JSON —
# the caller records the warning and skips the file.
function ConvertFrom-PSMMJson {
    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$Json)
    $obj = $Json | ConvertFrom-Json -ErrorAction Stop
    [pscustomobject]@{
        Legend     = $obj._legend
        HasEnabled = [bool]$obj.PSObject.Properties['Enabled']
        Enabled    = if ($obj.PSObject.Properties['Enabled'] -and $null -ne $obj.Enabled) { [bool]$obj.Enabled } else { $true }
        Includes   = if ($obj.PSObject.Properties['Includes']) { @($obj.Includes | Where-Object { $_ }) } else { @() }
        Modules    = @($obj.Modules)
    }
}