Commit e559870b43bc9bfe8a9b412ffb8d23c1ee6039e4

Authored by zichun
1 parent cce98737

feat(downstream-gen): 新增 derive-gitlab.ps1,Windows 用户零外部依赖

PS 5.1+ 系统自带,用 Invoke-WebRequest + .NET 字符串处理替代 curl/jq/sed。
与 .sh 严格等价;SKILL.md 结束横幅按平台二选一打印调用命令。
skills/plan/downstream-gen/SKILL.md
... ... @@ -139,8 +139,11 @@ cp "${CLAUDE_SKILL_DIR}/templates/docs-10-header-template.md" docs/10-验收检
139 139 4. 补齐 `.env.local` 三个 GITLAB_* 字段
140 140  
141 141 - `GITLAB_TOKEN`:去 GitLab Profile → Account → Private token 生成后粘贴
142   - - `GITLAB_PROJECT_ID`,`GITLAB_API_URL`:运行
143   - bash <plugin-skill-dir>/scripts/derive-gitlab.sh
  142 + - `GITLAB_PROJECT_ID`,`GITLAB_API_URL`:按平台运行(脚本只依赖 git)
  143 + macOS / Linux:
  144 + bash <plugin-skill-dir>/scripts/derive-gitlab.sh
  145 + Windows (PowerShell 5.1+,系统自带,无需额外安装):
  146 + powershell -NoProfile -ExecutionPolicy Bypass -File <plugin-skill-dir>/scripts/derive-gitlab.ps1
144 147  
145 148 5. remote git 就绪后,再运行 /erp-workflow:coding-start
146 149 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
... ... @@ -154,4 +157,4 @@ cp &quot;${CLAUDE_SKILL_DIR}/templates/docs-10-header-template.md&quot; docs/10-验收检
154 157 - `${CLAUDE_SKILL_DIR}/templates/docs-06-module-pagelist-template.md`(追加到 docs/06 § 三)
155 158 - `${CLAUDE_SKILL_DIR}/templates/docs-08-module-row-template.md`(模块 bullet 行模板)
156 159 - `${CLAUDE_SKILL_DIR}/templates/docs-10-header-template.md`
157   -- `${CLAUDE_SKILL_DIR}/scripts/derive-gitlab.sh`(**用户可选辅助**:在 add origin 之后手动跑一次,自动派生 GITLAB_PROJECT_ID / GITLAB_API_URL 写入 .env.local)
  160 +- `${CLAUDE_SKILL_DIR}/scripts/derive-gitlab.sh` / `derive-gitlab.ps1`(**用户可选辅助**:在 add origin 之后手动跑一次,自动派生 GITLAB_PROJECT_ID / GITLAB_API_URL 写入 .env.local;`.sh` 给 macOS/Linux,`.ps1` 给 Windows,两份严格等价 — 改动必须同步双方)
