Files
superpowers-zh/scripts/audit.sh
AI不止语 8a2f37071a feat(installer): 新增 Crush 适配(23 款工具,#40);补齐 verify-release 覆盖盲区
## Crush(#40)

查权威源(charmbracelet/crush repo 文档)确认:Crush 遵循 Agent Skills 开放标准,
项目级**自动发现** .crush/skills、.agents/skills、.claude/skills、.cursor/skills
四个目录,无需任何配置;用户级为 ~/.config/crush/skills。

重要副作用写进了 docs:已经为 CC / Cursor / Codex / Antigravity 装过的用户,
Crush 现在就已经能读到那些 skills,此时不要再装 --tool crush,否则同一批
skills 会有两份、被重复加载。docs 里给了确认命令。

- TARGETS 加 Crush(detect: .crush / crush.json / .crush.json),支持 --global
- CLI_PROBES 加 crush(它是 CLI,PATH 探得到)
- skills-only 适配,不需要 bootstrap;不动用户的 crush.json
- 新增 docs/README.crush.md
- 计数 22 -> 23(installer TARGETS 22 条 + Copilot CLI 单独计)

实测:装 20 skills / 二次装幂等 / 卸载零残留且正确保留用户的 crush.json;
--global 装到 ~/.config/crush/skills 并可干净卸载。

## audit Category 5 当场抓到我漏改的 5 处

加计数时忘了 site/build.mjs 的 5 个位置(三语言标语 + FAQ 枚举)。上个版本
新加的计数一致性检查直接 FAIL 拦下 —— 这是它第一次在真实改动中生效。已补
site 18 处并重建,三语言首页 23 计数、0 残留、Crush 均已出现。

## verify-release.sh 有同样的漂移问题,已修

它有自己独立的覆盖清单,加新工具时不进去就静默不测(本次 Crush 就是:
分数仍是 82,因为压根没测它)。

- SPEC / DETECT / GLOBAL_OK 三处补 Crush
- 顺带发现 B 段还漏了 3 个工具的检测标记:DeerFlow(deer_flow)、
  VS Code(.github/copilot-instructions.md)、Gemini CLI(GEMINI.md)——
  后两个是文件而非目录,DETECT 循环原先一律 mkdir,现按扩展名区分创建方式
- 新增 G 段自检:SPEC 覆盖数必须等于 installer 宣称的工具数;
  DETECT 覆盖的工具名集合必须包含全部 TARGETS 名字(用集合比对而非数条数 ——
  一个工具可以有多个检测标记,数数会误判)

覆盖从 82 项升到 90 项。

回归:audit.sh 152 pass / 0 warn / 0 fail、verify-release.sh 90 pass / 0 fail
2026-08-08 06:14:48 +08:00

339 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 功能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
#==============================================================================
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
# 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
# ── 自检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