Files
superpowers-zh/scripts/audit.sh
AI不止语 1321cc65fb test(audit): 新增工具计数一致性检查;修 bash 3.2 下 CJK 报错信息乱码
审核 v1.7.2 时发现三个问题,一并修掉。

1) 工具计数散落十几处,没有任何检查兜着(audit 新增 Category 5,+24 项)

加一款工具要同步简繁 README(各 6 处)、package.json、CLAUDE.md、
site/build.mjs(三语言 15 处)、3 份 plugin manifest —— 这次全靠人工 grep 才
没漏。现在改为从 installer 的 TARGETS 推导期望值并逐处断言。

口径写进注释:TARGETS 是「安装目标」共 21 个;Copilot CLI 与 CC 共用
.claude/skills(别名 copilot 映射过去)不占独立条目,但文案里作为独立产品
单独计数,所以「文案数 = TARGETS 数 + 1 = 22」。

除逐处计数外还加了两条结构性断言:
- README 工具表的实际表格行数必须等于宣称数(防漏加/多加行)
- Category 2 实际回归测试的工具数必须等于宣称数(宣称了就必须测)

已验证这个检查真的会拦:把标题改成 23 款、删掉一行工具表,两种漂移都被抓到。

2) bash 3.2 下 CJK 报错信息乱码(真 bug,只在检查失败时才暴露)

macOS 自带 bash 3.2.57 解析标识符时不识别多字节字符,`$file「` 会把「的首字节
(0xE3) 吞进变量名,导致变量展开为空且字符被截断 —— 报错信息变成
「计数不一致: ����= ��应为 22」。写检查的人看不到,因为只有失败时才打印。

复现:bash -c 'f=README.md; echo "文件: $f「标签」结束"' -> 文件: ��标签」结束

修法是给紧跟 CJK 标点的变量加花括号。全仓扫描 `$var` 紧跟非 ASCII 字节的模式,
命中 3 处:audit.sh 2 处(本次新增)、verify-release.sh 1 处(上个提交带进来的,
因为检测项一直全通过所以从没暴露)。修后报错信息可读:
  计数不一致: README.md「工具表标题」= 23,应为 22
  检测 .clinerules/ -> 得到「Cline」,期望「错误期望值」

3) .gitignore 漏了新工具的安装产物

.trae/、.qoder/ 等都在忽略列表里,但 .cline/、.clinerules/、.kilocode/ 没加 ——
在本仓库内跑安装器自测会留一堆未跟踪文件。已补上并验证 git status 干净。

验证:audit.sh 154 pass / 0 fail(原 130,+24 为 Category 5)
      verify-release.sh 82 pass / 0 fail
2026-08-07 21:54:48 +08:00

331 lines
13 KiB
Bash
Executable File
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.
#!/usr/bin/env bash
# 质量审计脚本 —— 跑 4 类检查防漂移
#
# 1. 静态校验JSON parse / SKILL.md frontmatter / symlink / hook 可执行性
# 2. Installer 功能22 款工具装 / 卸载 / 幂等
# 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
#==============================================================================
if [ "$QUICK" != "1" ]; then
hdr "Category 2: Installer 功能测试22 款工具)"
#==============================================================================
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)
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 标题数)
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 | grep -cE '^#{1,4} ' || echo 0)
our=$(grep -cE '^#{1,4} ' "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
# 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 | grep -cE '^#{1,3} ' || echo 0)
our=$(grep -cE '^#{1,3} ' 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
# ── 自检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