125 lines
5.5 KiB
PowerShell
125 lines
5.5 KiB
PowerShell
#Requires -Version 5.1
|
||
# 生成 skills/INDEX.md:从各模块 SKILL.md 的 frontmatter 提取 name+description,生成导航索引。
|
||
# 幂等:重复运行输出一致(CI 用 git diff 校验防 drift)。
|
||
# 用法:
|
||
# powershell -NoProfile -ExecutionPolicy Bypass -File skills/scripts/extract-summaries.ps1
|
||
# powershell -File skills/scripts/extract-summaries.ps1 -Check # 只校验不写(CI 模式,不一致 exit 1)
|
||
param(
|
||
[switch] $Check
|
||
)
|
||
$ErrorActionPreference = 'Stop'
|
||
|
||
$scriptDir = $PSScriptRoot
|
||
if (-not $scriptDir) { $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path }
|
||
$skillsRoot = Split-Path -Parent $scriptDir
|
||
$indexPath = Join-Path $skillsRoot 'INDEX.md'
|
||
|
||
# 扫描版本库中的模块 SKILL.md(排除本地 ignored/private trees,保证 clean clone 与开发机幂等)。
|
||
$skipDirs = @('ops', 'scripts', 'config', 'tests', 'field-journal', 'references')
|
||
$packageRoot = Split-Path -Parent $skillsRoot
|
||
$trackedSkillFiles = @()
|
||
$git = Get-Command git -ErrorAction SilentlyContinue
|
||
if ($git) {
|
||
$trackedPaths = @(& $git.Source -C $packageRoot ls-files -- 'skills/**/SKILL.md')
|
||
if ($LASTEXITCODE -eq 0) {
|
||
$trackedSkillFiles = @($trackedPaths | ForEach-Object {
|
||
$fullPath = Join-Path $packageRoot ($_ -replace '/', [IO.Path]::DirectorySeparatorChar)
|
||
if (Test-Path -LiteralPath $fullPath -PathType Leaf) { Get-Item -LiteralPath $fullPath }
|
||
})
|
||
}
|
||
}
|
||
$candidateSkillFiles = if ($trackedSkillFiles.Count -gt 0) {
|
||
$trackedSkillFiles
|
||
} else {
|
||
@(Get-ChildItem -Path $skillsRoot -Recurse -Filter 'SKILL.md')
|
||
}
|
||
$skillFiles = $candidateSkillFiles | Where-Object {
|
||
$rel = $_.FullName.Substring($skillsRoot.Length + 1)
|
||
$rel -ne 'SKILL.md' -and -not ($skipDirs | Where-Object { $rel.StartsWith($_ + '\') -or $rel.StartsWith($_ + '/') })
|
||
} | Sort-Object { $_.FullName.Substring($skillsRoot.Length + 1) }
|
||
|
||
$rows = New-Object System.Collections.ArrayList
|
||
foreach ($sf in $skillFiles) {
|
||
$rel = $sf.FullName.Substring($skillsRoot.Length + 1)
|
||
# 兼容 Windows(\)与 Linux/macOS(/)两种路径分隔符
|
||
$dir = $rel.Split(@('\', '/'))[0]
|
||
$head = Get-Content -LiteralPath $sf.FullName -TotalCount 15 -Encoding UTF8
|
||
$name = ''; $desc = ''; $blockMode = $false; $inFm = $false
|
||
foreach ($line in $head) {
|
||
if ($line -match '^---') {
|
||
if ($inFm) { break } # 第二个 --- 结束 frontmatter
|
||
$inFm = $true; continue
|
||
}
|
||
if (-not $inFm) { continue }
|
||
if ($line -match '^name:\s*(.+)$') { $name = $Matches[1].Trim(); continue }
|
||
if ($line -match '^description:\s*\|') { $blockMode = $true; continue }
|
||
if ($line -match '^description:\s*(.+)$') { $desc = $Matches[1].Trim(); continue }
|
||
if ($blockMode) {
|
||
# YAML 块:缩进的文本行拼接(取首行,超长截断)
|
||
if ($line -match '^\s{2,}(.+)$') {
|
||
if (-not $desc) { $desc = $Matches[1].Trim() }
|
||
} elseif ($line -match '^\S') { $blockMode = $false }
|
||
}
|
||
}
|
||
if (-not $name) { $name = $dir }
|
||
if (-not $desc) { $desc = '(无摘要)' }
|
||
if ($desc.Length -gt 160) { $desc = $desc.Substring(0, 157) + '...' }
|
||
[void]$rows.Add([pscustomobject]@{
|
||
Dir = $dir
|
||
Name = $name
|
||
Desc = $desc
|
||
Path = $rel
|
||
})
|
||
}
|
||
|
||
$sb = New-Object System.Text.StringBuilder
|
||
[void]$sb.AppendLine('# reverse-skill 技能导航索引')
|
||
[void]$sb.AppendLine('')
|
||
[void]$sb.AppendLine('> 本文件由 `skills/scripts/extract-summaries.ps1` 自动生成,**请勿手改**。')
|
||
[void]$sb.AppendLine('> 修改摘要请编辑对应模块 `SKILL.md` 的 frontmatter `description`,然后重跑脚本。')
|
||
[void]$sb.AppendLine('')
|
||
[void]$sb.AppendLine('## 模块总览')
|
||
[void]$sb.AppendLine('')
|
||
[void]$sb.AppendLine('| 模块 | 摘要 |')
|
||
[void]$sb.AppendLine('|------|------|')
|
||
foreach ($r in $rows) {
|
||
$esc = $r.Desc -replace '\|', '\|'
|
||
[void]$sb.AppendLine(("| [{0}]({1}) | {2} |" -f $r.Name, ($r.Path -replace '\\', '/'), $esc))
|
||
}
|
||
[void]$sb.AppendLine('')
|
||
[void]$sb.AppendLine('## 目录树')
|
||
[void]$sb.AppendLine('')
|
||
[void]$sb.AppendLine('```')
|
||
foreach ($r in $rows) {
|
||
[void]$sb.AppendLine(("skills/{0}/" -f ($r.Path -replace '\\', '/')))
|
||
}
|
||
[void]$sb.AppendLine('```')
|
||
[void]$sb.AppendLine('')
|
||
[void]$sb.AppendLine('## 路由')
|
||
[void]$sb.AppendLine('')
|
||
[void]$sb.AppendLine('PRIMARY 路由由 `skills/config/routing.json`(唯一事实源)驱动,用 `master-route.ps1 -Hint "<任务>"` 分诊。')
|
||
[void]$sb.AppendLine('歧义场景读 `skills/routing.md` 全矩阵;CTF 多类型任务走 `CTF-Sandbox-Orchestrator/`。')
|
||
|
||
$newContent = $sb.ToString()
|
||
# 统一 LF 行尾:与 .gitattributes (*.md eol=lf) 保持一致,保证 clone 后 -Check 幂等
|
||
$newContent = $newContent -replace "`r`n", "`n"
|
||
$utf8 = New-Object System.Text.UTF8Encoding $true
|
||
|
||
if ($Check) {
|
||
if (-not (Test-Path -LiteralPath $indexPath)) {
|
||
Write-Host '[CHECK] INDEX.md missing' -ForegroundColor Red
|
||
exit 1
|
||
}
|
||
# 行尾免疫:autocrlf=true 的机器上 worktree 可能是 CRLF,normalize 后再比
|
||
$old = ([System.IO.File]::ReadAllText($indexPath)) -replace "`r`n", "`n"
|
||
if ($old -eq $newContent) {
|
||
Write-Host '[CHECK] INDEX.md up to date' -ForegroundColor Green
|
||
exit 0
|
||
}
|
||
Write-Host '[CHECK] INDEX.md out of date (run extract-summaries.ps1)' -ForegroundColor Red
|
||
exit 1
|
||
}
|
||
|
||
[System.IO.File]::WriteAllText($indexPath, $newContent, $utf8)
|
||
Write-Host ("INDEX.md regenerated: {0} modules" -f $rows.Count) -ForegroundColor Green
|