Gallery发布.ps1

<#PSScriptInfo
.VERSION 1.0.0
.GUID 7e38b9f1-8c62-4a53-9e14-d8c7f2a6b10d
.AUTHOR 埃博拉酱-机器人
.COMPANYNAME 一致行动党
.COPYRIGHT (c) 2026 一致行动党。MIT 许可证。
.TAGS PSGallery publish api key secure encrypt DPAPI PowerShell 发布 密钥 加密
.LICENSEURI https://opensource.org/licenses/MIT
.EXTERNALMODULEDEPENDENCIES Microsoft.PowerShell.PSResourceGet
.RELEASENOTES 初始版本。首次输入 API 密钥后自动加密保存(Windows DPAPI,仅本机当前用户可解密),以后无需输入。
#>


<#
.SYNOPSIS
    将 PowerShell 模块发布到 PowerShell Gallery,首次输入 API 密钥后自动加密保存,后续无需再次输入。
.DESCRIPTION
    使用 Export-Clixml 将 PSGallery API 密钥加密保存到本地文件(Windows DPAPI,仅本机当前用户可解密)。
    首次运行提示输入密钥并保存,后续运行自动读取,实现一键发布。密钥文件会自动加入 .gitignore。
.PARAMETER 模块路径
    要发布的模块所在文件夹路径(或 .psd1 清单文件路径)。默认在当前工作目录下查找。
.PARAMETER 自身
    指定此开关时,将 Gallery发布 脚本自身发布到 PowerShell Gallery。
.EXAMPLE
    .\Gallery发布.ps1
    在当前目录下查找模块并发布到 PSGallery。
.EXAMPLE
    .\Gallery发布.ps1 -模块路径 ".\GitHub加速"
    发布 GitHub加速 模块到 PSGallery。
.EXAMPLE
    .\Gallery发布.ps1 -自身
    将 Gallery发布 脚本自身发布到 PSGallery。
#>


[CmdletBinding(DefaultParameterSetName = "模块")]
param(
    [Parameter(Position = 0, ParameterSetName = "模块")]
    [string]$模块路径 = ".",

    [Parameter(Mandatory = $true, ParameterSetName = "自身")]
    [switch]$自身
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

# ============================================================
# 0. 确定要发布的对象和密钥文件位置
# ============================================================
if ($自身) {
    # 发布自身脚本
    $脚本文件 = $MyInvocation.MyCommand.Path
    if (-not $脚本文件) {
        throw "无法确定脚本文件路径。"
    }
    $脚本文件 = Resolve-Path $脚本文件
    $密钥目录 = Split-Path -Parent $脚本文件
    $发布目标 = "脚本"  # 标记,后续分支处理
}
else {
    # 发布模块
    if (Test-Path -LiteralPath $模块路径 -PathType Container) {
        $模块文件夹 = Resolve-Path $模块路径
    }
    elseif (Test-Path -LiteralPath $模块路径 -PathType Leaf) {
        $模块文件夹 = Split-Path -Parent (Resolve-Path $模块路径)
    }
    else {
        throw "模块路径不存在:$模块路径"
    }

    $清单文件 = Get-ChildItem -LiteralPath $模块文件夹 -Filter "*.psd1" | Select-Object -First 1
    if (-not $清单文件) {
        throw "在 $模块文件夹 中未找到 .psd1 清单文件。"
    }

    $模块信息 = Test-ModuleManifest -Path $清单文件.FullName
    $模块名称 = $模块信息.Name
    $版本 = $模块信息.Version
    $密钥目录 = $模块文件夹
    $发布目标 = "模块"
}

$密钥文件 = Join-Path $密钥目录 ".apikey"

# ============================================================
# 0a. 确保 .apikey 在 .gitignore 中
# ============================================================
$Git目录 = $密钥目录
$Git忽略文件 = $null
while ($Git目录 -and (Split-Path -Parent $Git目录) -ne $Git目录) {
    if (Test-Path -LiteralPath (Join-Path $Git目录 ".git")) {
        $Git忽略文件 = Join-Path $Git目录 ".gitignore"
        break
    }
    $Git目录 = Split-Path -Parent $Git目录
}

if ($Git忽略文件) {
    $已忽略 = $false
    if (Test-Path -LiteralPath $Git忽略文件) {
        $已有行 = Get-Content -LiteralPath $Git忽略文件 | ForEach-Object { $_.Trim() }
        $已忽略 = $已有行 -contains ".apikey"
    }
    if (-not $已忽略) {
        Add-Content -LiteralPath $Git忽略文件 -Value ".apikey"
        Write-Host "已将 .apikey 加入 $Git忽略文件" -ForegroundColor DarkGray
    }
}

# ============================================================
# 1. 尝试从加密文件读取 API 密钥
# ============================================================
$ApiKey = $null
if (Test-Path -LiteralPath $密钥文件) {
    try {
        $安全串 = Import-Clixml -LiteralPath $密钥文件
        $ApiKey = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
            [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($安全串)
        )
        if (-not $ApiKey) { throw "解密结果为空" }
        Write-Host "已从加密文件读取 API 密钥。" -ForegroundColor Green
    }
    catch {
        Write-Host "加密文件损坏或无法解密,将重新获取。($($_.Exception.Message))" -ForegroundColor Yellow
    }
}

# ============================================================
# 2. 若无密钥则交互输入
# ============================================================
if (-not $ApiKey) {
    Write-Host "请输入 PowerShell Gallery API 密钥:" -ForegroundColor Yellow
    Write-Host " (获取方式:PowerShell Gallery 右上角 → API Keys → Create)" -ForegroundColor DarkGray
    $安全密钥 = Read-Host -AsSecureString
    $ApiKey = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
        [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($安全密钥)
    )
    if (-not $ApiKey) {
        throw "未提供 API 密钥,无法发布。"
    }

    # 用 Export-Clixml 保存,原生支持 SecureString DPAPI 加密
    $安全密钥 | Export-Clixml -LiteralPath $密钥文件
    Write-Host "已加密保存(仅本机当前用户可解密)。" -ForegroundColor Green
}

# ============================================================
# 3. 确保 PSResourceGet 已安装
# ============================================================
$新发布模块 = Get-Module -ListAvailable Microsoft.PowerShell.PSResourceGet
if (-not $新发布模块) {
    Write-Host "正在安装 Microsoft.PowerShell.PSResourceGet ..." -ForegroundColor Yellow
    Install-Module Microsoft.PowerShell.PSResourceGet -Repository PSGallery -Scope CurrentUser -Force
}

# ============================================================
# 4. 发布
# ============================================================
if ($自身) {
    Write-Host "正在发布 Gallery发布 脚本到 PSGallery ..."
    Publish-PSResource -Path $脚本文件 -ApiKey $ApiKey -Repository PSGallery
}
else {
    Write-Host "准备发布 $模块名称 v$版本 ..."
    Write-Host "正在发布到 PSGallery ..."
    Publish-PSResource -Path $模块文件夹 -ApiKey $ApiKey -Repository PSGallery
}

Write-Host "发布完成!" -ForegroundColor Green