Files
superpowers-zh/scripts/audit.sh
AI不止语 1f66b091b7 fix(site): 官网工具墙停在 20 款 —— Cline / Kilo Code / Crush 的用户在下拉里找不到自己的工具
起因是核对首页:统计块写着「20 支持工具」,同一屏下面的标题写着「一套 skill,
23 款工具通用」,FAQ 里又老老实实枚举了 23 款。三处自己打自己。

根因:文案计数早就跟着 installer 改到了 23,但 `site/build.mjs` 的 `TOOLS`
数组从来没人补 —— audit 只校验文案里的数字,不校验列表本身。

## 1. 工具列表补 3 款 + 修 4 处错的

`TOOLS` 20 条 -> 23 条(= TARGETS 22 条 + Copilot CLI 单独计数)。
统计块的 `20` 是硬编码的,改成 `TOOLS.length`。

漏的三款(installer 一直支持,官网从没有过):Cline、Kilo Code、Crush。

已有条目里查出的错(逐条实测过命令):

| 条目 | 原来 | 现在 | 依据 |
|---|---|---|---|
| Claw Code | `--tool claw` | 直接 `npx superpowers-zh` | detect 是 `.claw` / `CLAW.md`,实测能自动检出 |
| Hermes Agent | `--tool hermes` | `--global --tool hermes` | 官方只自动加载 `~/.hermes/skills/`,项目级要手写 config.yaml,给项目级命令等于给「装了不生效」 |
| Qwen Code | 类型 IDE | CLI | 它是 QwenLM/qwen-code 命令行工具,不是通义灵码 |
| Antigravity | 类型 CLI | IDE | Google 的 AI IDE,docs/README.antigravity.md 里本来就这么写 |

`auto: true/false` 两态不够用了(Hermes 是「能检出但项目级不生效」),换成
`mode: auto / manual / global`,三语各补一条对应说明文案。

## 2. FAQ 的「支持全局」清单少了 4 款

写着 7 款,installer 实际 11 款 —— CodeArts / Crush / Hermes / CodeBuddy
陆续拿到 `--global` 之后这句话没跟。三语同步补齐。
两版 README 的同一句也少了 CodeArts(v1.7.11 补 --global 时漏改),一并修。

## 3. skill 详情页有 6 条死链

`SKILL.md` 里指向兄弟文件的相对链接(`implementer-prompt.md`、
`../requesting-code-review/code-reviewer.md` 等)站点上根本没有这些 .md:
`../` 开头的直接 404,不带 `./` 的被 md.mjs 的 scheme 白名单打成 `href="#"`
—— 点了没反应,比 404 还难查。

`renderMarkdown(src, base)` 加基址参数,相对链接解析到 GitHub 源文件。
5 个目标地址逐个 curl 过,都是 200。全站扫描:死链 0、`href="#"` 0。

## 4. 加两条门禁,堵住这次的漏法

audit 原来只数文案里的数字,这次两个问题它一个都发现不了。补:

- 官网 `TOOLS` 条目数必须 = TARGETS + 1
- 官网 FAQ 全局清单必须包含 installer 里每个带 `global:` 的工具

反向验证过:删掉 Crush、去掉 CodeArts,两条各自报错。

audit 170 pass / 0 fail(新增 2 条);verify-release 114 pass / 1 fail,
唯一那条是 windsurf.com 返回 429 限流,改动前的干净树上同样失败,与本次无关。
2026-08-28 21:48:01 +08:00

