Private/Engine/DryRunEngine.ps1

<#
.SYNOPSIS
    Shared, tool-agnostic DRY RUN engine - extracted from CA-Manager's own Modules\CACore.ps1
    (2026-09-10, Part A item 9: "this is a great piece of functionality to have. Modularize it?").
    Every mutating action in a tool that dot-sources this file routes through Invoke-CAStep, which
    just prints the plan and does nothing while $script:CADryRun is $true - so an entire wizard can
    be walked end-to-end on any machine with zero side effects, then re-run for real once reviewed.
 
.DESCRIPTION
    Deliberately just MOVED here, not renamed - Invoke-CAStep/Get-CADryRun/Set-CADryRun keep their
    CA-prefixed names (a cosmetic wart for a "shared" file, not a functional one) so every existing
    CA-Manager call site across every Modules\CA*.ps1 file and every test keeps working completely
    unchanged; only WHERE the three functions live moved, not their names or behavior. Renaming them
    to something tool-neutral is real, separate future work if/when NPS Manager (which has no
    dry-run today) or a future GPO Manager actually adopts this file - not done here to keep this
    extraction itself a zero-ripple, purely mechanical move.
 
    CACore.ps1 dot-sources this file (see its own header) rather than defining these three functions
    itself now - same "locate one of a couple of candidate paths" pattern already proven by
    Modules\CATestSuite.ps1's own Request-VPNCertCore.ps1 lookup: a zip-layout candidate (this file
    bundled next to CA-Manager.ps1 by Build-CAManagerZip.ps1) and a repo-layout candidate (this file
    read directly from IPSEC AIO\MiscTools\Shared\ while developing/testing in the repo).
#>


# ---------------------------------------------------------------------------
# DRY RUN - every mutating engine (CAInstall / CATemplates / CAAutoEnrollGPO / CAUrls / ...) runs its
# real work through Invoke-CAStep. When $script:CADryRun is $true it prints exactly what it WOULD do
# and runs nothing; otherwise it prints the same "would do" summary, runs the action, and reports the
# outcome. The dashboard sets $script:CADryRun from a top-of-session prompt (and lets a tech toggle
# it), so every wizard can be walked end-to-end on any machine with zero side effects.
# ---------------------------------------------------------------------------
$script:CADryRun = $true

function Set-CADryRun {
    param([Parameter(Mandatory)][bool]$Enabled)
    $script:CADryRun = $Enabled
}
function Get-CADryRun { return [bool]$script:CADryRun }

function Invoke-CAStep {
    <#
    .SYNOPSIS
        Runs one mutating step, or (in dry-run) just describes it. Returns a result object:
        [pscustomobject]@{ Ran = <bool>; Description; Commands = @(...); Output; Error }.
 
    .PARAMETER Description
        One-line human summary ("Publish the auto-enroll GPO 'NSP - Certificate Auto-Enrollment'").
 
    .PARAMETER Commands
        The literal command(s) this step would run, as strings, for the dry-run preview and the
        transcript. Illustrative - not necessarily re-executable verbatim.
 
    .PARAMETER Action
        Scriptblock that performs the real work. Only invoked when not in dry-run. Its output is
        captured into .Output; a throw is caught into .Error and (unless -ContinueOnError) re-thrown.
    #>

    param(
        [Parameter(Mandatory)][string]$Description,
        [string[]]$Commands = @(),
        [Parameter(Mandatory)][scriptblock]$Action,
        [switch]$ContinueOnError
    )

    $dry = Get-CADryRun
    Write-Host ""
    Write-Host (" {0} {1}" -f $(if ($dry) { '[DRY RUN]' } else { '[APPLY] ' }), $Description) -ForegroundColor $(if ($dry) { 'Cyan' } else { 'Yellow' })
    foreach ($c in $Commands) { Write-Host " $c" -ForegroundColor DarkGray }

    $res = [pscustomobject]@{ Ran = $false; Description = $Description; Commands = $Commands; Output = $null; Error = $null }
    if ($dry) { return $res }

    try {
        $res.Output = & $Action
        $res.Ran = $true
        Write-Host " -> done" -ForegroundColor Green
    } catch {
        $res.Error = $_.Exception.Message
        Write-Host " -> ERROR: $($res.Error)" -ForegroundColor Red
        if (-not $ContinueOnError) { throw }
    }
    return $res
}