Commit e559870b43bc9bfe8a9b412ffb8d23c1ee6039e4
1 parent
cce98737
feat(downstream-gen): 新增 derive-gitlab.ps1,Windows 用户零外部依赖
PS 5.1+ 系统自带,用 Invoke-WebRequest + .NET 字符串处理替代 curl/jq/sed。 与 .sh 严格等价;SKILL.md 结束横幅按平台二选一打印调用命令。
Showing
3 changed files
with
191 additions
and
3 deletions
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 "${CLAUDE_SKILL_DIR}/templates/docs-10-header-template.md" 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