404 lines
17 KiB
Bash
Executable File
Raw Permalink 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.
#!/usr/bin/env bash
# 质量审计脚本 —— 跑 4 类检查防漂移
#
# 1. 静态校验JSON parse / SKILL.md frontmatter / symlink / hook 可执行性
# 2. Installer 功能23 款工具装 / 卸载 / 幂等
# 3. 上游对齐hooks 3 文件 + brainstorm scripts 3 文件 + 14 翻译 skill 结构层级
# 4. 交叉引用README → docs/ 链接 + skill 间引用 + bootstrap 注入路径
#
# 用法:
# bash scripts/audit.sh # 跑全部FAIL > 0 时 exit 1
# bash scripts/audit.sh --quick # 跳过 installer 功能测试
# bash scripts/audit.sh --no-upstream # 跳过上游对齐CI 没 upstream remote 时)
#
# CI 默认在 PR + push to main 跑,发现漂移立刻拦下。
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
QUICK=0
NO_UPSTREAM=0
for arg in "$@"; do
case "$arg" in
--quick) QUICK=1 ;;
--no-upstream) NO_UPSTREAM=1 ;;
esac
done
PASS=0; FAIL=0; WARN=0
declare -a FAILURES=()
declare -a WARNINGS=()
INSTALLER="$ROOT/bin/superpowers-zh.js"
ok() { PASS=$((PASS+1)); }
bad() { FAIL=$((FAIL+1)); FAILURES+=("$1"); echo "$1"; }
warn() { WARN=$((WARN+1)); WARNINGS+=("$1"); echo " ⚠️ $1"; }
hdr() { echo ""; echo "=== $1 ==="; }
# 确保有 upstream remoteCI 上需要 fetch
ensure_upstream() {
if [ "$NO_UPSTREAM" = "1" ]; then return 1; fi
if ! git ls-remote --exit-code upstream HEAD >/dev/null 2>&1; then
if git remote get-url upstream >/dev/null 2>&1; then
git fetch upstream main --depth=50 --quiet 2>/dev/null || return 1
else
git remote add upstream https://github.com/obra/superpowers.git 2>/dev/null
git fetch upstream main --depth=50 --quiet 2>/dev/null || return 1
fi
fi
return 0
}
#==============================================================================
hdr "Category 1: 静态校验"
#==============================================================================
# 1a. JSON parse
while IFS= read -r f; do
if node -e "JSON.parse(require('fs').readFileSync('$f','utf8'))" 2>/dev/null; then
ok
else
bad "JSON parse failure: $f"
fi
done < <(find . -name "*.json" \
-not -path "./node_modules/*" \
-not -path "./.git/*" \
-not -path "./tests/*/node_modules/*")
# 1b. SKILL.md frontmatter 完整性
for f in skills/*/SKILL.md; do
if ! head -1 "$f" | grep -q '^---$'; then
bad "No frontmatter: $f"
continue
fi
fm=$(sed -n '/^---$/,/^---$/p' "$f" | head -20)
for field in name description; do
if ! echo "$fm" | grep -q "^${field}:"; then
bad "Missing frontmatter field '$field': $f"
fi
done
ok
done
# 1c. Symlink 解析
while IFS= read -r l; do
if [ -e "$l" ]; then ok; else bad "Broken symlink: $l"; fi
done < <(find . -type l -not -path "./node_modules/*" -not -path "./.git/*")
# 1d. Hook 脚本可执行权限
for f in hooks/session-start hooks/run-hook.cmd; do
if [ -x "$f" ]; then ok; else bad "Not executable: $f"; fi
done
# 1e. shell 脚本里「变量后紧跟多字节字符」
#
# bash 解析变量名时会把紧随其后的多字节字符吞进名字里,于是 set -u 下直接报
# unbound variable 并中断脚本。本仓已踩过两次,两次都是「脚本半途崩掉、看起来
# 像没跑」。写成花括号界定即可。
#
# 注意实现:这里不能用 grep -P —— macOS 的 /usr/bin/grep 是 BSD grep不支持 -P
# 配上 2>/dev/null 就成了静默失效的检查(本检查第一版正是这么写的,交互 shell 里
# 用 ugrep 看着能用,进了脚本一个都匹配不到)。用 LC_ALL=C + EREC locale 下
# 多字节字符按单字节处理,>0x7F 的字节自然落在 [^ -~] 之外BSD/GNU 都支持。
# 排除注释行,否则本条说明文字自己会被命中。
while IFS= read -r hit; do
case "${hit#*:*:}" in
\#*|" "*\#*) continue ;; # 注释行(含缩进注释)不算
esac
bad "变量后紧跟多字节字符bash 会吞进变量名(用 \${VAR} 界定): $hit"
done < <(LC_ALL=C grep -nE '\$[A-Za-z_][A-Za-z0-9_]*[^ -~]' scripts/*.sh hooks/session-start 2>/dev/null)
ok
#==============================================================================
if [ "$QUICK" != "1" ]; then
hdr "Category 2: Installer 功能测试23 款工具)"
#==============================================================================
declare -a TOOLS=(claude cursor codex kiro deerflow trae antigravity vscode openclaw windsurf gemini aider opencode qwen hermes claw copilot qoder codebuddy codearts cline kilocode crush)
for tool in "${TOOLS[@]}"; do
TMP=$(mktemp -d)
pushd "$TMP" >/dev/null
if ! node "$INSTALLER" --tool "$tool" >/dev/null 2>&1; then
bad "Installer: $tool 安装失败"
popd >/dev/null
rm -rf "$TMP"
continue
fi
# 幂等:再装一遍不应炸
if ! node "$INSTALLER" --tool "$tool" >/dev/null 2>&1; then
bad "Installer: $tool 二次安装失败(幂等性破坏)"
popd >/dev/null
rm -rf "$TMP"
continue
fi
if ! node "$INSTALLER" --uninstall >/dev/null 2>&1; then
bad "Installer: $tool 卸载失败"
else
ok
fi
popd >/dev/null
rm -rf "$TMP"
done
else
echo ""
echo "[--quick 跳过 installer 功能测试]"
fi
#==============================================================================
hdr "Category 3: 上游对齐"
#==============================================================================
if ! ensure_upstream; then
warn "无法访问 upstream跳过对齐检查CI 上请确保有网络)"
else
# 3a. Hooks 3 文件 + cursor manifest
for f in hooks/session-start hooks/hooks.json hooks/run-hook.cmd hooks/hooks-cursor.json; do
d=$(diff <(git show upstream/main:$f 2>/dev/null) "$f" 2>/dev/null | wc -l | tr -d ' ')
if [ "$d" = "0" ]; then ok; else bad "Hooks 漂移: $f ($d 行)"; fi
done
# 3b. Brainstorm scripts 3 文件
for f in skills/brainstorming/scripts/server.cjs \
skills/brainstorming/scripts/start-server.sh \
skills/brainstorming/scripts/stop-server.sh; do
d=$(diff <(git show upstream/main:$f 2>/dev/null) "$f" 2>/dev/null | wc -l | tr -d ' ')
if [ "$d" = "0" ]; then ok; else bad "Brainstorm script 漂移: $(basename $f) ($d 行)"; fi
done
# 3c. 14 翻译 skill 结构层级H1-H4 标题数)
#
# 必须排除 ``` 围栏内的行shell 注释(`# 运行测试`)同样匹配 ^#{1,4}
# 会被当成 markdown 标题数进去。用 grep 直接数的话,一个 skill 里多几行
# bash 注释就能凭空造出「结构漂移」—— executing-plans 与
# finishing-a-development-branch 两条告警此前就是这么来的假阳性。
count_headings() { # 读 stdin只数围栏之外的 H1-H4
awk '/^```/{fence = !fence; next} !fence && /^#{1,4} /{n++} END{print n+0}'
}
declare -a SKILLS=(brainstorming dispatching-parallel-agents executing-plans \
finishing-a-development-branch receiving-code-review requesting-code-review \
subagent-driven-development systematic-debugging test-driven-development \
using-git-worktrees using-superpowers verification-before-completion \
writing-plans writing-skills)
for s in "${SKILLS[@]}"; do
up=$(git show upstream/main:skills/$s/SKILL.md 2>/dev/null | count_headings || echo 0)
our=$(count_headings < "skills/$s/SKILL.md" 2>/dev/null || echo 0)
diff=$((up - our))
abs=${diff#-}
# 允许 3 个 header 差异(翻译造成的合并/拆分小幅波动)
if [ "$abs" -le "3" ]; then
ok
else
warn "Skill 结构漂移: ${s} (上游 H=${up}, 我们 H=${our}) -- 可能 v5.1.0 没跟,或主动扩写"
fi
done
# 3c-bis. fork 增量必须显式声明
#
# 翻译 skill 的标题数应等于「上游标题数 + 本文件里声明的 fork 增量节数」。
# 增量节靠正文里的一行标记声明(见下方 FORK_MARK标记与内容同处一文件
# 不会各自漂移。没打标记就多出章节 = 隐性分叉,下次同步时会被误当成漏译。
FORK_MARK='本节是 superpowers-zh 的增量内容'
for s in "${SKILLS[@]}"; do
up=$(git show upstream/main:skills/$s/SKILL.md 2>/dev/null | count_headings || echo 0)
our=$(count_headings < "skills/$s/SKILL.md" 2>/dev/null || echo 0)
# 注意grep -c 找到 0 个时输出 "0" 但退出码为 1写成 `|| echo 0` 会拼出
# "0\n0",后续整数比较直接报错、检查静默失效。用 `; true` 只吞退出码。
declared=$(grep -c "$FORK_MARK" "skills/$s/SKILL.md" 2>/dev/null; true)
declared=${declared:-0}
delta=$((our - up))
if [ "$delta" = "$declared" ]; then
ok
elif [ "$delta" -gt "$declared" ]; then
bad "未声明的 fork 增量: ${s} 比上游多 ${delta} 节,但只声明了 ${declared} 节 —— 给增量节加上「${FORK_MARK}」标记,或回归上游"
fi
# delta < declared 由 3c 的漂移检查覆盖,此处不重复报
done
# 3d. requesting-code-review/code-reviewer.md 结构v5.1.0 self-contained
up=$(git show upstream/main:skills/requesting-code-review/code-reviewer.md 2>/dev/null | count_headings || echo 0)
our=$(count_headings < skills/requesting-code-review/code-reviewer.md)
diff=$((up - our))
abs=${diff#-}
if [ "$abs" -le "2" ]; then
ok
else
bad "code-reviewer.md 结构漂移 (上游 v5.1.0 self-contained, H=${up}; 我们 H=${our})"
fi
fi
#==============================================================================
hdr "Category 4: 交叉引用完整性"
#==============================================================================
# 4a. README → docs/ 链接
BROKEN=0
while IFS= read -r link; do
link=${link#(}; link=${link%)}
if [ -f "$link" ]; then ok; else
bad "README 链接断: $link"
BROKEN=$((BROKEN+1))
fi
done < <(grep -oE '\(docs/README\.[a-z-]+\.md\)' README.md)
# 4b. Skill 间引用superpowers:xxx
while IFS= read -r line; do
skill_file=$(echo "$line" | cut -d: -f1)
refs=$(echo "$line" | grep -oE '\bsuperpowers:[a-z-]+\b' | sort -u)
for ref in $refs; do
name=${ref#superpowers:}
if [ -d "skills/$name" ]; then ok; else
src=$(basename $(dirname "$skill_file"))
bad "Skill 引用断: $src 引用了不存在的 skills/$name"
fi
done
done < <(grep -rln 'superpowers:' skills/*/SKILL.md 2>/dev/null | \
xargs -I{} grep -H 'superpowers:' {} 2>/dev/null)
# 4c. 装完后 .claude/skills/using-superpowers/SKILL.md 路径必须存在hook 依赖)
TMP=$(mktemp -d)
pushd "$TMP" >/dev/null
if node "$INSTALLER" --tool claude >/dev/null 2>&1; then
if [ -f "$TMP/.claude/skills/using-superpowers/SKILL.md" ]; then
ok
else
bad "装完后 .claude/skills/using-superpowers/SKILL.md 不存在hook 会找不到)"
fi
fi
popd >/dev/null
rm -rf "$TMP"
#==============================================================================
hdr "Category 5: 工具计数一致性"
#==============================================================================
# 文案里宣称的工具数必须与 installer 实际支持的数量一致。
#
# 口径说明installer 的 TARGETS 是「安装目标」Copilot CLI 与 Claude Code 共用
# .claude/skills别名 copilot -> Claude Code在 TARGETS 里不占独立条目,但文案
# 里作为独立产品单独计数 —— 所以「文案工具数 = TARGETS 条目数 + 1」。
#
# 加新工具时最容易漏改文案:计数散落在简繁 README、package.json、CLAUDE.md、
# site/build.mjs、3 份 plugin manifest 十几处。这一类检查专门堵这个。
TARGET_COUNT=$(sed -n '/^const TARGETS = \[/,/^\];/p' "$INSTALLER" | grep -cE "^ \{ name: '")
EXPECTED_TOOLS=$((TARGET_COUNT + 1))
echo " installer TARGETS = $TARGET_COUNT 个安装目标 -> 文案应宣称 $EXPECTED_TOOLS"
# check_count <文件> <正则> <说明>:正则匹配到的所有数字都必须等于 EXPECTED_TOOLS
check_count() {
local file="$1" re="$2" label="$3" got n
if [ ! -f "$file" ]; then bad "计数检查: $file 不存在"; return; fi
got=$(grep -oE "$re" "$file" 2>/dev/null | grep -oE '[0-9]+' | sort -u)
if [ -z "$got" ]; then
bad "计数检查: ${file} 里没匹配到「${label}」—— 文案改动过?正则需同步更新"
return
fi
for n in $got; do
if [ "$n" != "$EXPECTED_TOOLS" ]; then
bad "计数不一致: ${file}${label}」= ${n},应为 ${EXPECTED_TOOLS}"
return
fi
done
ok
}
# ── 简体 README ──
check_count README.md '等 \*\*[0-9]+ 款 AI 编程工具\*\*' '首屏标语'
check_count README.md '\*\*[0-9]+ 款\*\*:上述' '对比表支持工具'
check_count README.md '\*\*\+\*\* [0-9]+ 款工具一键适配' '一句话总结'
check_count README.md '### 🤖 支持 [0-9]+ 款主流' '工具表标题'
check_count README.md '等 [0-9]+ 款工具真正会干活' '页脚标语'
check_count README.md 'across [0-9]+ AI coding tools' '英文简介'
# ── 繁體 README ──
check_count README.zh-Hant.md '等 \*\*[0-9]+ 款 AI 編程工具\*\*' '首屏標語'
check_count README.zh-Hant.md '\*\*[0-9]+ 款\*\*:上述' '對比表支援工具'
check_count README.zh-Hant.md '\*\*\+\*\* [0-9]+ 款工具一鍵適配' '一句話總結'
check_count README.zh-Hant.md '### 🤖 支援 [0-9]+ 款主流' '工具表標題'
check_count README.zh-Hant.md '等 [0-9]+ 款工具真正會幹活' '頁腳標語'
check_count README.zh-Hant.md 'across [0-9]+ AI coding tools' '英文簡介'
# ── 元数据与 manifest ──
check_count package.json '等 [0-9]+ 款工具' 'npm description'
check_count CLAUDE.md '支持 [0-9]+ 款 IDE/CLI' '贡献者指南'
check_count .claude-plugin/plugin.json '等 [0-9]+ 款工具' 'plugin manifest'
check_count .claude-plugin/marketplace.json '等 [0-9]+ 款工具' 'marketplace manifest'
check_count .cursor-plugin/plugin.json '等 [0-9]+ 款工具' 'cursor manifest'
# ── 官网(三语言) ──
check_count site/build.mjs '[0-9]+ 款 AI 编程工具装上' '官网简体标语'
check_count site/build.mjs '[0-9]+ 款 AI 編程工具裝上' '官网繁體標語'
check_count site/build.mjs '共 [0-9]+ 款:' '官网 FAQ 枚举'
check_count site/build.mjs 'into [0-9]+ AI coding tools' '官网英文标语'
check_count site/build.mjs '^ \{ q: .Which AI coding tools are supported.*[0-9]+ tools:' '官网英文 FAQ'
# ── 结构性检查README 工具表的实际行数 ──
rows=$(awk '/^### 🤖 支持 [0-9]+ 款主流/,/^> 运行 `npx/' README.md | grep -cE '^\| \[')
if [ "$rows" = "$EXPECTED_TOOLS" ]; then ok; else
bad "README 工具表有 $rows 行,应为 $EXPECTED_TOOLS 行(漏加/多加了表格行)"
fi
# ── 结构性检查:官网工具墙 / 安装命令下拉的条目数 ──
# 计数文案对了不代表列表对了v1.7.10 时三处文案都写着 23而 site/build.mjs 的
# TOOLS 只有 20 条 —— 首页统计块显示「20 支持工具」、下拉里根本找不到
# Cline / Kilo Code / Crush。这条专门卡列表本身。
site_tools=$(sed -n '/^const TOOLS = \[/,/^\];/p' site/build.mjs | grep -cE "^ \{ name: '")
if [ "$site_tools" = "$EXPECTED_TOOLS" ]; then ok; else
bad "官网 TOOLS 有 ${site_tools} 条,应为 ${EXPECTED_TOOLS}site/build.mjs 漏加工具)"
fi
# ── 一致性检查:官网 FAQ 的「支持全局」清单必须与 installer 的 global 目标一致 ──
# 同理CodeArts / Crush / Hermes / CodeBuddy 陆续拿到 --global官网 FAQ 一直停在 7 款。
global_targets=$(sed -n '/^const TARGETS = \[/,/^\];/p' "$INSTALLER" | grep -E "^ \{ name: '" | grep 'global:' | sed -E "s/^ \{ name: '([^']+)'.*/\1/")
global_n=$(echo "$global_targets" | grep -c .)
faq_line=$(grep -n '支持全局的工具' site/build.mjs | head -1 | cut -d: -f2-)
missing=""
while IFS= read -r g; do
case "$faq_line" in *"$g"*) ;; *) missing="$missing $g";; esac
done <<< "$global_targets"
if [ -n "$missing" ]; then
bad "官网 FAQ 全局清单漏了:${missing}installer 共 ${global_n} 款支持 --global"
else
ok
fi
# ── 自检Category 2 测的工具数应等于宣称数(宣称了就必须测) ──
tools_tested=$(grep -oE '^declare -a TOOLS=\(.*\)' "$0" | sed -E 's/^declare -a TOOLS=\(//; s/\)$//' | wc -w | tr -d ' ')
if [ "$tools_tested" = "$EXPECTED_TOOLS" ]; then ok; else
bad "Category 2 只测了 $tools_tested 款,但文案宣称 $EXPECTED_TOOLS 款(宣称的工具必须都有回归测试)"
fi
#==============================================================================
echo ""
echo "=========================================="
echo "📊 审计结果"
echo "=========================================="
echo "✅ PASS: $PASS"
echo "⚠️ WARN: $WARN"
echo "❌ FAIL: $FAIL"
if [ "$WARN" -gt 0 ]; then
echo ""
echo "Warnings不阻塞"
for w in "${WARNINGS[@]}"; do echo " ⚠️ $w"; done
fi
if [ "$FAIL" -gt 0 ]; then
echo ""
echo "Failures必须修"
for f in "${FAILURES[@]}"; do echo "$f"; done
echo ""
echo "❌ Audit 失败:$FAIL 个 P0 问题。看 README 「质量审计」段了解每项含义。"
exit 1
fi
echo ""
echo "✅ Audit 通过"
exit 0