Build/New-PublishStaging.ps1

function New-PublishStaging {
    <#
    .SYNOPSIS
    Builds the folder Publish-Module packs: a clean copy of the module, never a secret.

    .DESCRIPTION
    Publish-Module packs every file of the folder it is given into the public package. The module
    folder is also where a developer keeps .env (NUGET_API_KEY, AI provider keys), so publishing the
    folder itself, or a plain copy of it, would ship those keys to the PowerShell Gallery.

    The staging folder holds only the files git tracks in the module folder (working-tree content,
    so an uncommitted version bump is included). .env, test results and other untracked or ignored
    files never get in. Outside a git checkout every file is copied. Either way, files named like a
    secret (.env, .env.*, *.apikey, *.secret, secrets.json; .env.example excepted) are removed, and
    the function throws if one is still there.

    The staging folder is named after the module, as Publish-Module requires. The caller removes
    its parent folder (Remove-PublishStaging) after publishing.

    .OUTPUTS
    System.String - full path of the staging folder (<temp>/aicr-publish-<guid>/<ModuleName>)
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$ModuleRoot,
        [Parameter(Mandatory)][string]$ModuleName
    )

    $secretPattern = '^(\.env(\..+)?|.+\.apikey|.+\.secret|secrets\.json)$'
    $allowed = @('.env.example')

    $stagingRoot = Join-Path ([System.IO.Path]::GetTempPath()) "aicr-publish-$([guid]::NewGuid().ToString('N'))"
    $staging = Join-Path $stagingRoot $ModuleName
    New-Item -ItemType Directory -Path $staging -Force | Out-Null

    $tracked = @(git -C $ModuleRoot -c core.quotepath=off ls-files --cached -- . 2>$null)
    if ($LASTEXITCODE -eq 0 -and $tracked.Count -gt 0) {
        foreach ($relative in $tracked) {
            $source = Join-Path $ModuleRoot $relative
            if (-not (Test-Path -LiteralPath $source -PathType Leaf)) { continue }   # deleted, not committed
            $target = Join-Path $staging $relative
            New-Item -ItemType Directory -Path (Split-Path $target -Parent) -Force | Out-Null
            Copy-Item -LiteralPath $source -Destination $target -Force
        }
        Write-Host " ✓ Staged $($tracked.Count) git-tracked file(s) (untracked and ignored files, such as .env, stay out)" -ForegroundColor Green
    } else {
        Get-ChildItem -LiteralPath $ModuleRoot -Force | Copy-Item -Destination $staging -Recurse -Force
        Write-Host " ⚠️ Not a git checkout: staged every file of $ModuleRoot" -ForegroundColor Yellow
    }

    $secrets = @(Get-ChildItem -LiteralPath $staging -Recurse -Force -File |
        Where-Object { $_.Name -match $secretPattern -and $_.Name -notin $allowed })
    foreach ($file in $secrets) {
        Write-Host " ✓ Left out of the package: $($file.FullName.Substring($staging.Length + 1))" -ForegroundColor Yellow
        Remove-Item -LiteralPath $file.FullName -Force
    }
    $left = @(Get-ChildItem -LiteralPath $staging -Recurse -Force -File |
        Where-Object { $_.Name -match $secretPattern -and $_.Name -notin $allowed })
    if ($left.Count -gt 0) {
        Remove-Item -LiteralPath $stagingRoot -Recurse -Force -ErrorAction SilentlyContinue
        throw "Refusing to publish: secret-like file(s) still in the staging folder: $($left.Name -join ', ')"
    }

    return $staging
}

function Remove-PublishStaging {
    [CmdletBinding()]
    param([string]$StagingPath)
    if (-not $StagingPath) { return }
    $root = Split-Path $StagingPath -Parent
    if ($root -and (Split-Path $root -Leaf) -like 'aicr-publish-*' -and (Test-Path -LiteralPath $root)) {
        Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
    }
}