From e559870b43bc9bfe8a9b412ffb8d23c1ee6039e4 Mon Sep 17 00:00:00 2001 From: zichun Date: Mon, 18 May 2026 17:28:10 +0800 Subject: [PATCH] feat(downstream-gen): 新增 derive-gitlab.ps1,Windows 用户零外部依赖 --- skills/plan/downstream-gen/SKILL.md | 9 ++++++--- skills/plan/downstream-gen/scripts/derive-gitlab.ps1 | 183 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ skills/plan/downstream-gen/scripts/derive-gitlab.sh | 2 ++ 3 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 skills/plan/downstream-gen/scripts/derive-gitlab.ps1 diff --git a/skills/plan/downstream-gen/SKILL.md b/skills/plan/downstream-gen/SKILL.md index a494bc6..76cefe3 100644 --- a/skills/plan/downstream-gen/SKILL.md +++ b/skills/plan/downstream-gen/SKILL.md @@ -139,8 +139,11 @@ cp "${CLAUDE_SKILL_DIR}/templates/docs-10-header-template.md" docs/10-验收检 4. 补齐 `.env.local` 三个 GITLAB_* 字段 - `GITLAB_TOKEN`:去 GitLab Profile → Account → Private token 生成后粘贴 - - `GITLAB_PROJECT_ID`,`GITLAB_API_URL`:运行 - bash /scripts/derive-gitlab.sh + - `GITLAB_PROJECT_ID`,`GITLAB_API_URL`:按平台运行(脚本只依赖 git) + macOS / Linux: + bash /scripts/derive-gitlab.sh + Windows (PowerShell 5.1+,系统自带,无需额外安装): + powershell -NoProfile -ExecutionPolicy Bypass -File /scripts/derive-gitlab.ps1 5. remote git 就绪后,再运行 /erp-workflow:coding-start ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -154,4 +157,4 @@ cp "${CLAUDE_SKILL_DIR}/templates/docs-10-header-template.md" docs/10-验收检 - `${CLAUDE_SKILL_DIR}/templates/docs-06-module-pagelist-template.md`(追加到 docs/06 § 三) - `${CLAUDE_SKILL_DIR}/templates/docs-08-module-row-template.md`(模块 bullet 行模板) - `${CLAUDE_SKILL_DIR}/templates/docs-10-header-template.md` -- `${CLAUDE_SKILL_DIR}/scripts/derive-gitlab.sh`(**用户可选辅助**:在 add origin 之后手动跑一次,自动派生 GITLAB_PROJECT_ID / GITLAB_API_URL 写入 .env.local) +- `${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,两份严格等价 — 改动必须同步双方) diff --git a/skills/plan/downstream-gen/scripts/derive-gitlab.ps1 b/skills/plan/downstream-gen/scripts/derive-gitlab.ps1 new file mode 100644 index 0000000..5cedc60 --- /dev/null +++ b/skills/plan/downstream-gen/scripts/derive-gitlab.ps1 @@ -0,0 +1,183 @@ +#!/usr/bin/env pwsh +# derive-gitlab.ps1 — Windows 对应版(PowerShell 5.1+ 原生可跑,无外部依赖) +# 与同目录 derive-gitlab.sh 严格等价 — 改动任一份必须同步对方。 +# +# 用法: powershell -NoProfile -ExecutionPolicy Bypass -File derive-gitlab.ps1 [.env.local 路径] +# +# 派生字段、退出码、回填策略 见 derive-gitlab.sh 头部注释。 + +[CmdletBinding()] +param([string]$EnvFile = '.env.local') + +# 让 Chinese 输出在 cmd / PowerShell 控制台正常显示 +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} + +if (-not (Test-Path -LiteralPath $EnvFile -PathType Leaf)) { + [Console]::Error.WriteLine("derive-gitlab.ps1: env file not found: $EnvFile") + exit 2 +} + +# ---- 取 origin URL ---- +$url = '' +try { + $raw = & git remote get-url origin 2>$null + if ($LASTEXITCODE -eq 0 -and $raw) { $url = ($raw -join '').Trim() } +} catch { $url = '' } + +if ([string]::IsNullOrEmpty($url)) { + [Console]::Error.WriteLine("derive-gitlab.ps1: 未配置 origin 远程,GITLAB_* 留给用户手填") + exit 0 +} + +# ---- 仅支持 http(s) ---- +$scheme = $null +if ($url.StartsWith('https://')) { $scheme = 'https' } +elseif ($url.StartsWith('http://')) { $scheme = 'http' } +else { + [Console]::Error.WriteLine("derive-gitlab.ps1: origin 不是 http(s) URL ($url),跳过派生") + exit 0 +} + +$rest = $url.Substring("${scheme}://".Length) +$slashIdx = $rest.IndexOf('/') +if ($slashIdx -lt 0) { + [Console]::Error.WriteLine("derive-gitlab.ps1: origin URL 缺少路径段 ($url),跳过派生") + exit 0 +} +$gitlabHost = $rest.Substring(0, $slashIdx) +$pathRaw = $rest.Substring($slashIdx + 1) +if ($pathRaw.EndsWith('.git')) { $pathRaw = $pathRaw.Substring(0, $pathRaw.Length - 4) } +$repoName = Split-Path -Leaf $pathRaw + +$apiUrl = "${scheme}://${gitlabHost}/api/v3" + +# ---- 读 .env.local 全文,按原行尾切分 ---- +$rawBytes = [System.IO.File]::ReadAllBytes($EnvFile) +$hasBom = ($rawBytes.Length -ge 3 -and $rawBytes[0] -eq 0xEF -and $rawBytes[1] -eq 0xBB -and $rawBytes[2] -eq 0xBF) +$rawText = if ($hasBom) { + [System.Text.Encoding]::UTF8.GetString($rawBytes, 3, $rawBytes.Length - 3) +} else { + [System.Text.Encoding]::UTF8.GetString($rawBytes) +} +$eol = if ($rawText -match "`r`n") { "`r`n" } else { "`n" } +$hadTrailingNewline = $rawText.EndsWith("`n") +$lines = [System.Collections.Generic.List[string]]::new() +foreach ($l in ($rawText -split "`r?`n")) { $lines.Add($l) | Out-Null } +if ($hadTrailingNewline -and $lines.Count -gt 0 -and $lines[$lines.Count - 1] -eq '') { + $lines.RemoveAt($lines.Count - 1) +} + +function Get-FieldStripped { + param([string]$Key) + for ($i = 0; $i -lt $script:lines.Count; $i++) { + $ln = $script:lines[$i] + if ($ln.StartsWith("$Key=")) { + $val = $ln.Substring($Key.Length + 1) + # 剥外层单/双引号 + if ($val.Length -ge 2) { + $f = $val[0]; $l = $val[$val.Length - 1] + if (($f -eq "'" -and $l -eq "'") -or ($f -eq '"' -and $l -eq '"')) { + $val = $val.Substring(1, $val.Length - 2) + } + } + return @{ Found = $true; Index = $i; Stripped = $val } + } + } + return @{ Found = $false } +} + +function Update-Field { + param([string]$Key, [string]$NewVal) + $info = Get-FieldStripped -Key $Key + if (-not $info.Found) { + [Console]::Error.WriteLine(" $Key = (.env.local 中无此行,跳过)") + return + } + $stripped = $info.Stripped + if ([string]::IsNullOrEmpty($stripped) -or $stripped -eq 'TBD(A5 自动补)') { + $script:lines[$info.Index] = "$Key=$NewVal" + $script:modified = $true + Write-Host " $Key = $NewVal [已派生填入]" + } elseif ($stripped -eq $NewVal) { + Write-Host " $Key = $NewVal [已是派生值,无需更新]" + } else { + Write-Host " $Key = $stripped [保留用户手填,未覆盖派生值 $NewVal]" + } +} + +function Report-Token { + $info = Get-FieldStripped -Key 'GITLAB_TOKEN' + if (-not $info.Found) { + [Console]::Error.WriteLine(" GITLAB_TOKEN = (.env.local 中无此行,跳过)") + return + } + $stripped = $info.Stripped + if ([string]::IsNullOrEmpty($stripped) -or $stripped.StartsWith('【人工填写:') -or $stripped.StartsWith('TBD')) { + Write-Host " GITLAB_TOKEN = $stripped [待人工填写:GitLab Profile → Account → Private token]" + } else { + $masked = if ($stripped.Length -le 8) { + ('*' * $stripped.Length) + } else { + $stripped.Substring(0, 4) + ('*' * ($stripped.Length - 8)) + $stripped.Substring($stripped.Length - 4) + } + Write-Host " GITLAB_TOKEN = $masked [已填入,长度 $($stripped.Length)]" + } +} + +$script:modified = $false + +Write-Host "derive-gitlab.ps1: 从 origin ($url) 派生 GitLab 凭据:" +Update-Field -Key 'GITLAB_API_URL' -NewVal $apiUrl + +# ---- GITLAB_PROJECT_ID:经 GitLab API 解析数字 ID ---- +$tokenInfo = Get-FieldStripped -Key 'GITLAB_TOKEN' +$tokenVal = if ($tokenInfo.Found) { $tokenInfo.Stripped } else { '' } + +$tokenUsable = $true +if ([string]::IsNullOrEmpty($tokenVal) -or $tokenVal.StartsWith('【人工填写:') -or $tokenVal.StartsWith('TBD')) { + Write-Host " GITLAB_PROJECT_ID = TBD [token 未填,跳过 API 解析;填完 token 后重跑此脚本]" + $tokenUsable = $false +} + +if ($tokenUsable) { + # PS 5.1 默认 TLS1.0/1.1,自建 GitLab 通常需要 TLS1.2 + try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} + $headers = @{ 'PRIVATE-TOKEN' = $tokenVal } + + $userOk = $false + try { + $userResp = Invoke-WebRequest -Uri "$apiUrl/user" -Headers $headers -UseBasicParsing -ErrorAction Stop + if ([int]$userResp.StatusCode -eq 200) { $userOk = $true } + else { Write-Host " GITLAB_PROJECT_ID = TBD [token 验证失败 HTTP $([int]$userResp.StatusCode),留 TBD 待人工确认]" } + } catch { + $code = 0 + if ($_.Exception.Response) { try { $code = [int]$_.Exception.Response.StatusCode } catch {} } + if ($code -eq 0) { $code = '000' } + Write-Host " GITLAB_PROJECT_ID = TBD [token 验证失败 HTTP $code,留 TBD 待人工确认]" + } + + if ($userOk) { + try { + $searchUri = "$apiUrl/projects?search=$([uri]::EscapeDataString($repoName))&simple=true&per_page=50" + $projects = Invoke-RestMethod -Uri $searchUri -Headers $headers -UseBasicParsing -ErrorAction Stop + $match = $projects | Where-Object { $_.path_with_namespace -eq $pathRaw } | Select-Object -First 1 + if ($match) { + Update-Field -Key 'GITLAB_PROJECT_ID' -NewVal "$($match.id)" + } else { + Write-Host " GITLAB_PROJECT_ID = TBD [API 未匹配 path_with_namespace=$pathRaw,请到 GitLab 项目设置页查数字 ID 后填入]" + } + } catch { + Write-Host " GITLAB_PROJECT_ID = TBD [API 调用失败:$($_.Exception.Message)]" + } + } +} + +Report-Token + +# ---- 回写 .env.local(保持 EOL / BOM 状态)---- +if ($script:modified) { + $newText = [string]::Join($eol, $lines.ToArray()) + if ($hadTrailingNewline) { $newText += $eol } + $encoding = New-Object System.Text.UTF8Encoding($hasBom) + [System.IO.File]::WriteAllText($EnvFile, $newText, $encoding) +} diff --git a/skills/plan/downstream-gen/scripts/derive-gitlab.sh b/skills/plan/downstream-gen/scripts/derive-gitlab.sh index 139117c..17c6898 100644 --- a/skills/plan/downstream-gen/scripts/derive-gitlab.sh +++ b/skills/plan/downstream-gen/scripts/derive-gitlab.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash # derive-gitlab.sh — 从 git origin 远程派生 GitLab 凭据并回填 .env.local +# macOS / Linux 用此版本;Windows 用同目录 derive-gitlab.ps1。 +# 两份脚本严格等价 — 改动任一份必须同步对方。 # # 用法: bash derive-gitlab.sh [.env.local 路径,默认 .env.local] # -- libgit2 0.22.2