Public/Optimize-FoFile.ps1

function Optimize-FoFile {
    <#
    .SYNOPSIS
    Optimizes one or more files using FileOptimizer plugin chains.
 
    .DESCRIPTION
    Resolves settings, selects pipeline groups per file extension, runs the plugin
    chain, and optionally records history. Supports -WhatIf for dry-run output.
 
    .PARAMETER Path
    File or directory paths to optimize. Directories are not expanded unless -Recurse is set.
 
    .PARAMETER ConfigPath
    Optional local JSON config file merged after global config.
 
    .PARAMETER ContinueOnError
    When optimizing multiple files, record per-file errors and continue instead of stopping the batch.
 
    .PARAMETER Level
    Optimization level (0–9). Default from config.
 
    .PARAMETER PluginSearchMode
    How to resolve plugin executables: PortableFirst, PathFirst, PortableOnly, or PathOnly.
 
    .PARAMETER PluginPath
    Portable plugin directory (Plugins64/Plugins32).
 
    .PARAMETER OutputMode
    TempMove, Replace, OptimizedSuffix, BackupSuffix, or BackupMove.
 
    .PARAMETER ShowProgress
    Show per-step progress during optimization.
 
    .PARAMETER Recurse
    When Path is a directory, include files in subdirectories.
 
    .PARAMETER MissingToolsPolicy
    How to handle required plugin tools that are not found: Error (default),
    SkipTool (skip those steps), or SkipFile (skip the whole file).
 
    .PARAMETER AcknowledgeOutdatedPlugins
    Persist acknowledgment of the current minimum plugin-bundle version and continue
    with a warning when the installed bundle is older than required.
 
    .EXAMPLE
    Optimize-FoFile -Path .\images\photo.png
 
    .EXAMPLE
    Optimize-FoFile -Path .\docs -Recurse -WhatIf
 
    .EXAMPLE
    .\Scripts\Optimize-File.ps1 .\images\*.png
    # CLI wrapper with the same parameters.
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Optimize')]
    [OutputType([object[]])]
    param(
        [Parameter(ParameterSetName = 'Optimize', ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('FullName', 'PSPath')]
        [string[]]$Path,
        [string]$ConfigPath,
        [ValidateRange(0, 9)]
        [nullable[int]]$Level,
        [ValidateSet('PortableFirst', 'PathFirst', 'PortableOnly', 'PathOnly')]
        [string]$PluginSearchMode,
        [string]$PluginPath,
        [ValidateRange(0, 3)]
        [nullable[int]]$LogLevel,
        [ValidateRange(0, 3)]
        [nullable[int]]$ReportLogLevel,
        [string]$ReportPath,
        [ValidateSet('Replace', 'OptimizedSuffix', 'BackupSuffix', 'BackupMove', 'TempMove')]
        [string]$OutputMode,
        [string]$BackupPath,
        [string]$BackupSuffix,
        [string]$OptimizedSuffix,
        [string]$TempBackupPath,
        [nullable[bool]]$HistoryEnabled,
        [string]$HistoryPath,
        [ValidateSet('Error', 'SkipTool', 'SkipFile')]
        [string]$MissingToolsPolicy,
        [nullable[bool]]$SkipMissingTools,
        [switch]$ShowProgress,
        [switch]$Recurse,
        [switch]$ContinueOnError,
        [switch]$AcknowledgeOutdatedPlugins
    )

    begin {
        $script:FoBatchResults = [System.Collections.Generic.List[object]]::new()
        $script:FoBatchSettings = $null
    }

    process {
        if (-not $Path) { return }

        if ($AcknowledgeOutdatedPlugins) {
            $min = Get-FoMinimumPluginBundleVersion
            $ackConfig = if ($ConfigPath) { $ConfigPath } else { $null }
            Set-FoAcknowledgedPluginBundleMinimum -MinimumVersion $min -ConfigPath $ackConfig
        }

        $settings = Merge-FoSettings -BoundParameters $PSBoundParameters
        if ($AcknowledgeOutdatedPlugins) {
            $settings.AcknowledgedPluginBundleMinimum = Get-FoMinimumPluginBundleVersion
        }
        $script:FoBatchSettings = $settings
        $targets = Get-FoTargetFiles -Path $Path -Recurse:$Recurse

        foreach ($file in $targets) {
            $gate = Test-FoFileGate -Path $file -Settings $settings
            if (-not $gate.Pass) {
                Write-FoLog -LogLevel $settings.LogLevel -RequiredLevel 1 -Message "Skipped $file ($($gate.Reason))"
                $script:FoBatchResults.Add([PSCustomObject]@{ Path = $file; Status = 'Skipped'; Reason = $gate.Reason })
                continue
            }

            if ((Get-Item -LiteralPath $file).Length -eq 0) {
                Write-FoLog -LogLevel $settings.LogLevel -RequiredLevel 1 -Message "Skipped $file (zero-byte file)"
                $script:FoBatchResults.Add([PSCustomObject]@{ Path = $file; Status = 'Skipped'; Reason = 'ZeroByte' })
                continue
            }

            $groups = Get-FoPipelineGroupsForFile -Path $file -Settings $settings
            if ($groups.Count -eq 0) {
                Write-FoLog -LogLevel $settings.LogLevel -RequiredLevel 1 -Message "Skipped $file (unsupported extension)"
                $script:FoBatchResults.Add([PSCustomObject]@{ Path = $file; Status = 'Skipped'; Reason = 'Unsupported' })
                continue
            }

            if ($PSCmdlet.ShouldProcess($file, 'Optimize file')) {
                try {
                    $result = Invoke-FoPluginChain -Path $file -Settings $settings -ShowProgress:$ShowProgress -Confirm:$false
                    if ($result.Status -eq 'Optimized') {
                        if ($settings.LogLevel -ge 1) {
                            Write-Host ('Optimized {0}: {1} -> {2} (-{3}%)' -f $file, (Format-FoFileSize $result.OriginalSize), (Format-FoFileSize $result.FinalSize), $result.PercentSaved)
                        }
                        if ($settings.HistoryEnabled) {
                            Add-FoHistoryEntry -Result $result -Settings $settings
                        }
                    }
                    elseif ($result.Status -eq 'Unchanged' -and $settings.LogLevel -ge 1) {
                        if ($result.Reason -eq 'MissingTools') {
                            $missing = @($result.Missing)
                            $hint = if ($missing.Count -gt 0) {
                                "missing tools: $($missing -join ', ')"
                            }
                            else {
                                'no plugin tools available'
                            }
                            Write-Host ("Unchanged {0}: {1} ({2}; check PluginPath or run Install-FoPlugins)" -f $file, (Format-FoFileSize $result.OriginalSize), $hint)
                        }
                        else {
                            Write-Host "Unchanged $file`: $(Format-FoFileSize $result.OriginalSize) (already optimal)"
                        }
                    }
                    $script:FoBatchResults.Add($result)
                }
                catch {
                    if ($ContinueOnError) {
                        Write-FoLog -LogLevel $settings.LogLevel -RequiredLevel 1 -Message "Error optimizing ${file}: $($_.Exception.Message)"
                    }
                    else {
                        Write-Error $_
                    }
                    $script:FoBatchResults.Add([PSCustomObject]@{ Path = $file; Status = 'Error'; Reason = $_.Exception.Message })
                    if (-not $ContinueOnError) { throw }
                }
            }
            elseif ($WhatIfPreference) {
                $result = Invoke-FoPluginChain -Path $file -Settings $settings -ShowProgress:$false -Confirm:$false
                $script:FoBatchResults.Add($result)
            }
        }
    }

    end {
        if ($script:FoBatchSettings -and $script:FoBatchSettings.ReportPath -and $script:FoBatchResults.Count -gt 0) {
            Write-FoReport -Results @($script:FoBatchResults) -Settings $script:FoBatchSettings -ReportPath $script:FoBatchSettings.ReportPath
        }
        return @($script:FoBatchResults)
    }
}