playbook/scripts/sync_standards.ps1

343 lines
12 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Sync standards snapshot to project root.
# - Copies <snapshot>/rulesets/<AGENTS_NS> -> <project-root>/.agents/<AGENTS_NS>
# - Updates <project-root>/.gitattributes (append missing rules by default)
# Existing targets are backed up before overwrite.
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[Alias('h', 'help', '?')]
[switch]$Help,
# Sync multiple rulesets in one run:
# -Langs tsl,cpp
# -Langs @("tsl","cpp")
[Parameter(Mandatory = $false)]
[string[]]$Langs
)
$ErrorActionPreference = "Stop"
if ($Help) {
Write-Host "Usage:"
Write-Host " powershell -File scripts/sync_standards.ps1"
Write-Host " powershell -File scripts/sync_standards.ps1 -Langs tsl,cpp"
Write-Host ""
Write-Host "Options:"
Write-Host " -Langs <list> Comma/space-separated list or array."
Write-Host " -Help Show this help."
Write-Host ""
Write-Host "Env:"
Write-Host " SYNC_ROOT Target project root (default: git root)."
Write-Host " AGENTS_NS Single ruleset name (default: tsl)."
Write-Host " SYNC_GITATTR_MODE append|overwrite|block|skip (default: append)."
exit 0
}
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$Src = (Resolve-Path (Join-Path $ScriptDir "..")).Path
$Root = $env:SYNC_ROOT
if (-not $Root) {
$Root = (git -C $ScriptDir rev-parse --show-toplevel 2>$null)
if (-not $Root) { $Root = (Get-Location).Path }
}
$Root = (Resolve-Path $Root).Path
$AgentsSrcRoot = Join-Path $Src "rulesets"
$GitAttrSrc = Join-Path $Src ".gitattributes"
if (-not (Test-Path $AgentsSrcRoot)) {
throw "Standards snapshot not found at $AgentsSrcRoot. Run: git subtree add --prefix docs/standards/playbook <url> <branch> --squash"
}
$timestamp = Get-Date -Format "yyyyMMddHHmmss"
# Auto-detect languages from existing .agents when no args are provided.
if (-not $env:SYNC_STANDARDS_INNER -and (-not $Langs -or $Langs.Count -eq 0) -and -not $env:AGENTS_NS) {
$agentsRoot = Join-Path $Root ".agents"
if (Test-Path $agentsRoot) {
$autoLangs = @(Get-ChildItem -Path $agentsRoot -Directory | ForEach-Object { $_.Name } | Where-Object { Test-Path (Join-Path $AgentsSrcRoot $_) })
if ($autoLangs.Count -gt 0) {
$Langs = $autoLangs
}
}
}
# Multi rulesets: only on the outer invocation.
if (-not $env:SYNC_STANDARDS_INNER -and $Langs -and $Langs.Count -gt 0) {
$oldInner = $env:SYNC_STANDARDS_INNER
$oldAgentsNs = $env:AGENTS_NS
$oldMode = $env:SYNC_GITATTR_MODE
$syncModeFirst = $env:SYNC_GITATTR_MODE
if (-not $syncModeFirst) { $syncModeFirst = "append" }
$first = $true
foreach ($ns in $Langs) {
if (-not $ns) { continue }
$env:SYNC_STANDARDS_INNER = "1"
$env:AGENTS_NS = $ns
if ($first) {
$first = $false
$env:SYNC_GITATTR_MODE = $syncModeFirst
} else {
$env:SYNC_GITATTR_MODE = "skip"
}
& $MyInvocation.MyCommand.Path
}
$env:SYNC_STANDARDS_INNER = $oldInner
$env:AGENTS_NS = $oldAgentsNs
$env:SYNC_GITATTR_MODE = $oldMode
exit 0
}
$AgentsNs = $env:AGENTS_NS
if (-not $AgentsNs) { $AgentsNs = "tsl" }
if ($AgentsNs -match '[\\/]' -or $AgentsNs -match '\.\.') {
throw "Invalid AGENTS_NS=$AgentsNs"
}
$AgentsSrc = Join-Path $AgentsSrcRoot $AgentsNs
if (-not (Test-Path $AgentsSrc)) {
# Backward-compatible fallback: older snapshots used <snapshot>/.agents/* directly.
if ((Test-Path (Join-Path $AgentsSrcRoot "index.md")) -and (Test-Path (Join-Path $AgentsSrcRoot "auth.md"))) {
$AgentsSrc = $AgentsSrcRoot
} else {
throw "Agents ruleset not found: $AgentsSrc (set AGENTS_NS to one of the subdirs under $AgentsSrcRoot, e.g. tsl/cpp)."
}
}
$AgentsRoot = Join-Path $Root ".agents"
$AgentsDst = Join-Path $AgentsRoot $AgentsNs
if ($Src -ieq $Root) {
Write-Host "Skip: snapshot root equals project root."
Write-Host "Done."
exit 0
}
New-Item -ItemType Directory -Path $AgentsRoot -Force | Out-Null
if (Test-Path $AgentsDst) {
$bak = (Join-Path $AgentsRoot "$AgentsNs.bak.$timestamp")
Move-Item $AgentsDst $bak
Write-Host "Backed up existing $AgentsNs agents -> $(Split-Path -Leaf $bak)"
}
New-Item -ItemType Directory -Path $AgentsDst -Force | Out-Null
Copy-Item (Join-Path $AgentsSrc "*") $AgentsDst -Recurse -Force
Write-Host "Synced .agents/$AgentsNs from standards."
# Rewrite docs/* references to the snapshot docs path.
$relSnapshot = $null
$rootPrefix = $Root.TrimEnd('\', '/')
$rootPrefixWithSep = $rootPrefix + [System.IO.Path]::DirectorySeparatorChar
if ($Src.ToLowerInvariant().StartsWith($rootPrefixWithSep.ToLowerInvariant())) {
$relSnapshot = $Src.Substring($rootPrefixWithSep.Length)
}
if ($relSnapshot) {
$docsPrefix = (Join-Path $relSnapshot "docs") -replace "\\", "/"
Get-ChildItem -Path $AgentsDst -Filter *.md -File | ForEach-Object {
$content = Get-Content -Raw -Path $_.FullName
$content = $content.Replace("``docs/tsl/", "``$docsPrefix/tsl/")
$content = $content.Replace("``docs/cpp/", "``$docsPrefix/cpp/")
$content = $content.Replace("``docs/python/", "``$docsPrefix/python/")
$content = $content.Replace("``docs/markdown/", "``$docsPrefix/markdown/")
$content = $content.Replace("``docs/common/", "``$docsPrefix/common/")
Set-Content -Path $_.FullName -Value $content -Encoding UTF8
}
}
$AgentsIndex = Join-Path $AgentsRoot "index.md"
if (-not (Test-Path $AgentsIndex)) {
@'
# .agents多语言
本目录用于存放仓库级/语言级的代理规则集。
建议约定:
- `.agents/tsl/`TSL 相关规则集(由 `sync_standards.*` 同步;适用于 `.tsl`/`.tsf`
- `.agents/cpp/`C++ 相关规则集(由 `sync_standards.*` 同步;适用于 C++23/Modules
- `.agents/python/`Python 相关规则集(由 `sync_standards.*` 同步)
- `.agents/markdown/`Markdown 相关规则集(仅代码格式化)
规则发生冲突时,建议以“更靠近代码的目录规则更具体”为准。
入口建议从:
- `.agents/tsl/index.md`TSL 规则集入口)
- `.agents/cpp/index.md`C++ 规则集入口)
- `.agents/markdown/index.md`Markdown 规则集入口)
- `docs/standards/playbook/docs/`(人类开发规范快照:`tsl/`、`cpp/`、`python/`、`common/`
'@ | Set-Content -Path $AgentsIndex -Encoding UTF8
Write-Host "Created .agents/index.md"
}
$AgentsMd = Join-Path $Root "AGENTS.md"
$AgentsBlockStart = "<!-- playbook:agents:start -->"
$AgentsBlockEnd = "<!-- playbook:agents:end -->"
$agentsLangs = @()
if (Test-Path $AgentsRoot) {
Get-ChildItem -Path $AgentsRoot -Directory | ForEach-Object {
$name = $_.Name
if ($name -and -not $name.StartsWith(".") -and -not ($name -match "\.bak\.") -and (Test-Path (Join-Path $_.FullName "index.md"))) {
$agentsLangs += $name
}
}
}
if ($agentsLangs.Count -eq 0) { $agentsLangs = @($AgentsNs) }
$langsLine = ($agentsLangs | ForEach-Object { "`.agents/$_/index.md`" }) -join ""
$agentsBlock = @"
<!-- playbook:agents:start -->
请以 `.agents/` 下的规则为准
- 入口`.agents/index.md`
- 语言规则$langsLine
<!-- playbook:agents:end -->
"@
if (-not (Test-Path $AgentsMd)) {
@"
# Agent Instructions
$agentsBlock
"@ | Set-Content -Path $AgentsMd -Encoding UTF8
Write-Host "Created AGENTS.md"
} else {
$content = Get-Content -Raw -Path $AgentsMd
if ($content.Contains($AgentsBlockStart)) {
$pattern = [regex]::Escape($AgentsBlockStart) + ".*?" + [regex]::Escape($AgentsBlockEnd)
$regex = New-Object System.Text.RegularExpressions.Regex($pattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
$newContent = $regex.Replace($content, $agentsBlock, 1)
Set-Content -Path $AgentsMd -Value $newContent -Encoding UTF8
Write-Host "Updated AGENTS.md (playbook block)."
} elseif ($content.Contains(".agents/index.md")) {
Write-Host "Skip: AGENTS.md already references .agents/index.md"
} else {
Add-Content -Path $AgentsMd -Value "" -Encoding UTF8
Add-Content -Path $AgentsMd -Value $agentsBlock -Encoding UTF8
Add-Content -Path $AgentsMd -Value "" -Encoding UTF8
Write-Host "Appended playbook block to AGENTS.md"
}
}
$GitAttrDst = Join-Path $Root ".gitattributes"
if (Test-Path $GitAttrSrc) {
$mode = $env:SYNC_GITATTR_MODE
if (-not $mode) { $mode = "append" }
switch ($mode.ToLowerInvariant()) {
"skip" {
Write-Host "Skip: .gitattributes sync (SYNC_GITATTR_MODE=skip)."
break
}
"overwrite" {
if ($GitAttrSrc -ieq $GitAttrDst) {
Write-Host "Skip: .gitattributes source equals destination."
break
}
if (Test-Path $GitAttrDst) {
$bak = "$GitAttrDst.bak.$timestamp"
Move-Item $GitAttrDst $bak
Write-Host "Backed up existing .gitattributes -> $bak"
}
Copy-Item $GitAttrSrc $GitAttrDst -Force
Write-Host "Synced .gitattributes from standards (overwrite)."
break
}
"append" {
if ($GitAttrSrc -ieq $GitAttrDst) {
Write-Host "Skip: .gitattributes source equals destination."
break
}
$dstLines = @{}
if (Test-Path $GitAttrDst) {
Get-Content $GitAttrDst | ForEach-Object {
$line = $_.Trim()
if ($line -eq "" -or $line.StartsWith("#")) { return }
$dstLines[$line] = $true
}
}
$missing = New-Object System.Collections.Generic.List[string]
Get-Content $GitAttrSrc | ForEach-Object {
$line = $_.Trim()
if ($line -eq "" -or $line.StartsWith("#")) { return }
if (-not $dstLines.ContainsKey($line)) {
$dstLines[$line] = $true
$missing.Add($line)
}
}
if ($missing.Count -eq 0) {
Write-Host "No missing .gitattributes rules to append."
break
}
$bak = $null
if (Test-Path $GitAttrDst) {
$bak = "$GitAttrDst.bak.$timestamp"
Move-Item $GitAttrDst $bak -Force
Write-Host "Backed up existing .gitattributes -> $bak"
}
$sourceNote = $GitAttrSrc
$rootPrefix = "$Root\"
if ($sourceNote.StartsWith($rootPrefix)) {
$sourceNote = $sourceNote.Substring($rootPrefix.Length)
}
$header = "# Added from playbook .gitattributes (source: $sourceNote)"
$content = @()
if ($bak -and (Test-Path $bak)) {
$existing = Get-Content $bak
if ($existing.Count -gt 0) {
$content += $existing
$content += ""
}
}
$content += $header
$content += $missing
$content | Set-Content -Path $GitAttrDst -Encoding UTF8
Write-Host "Appended missing .gitattributes rules from standards."
break
}
"block" {
$begin = "# BEGIN playbook .gitattributes"
$end = "# END playbook .gitattributes"
$beginOld = "# BEGIN tsl-playbook .gitattributes"
$endOld = "# END tsl-playbook .gitattributes"
$src = Get-Content -Path $GitAttrSrc -Raw
$block = $begin + "`r`n" + $src.TrimEnd() + "`r`n" + $end + "`r`n"
$dst = ""
if (Test-Path $GitAttrDst) {
$bak = "$GitAttrDst.bak.$timestamp"
Move-Item $GitAttrDst $bak
Write-Host "Backed up existing .gitattributes -> $bak"
$dst = Get-Content -Path $bak -Raw
}
$pattern = "(?ms)^(" + [regex]::Escape($begin) + "|" + [regex]::Escape($beginOld) + ")\\R.*?^(" + [regex]::Escape($end) + "|" + [regex]::Escape($endOld) + ")\\R?"
if ($dst -and ($dst -match $pattern)) {
$new = [regex]::Replace($dst, $pattern, $block)
} elseif ($dst) {
$new = $dst.TrimEnd() + "`r`n`r`n" + $block
} else {
$new = $block
}
$new | Set-Content -Path $GitAttrDst -Encoding UTF8
Write-Host "Updated .gitattributes from standards (managed block)."
break
}
default {
throw "Invalid SYNC_GITATTR_MODE=$mode (use block|overwrite|append|skip)"
}
}
}
Write-Host "Done."