... ...
skills/plan/downstream-gen/scripts/derive-gitlab.ps1 0 → 100644
  1 +#!/usr/bin/env pwsh
  2 +# derive-gitlab.ps1 — Windows 对应版(PowerShell 5.1+ 原生可跑,无外部依赖)
  3 +# 与同目录 derive-gitlab.sh 严格等价 — 改动任一份必须同步对方。
  4 +#
  5 +# 用法: powershell -NoProfile -ExecutionPolicy Bypass -File derive-gitlab.ps1 [.env.local 路径]
  6 +#
  7 +# 派生字段、退出码、回填策略 见 derive-gitlab.sh 头部注释。
  8 +
  9 +[CmdletBinding()]
  10 +param([string]$EnvFile = '.env.local')
  11 +
  12 +# 让 Chinese 输出在 cmd / PowerShell 控制台正常显示
  13 +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
  14 +
  15 +if (-not (Test-Path -LiteralPath $EnvFile -PathType Leaf)) {
  16 + [Console]::Error.WriteLine("derive-gitlab.ps1: env file not found: $EnvFile")
  17 + exit 2
  18 +}
  19 +
  20 +# ---- 取 origin URL ----
  21 +$url = ''
  22 +try {
  23 + $raw = & git remote get-url origin 2>$null
  24 + if ($LASTEXITCODE -eq 0 -and $raw) { $url = ($raw -join '').Trim() }
  25 +} catch { $url = '' }
  26 +
  27 +if ([string]::IsNullOrEmpty($url)) {
  28 + [Console]::Error.WriteLine("derive-gitlab.ps1: 未配置 origin 远程,GITLAB_* 留给用户手填")
  29 + exit 0
  30 +}
  31 +
  32 +# ---- 仅支持 http(s) ----
  33 +$scheme = $null
  34 +if ($url.StartsWith('https://')) { $scheme = 'https' }
  35 +elseif ($url.StartsWith('http://')) { $scheme = 'http' }
  36 +else {
  37 + [Console]::Error.WriteLine("derive-gitlab.ps1: origin 不是 http(s) URL ($url),跳过派生")
  38 + exit 0
  39 +}
  40 +
  41 +$rest = $url.Substring("${scheme}://".Length)
  42 +$slashIdx = $rest.IndexOf('/')
  43 +if ($slashIdx -lt 0) {
  44 + [Console]::Error.WriteLine("derive-gitlab.ps1: origin URL 缺少路径段 ($url),跳过派生")
  45 + exit 0
  46 +}
  47 +$gitlabHost = $rest.Substring(0, $slashIdx)
  48 +$pathRaw = $rest.Substring($slashIdx + 1)
  49 +if ($pathRaw.EndsWith('.git')) { $pathRaw = $pathRaw.Substring(0, $pathRaw.Length - 4) }
  50 +$repoName = Split-Path -Leaf $pathRaw
  51 +
  52 +$apiUrl = "${scheme}://${gitlabHost}/api/v3"
  53 +
  54 +# ---- 读 .env.local 全文,按原行尾切分 ----
  55 +$rawBytes = [System.IO.File]::ReadAllBytes($EnvFile)
  56 +$hasBom = ($rawBytes.Length -ge 3 -and $rawBytes[0] -eq 0xEF -and $rawBytes[1] -eq 0xBB -and $rawBytes[2] -eq 0xBF)
  57 +$rawText = if ($hasBom) {
  58 + [System.Text.Encoding]::UTF8.GetString($rawBytes, 3, $rawBytes.Length - 3)
  59 +} else {
  60 + [System.Text.Encoding]::UTF8.GetString($rawBytes)
  61 +}
  62 +$eol = if ($rawText -match "`r`n") { "`r`n" } else { "`n" }
  63 +$hadTrailingNewline = $rawText.EndsWith("`n")
  64 +$lines = [System.Collections.Generic.List[string]]::new()
  65 +foreach ($l in ($rawText -split "`r?`n")) { $lines.Add($l) | Out-Null }
  66 +if ($hadTrailingNewline -and $lines.Count -gt 0 -and $lines[$lines.Count - 1] -eq '') {
  67 + $lines.RemoveAt($lines.Count - 1)
  68 +}
  69 +
  70 +function Get-FieldStripped {
  71 + param([string]$Key)
  72 + for ($i = 0; $i -lt $script:lines.Count; $i++) {
  73 + $ln = $script:lines[$i]
  74 + if ($ln.StartsWith("$Key=")) {
  75 + $val = $ln.Substring($Key.Length + 1)
  76 + # 剥外层单/双引号
  77 + if ($val.Length -ge 2) {
  78 + $f = $val[0]; $l = $val[$val.Length - 1]
  79 + if (($f -eq "'" -and $l -eq "'") -or ($f -eq '"' -and $l -eq '"')) {
  80 + $val = $val.Substring(1, $val.Length - 2)
  81 + }
  82 + }
  83 + return @{ Found = $true; Index = $i; Stripped = $val }
  84 + }
  85 + }
  86 + return @{ Found = $false }
  87 +}
  88 +
  89 +function Update-Field {
  90 + param([string]$Key, [string]$NewVal)
  91 + $info = Get-FieldStripped -Key $Key
  92 + if (-not $info.Found) {
  93 + [Console]::Error.WriteLine(" $Key = (.env.local 中无此行,跳过)")
  94 + return
  95 + }
  96 + $stripped = $info.Stripped
  97 + if ([string]::IsNullOrEmpty($stripped) -or $stripped -eq 'TBD(A5 自动补)') {
  98 + $script:lines[$info.Index] = "$Key=$NewVal"
  99 + $script:modified = $true
  100 + Write-Host " $Key = $NewVal [已派生填入]"
  101 + } elseif ($stripped -eq $NewVal) {
  102 + Write-Host " $Key = $NewVal [已是派生值,无需更新]"
  103 + } else {
  104 + Write-Host " $Key = $stripped [保留用户手填,未覆盖派生值 $NewVal]"
  105 + }
  106 +}
  107 +
  108 +function Report-Token {
  109 + $info = Get-FieldStripped -Key 'GITLAB_TOKEN'
  110 + if (-not $info.Found) {
  111 + [Console]::Error.WriteLine(" GITLAB_TOKEN = (.env.local 中无此行,跳过)")
  112 + return
  113 + }
  114 + $stripped = $info.Stripped
  115 + if ([string]::IsNullOrEmpty($stripped) -or $stripped.StartsWith('【人工填写:') -or $stripped.StartsWith('TBD')) {
  116 + Write-Host " GITLAB_TOKEN = $stripped [待人工填写:GitLab Profile → Account → Private token]"
  117 + } else {
  118 + $masked = if ($stripped.Length -le 8) {
  119 + ('*' * $stripped.Length)
  120 + } else {
  121 + $stripped.Substring(0, 4) + ('*' * ($stripped.Length - 8)) + $stripped.Substring($stripped.Length - 4)
  122 + }
  123 + Write-Host " GITLAB_TOKEN = $masked [已填入,长度 $($stripped.Length)]"
  124 + }
  125 +}
  126 +
  127 +$script:modified = $false
  128 +
  129 +Write-Host "derive-gitlab.ps1: 从 origin ($url) 派生 GitLab 凭据:"
  130 +Update-Field -Key 'GITLAB_API_URL' -NewVal $apiUrl
  131 +
  132 +# ---- GITLAB_PROJECT_ID:经 GitLab API 解析数字 ID ----
  133 +$tokenInfo = Get-FieldStripped -Key 'GITLAB_TOKEN'
  134 +$tokenVal = if ($tokenInfo.Found) { $tokenInfo.Stripped } else { '' }
  135 +
  136 +$tokenUsable = $true
  137 +if ([string]::IsNullOrEmpty($tokenVal) -or $tokenVal.StartsWith('【人工填写:') -or $tokenVal.StartsWith('TBD')) {
  138 + Write-Host " GITLAB_PROJECT_ID = TBD [token 未填,跳过 API 解析;填完 token 后重跑此脚本]"
  139 + $tokenUsable = $false
  140 +}
  141 +
  142 +if ($tokenUsable) {
  143 + # PS 5.1 默认 TLS1.0/1.1,自建 GitLab 通常需要 TLS1.2
  144 + try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {}
  145 + $headers = @{ 'PRIVATE-TOKEN' = $tokenVal }
  146 +
  147 + $userOk = $false
  148 + try {
  149 + $userResp = Invoke-WebRequest -Uri "$apiUrl/user" -Headers $headers -UseBasicParsing -ErrorAction Stop
  150 + if ([int]$userResp.StatusCode -eq 200) { $userOk = $true }
  151 + else { Write-Host " GITLAB_PROJECT_ID = TBD [token 验证失败 HTTP $([int]$userResp.StatusCode),留 TBD 待人工确认]" }
  152 + } catch {
  153 + $code = 0
  154 + if ($_.Exception.Response) { try { $code = [int]$_.Exception.Response.StatusCode } catch {} }
  155 + if ($code -eq 0) { $code = '000' }
  156 + Write-Host " GITLAB_PROJECT_ID = TBD [token 验证失败 HTTP $code,留 TBD 待人工确认]"
  157 + }
  158 +
  159 + if ($userOk) {
  160 + try {
  161 + $searchUri = "$apiUrl/projects?search=$([uri]::EscapeDataString($repoName))&simple=true&per_page=50"
  162 + $projects = Invoke-RestMethod -Uri $searchUri -Headers $headers -UseBasicParsing -ErrorAction Stop
  163 + $match = $projects | Where-Object { $_.path_with_namespace -eq $pathRaw } | Select-Object -First 1
  164 + if ($match) {
  165 + Update-Field -Key 'GITLAB_PROJECT_ID' -NewVal "$($match.id)"
  166 + } else {
  167 + Write-Host " GITLAB_PROJECT_ID = TBD [API 未匹配 path_with_namespace=$pathRaw,请到 GitLab 项目设置页查数字 ID 后填入]"
  168 + }
  169 + } catch {
  170 + Write-Host " GITLAB_PROJECT_ID = TBD [API 调用失败:$($_.Exception.Message)]"
  171 + }
  172 + }
  173 +}
  174 +
  175 +Report-Token
  176 +
  177 +# ---- 回写 .env.local(保持 EOL / BOM 状态)----
  178 +if ($script:modified) {
  179 + $newText = [string]::Join($eol, $lines.ToArray())
  180 + if ($hadTrailingNewline) { $newText += $eol }
  181 + $encoding = New-Object System.Text.UTF8Encoding($hasBom)
  182 + [System.IO.File]::WriteAllText($EnvFile, $newText, $encoding)
  183 +}
... ...
skills/plan/downstream-gen/scripts/derive-gitlab.sh
1 1 #!/usr/bin/env bash
2 2 # derive-gitlab.sh — 从 git origin 远程派生 GitLab 凭据并回填 .env.local
  3 +# macOS / Linux 用此版本;Windows 用同目录 derive-gitlab.ps1。
  4 +# 两份脚本严格等价 — 改动任一份必须同步对方。
3 5 #
4 6 # 用法: bash derive-gitlab.sh [.env.local 路径,默认 .env.local]
5 7 #
... ...