Files
superpowers-zh/scripts/verify-release.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

165 lines
7.3 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
#
# verify-release.sh — 发版前深度验证。
#
# 与 audit.sh 的分工audit.sh 只看 installer 的退出码,装完到底有没有东西落盘、
# 卸载有没有留垃圾它都不查。本脚本补上这些断言,发 npm 之前跑一次。
#
# bash scripts/verify-release.sh
#
# 覆盖:
# A. 22 款工具:装 -> 断言 skill 数落盘 -> 二次装幂等 -> 卸载零残留 + 无嵌套
# B. 每个检测标记只触发预期工具(防新增工具时误触发既有工具)
# C. --global 白名单成功 / 其余明确拒绝且退出码 1
# D. --global 拒绝信息引用的 docs 文件真实存在(防死链)
# E. rules 型工具Cline / Kilo Code的 bootstrap 索引内容断言
# F. PATH 探测在 PATH 为空 / 畸形 / 未定义时的健壮性
set -uo pipefail
REPO="$(cd "$(dirname "$0")/.." && pwd)"
INS="$REPO/bin/superpowers-zh.js"
NODE=$(command -v node) # 绝对路径F 段要清空 PATH用裸 node 会 127
EXPECT_SKILLS=$(ls -d "$REPO"/skills/*/ | wc -l | tr -d ' ')
PASS=0; FAIL=0
declare -a FAILURES=()
ok() { PASS=$((PASS+1)); }
bad() { FAIL=$((FAIL+1)); FAILURES+=("$1"); printf ' ❌ %s\n' "$1"; }
# 工具别名 -> 期望的 skills 目录(相对项目根)
declare -a SPEC=(
"claude:.claude/skills" "cursor:.cursor/skills" "codex:.codex/skills"
"kiro:.kiro/steering" "deerflow:skills/custom" "trae:.trae/skills"
"antigravity:.agents/skills" "vscode:.github/superpowers" "openclaw:skills"
"windsurf:.windsurf/skills" "gemini:.gemini/skills" "aider:.aider/skills"
"opencode:.opencode/skills" "qwen:.qwen/skills" "hermes:.hermes/skills"
"claw:.claw/skills" "copilot:.claude/skills" "qoder:.qoder/skills"
"codebuddy:.codebuddy/skills" "codearts:.codeartsdoer/skills"
"cline:.cline/skills" "kilocode:.kilocode/skills"
)
echo "═══ 期望每款装入 $EXPECT_SKILLS 个 skill ═══"
echo ""
echo "─── A. 项目级:装 / 落盘断言 / 幂等 / 卸载无残留22 款)───"
for entry in "${SPEC[@]}"; do
tool="${entry%%:*}"; sdir="${entry#*:}"
T=$(mktemp -d); cd "$T"
if ! node "$INS" --tool "$tool" >/dev/null 2>&1; then
bad "$tool: 安装退出码非 0"; rm -rf "$T"; continue
fi
n=$(ls -d "$T/$sdir"/*/ 2>/dev/null | wc -l | tr -d ' ')
if [ "$n" != "$EXPECT_SKILLS" ]; then
bad "$tool: $sdir 里是 $n 个 skill期望 $EXPECT_SKILLS"; rm -rf "$T"; continue
fi
# 嵌套 bug 检查:不应出现 skills/skills 之类
if find "$T" -type d -path "*/skills/skills" 2>/dev/null | grep -q .; then
bad "$tool: 出现嵌套 skills/skills 目录"
fi
node "$INS" --tool "$tool" >/dev/null 2>&1
n2=$(ls -d "$T/$sdir"/*/ 2>/dev/null | wc -l | tr -d ' ')
if [ "$n2" != "$EXPECT_SKILLS" ]; then
bad "$tool: 二次安装后变成 $n2 个(幂等性破坏)"; rm -rf "$T"; continue
fi
if ! node "$INS" --uninstall >/dev/null 2>&1; then
bad "$tool: 卸载退出码非 0"; rm -rf "$T"; continue
fi
left=$(find "$T" -type f 2>/dev/null | wc -l | tr -d ' ')
if [ "$left" != "0" ]; then
bad "$tool: 卸载后残留 $left 个文件: $(find "$T" -type f | head -3 | tr '\n' ' ')"
else
ok
fi
cd /; rm -rf "$T"
done
echo ""
echo "─── B. 自动检测:每个标记只触发预期工具 ───"
declare -a DETECT=(
".claude:Claude Code" ".cursor:Cursor" ".codex:Codex CLI"
".kiro:Kiro" ".trae:Trae" ".agents:Antigravity"
".openclaw:OpenClaw" ".windsurf:Windsurf" ".aider:Aider"
".opencode:OpenCode" ".qwen:Qwen Code" ".hermes:Hermes Agent"
".claw:Claw Code" ".qoder:Qoder" ".codebuddy:CodeBuddy"
".codeartsdoer:CodeArts" ".clinerules:Cline" ".kilocode:Kilo Code"
".kilo:Kilo Code"
)
for entry in "${DETECT[@]}"; do
marker="${entry%%:*}"; want="${entry#*:}"
T=$(mktemp -d); cd "$T"; mkdir -p "$marker"
got=$(node "$INS" 2>&1 | grep -oE '✅ [A-Za-z][A-Za-z ]*(\[|:)' | sed 's/✅ //; s/ *[:[]$//' | sort -u | tr '\n' ',' | sed 's/,$//')
if [ "$got" != "$want" ]; then
bad "检测 ${marker}/ -> 得到「${got}」,期望「${want}"
else
ok
fi
cd /; rm -rf "$T"
done
echo ""
echo "─── C. --global7 款应成功,其余应明确拒绝且退出码 1 ───"
declare -a GLOBAL_OK=(claude codex openclaw windsurf opencode qwen qoder)
declare -a GLOBAL_NO=(cursor kiro trae aider deerflow vscode hermes claw gemini antigravity codebuddy codearts cline kilocode)
for tool in "${GLOBAL_OK[@]}"; do
H=$(mktemp -d)
if HOME="$H" node "$INS" --global --tool "$tool" >/dev/null 2>&1; then ok; else bad "--global $tool 应成功但失败"; fi
rm -rf "$H"
done
for tool in "${GLOBAL_NO[@]}"; do
H=$(mktemp -d)
out=$(HOME="$H" node "$INS" --global --tool "$tool" 2>&1); rc=$?
if [ $rc -eq 0 ]; then bad "--global $tool 应拒绝但成功了"
elif ! echo "$out" | grep -q "不支持通用全局安装"; then bad "--global $tool 拒绝信息缺失"
else ok; fi
rm -rf "$H"
done
echo ""
echo "─── D. --global 拒绝信息里引用的 docs 文件必须存在 ───"
for slug in gemini-cli antigravity trae aider hermes kiro cline kilocode; do
if [ -f "$REPO/docs/README.$slug.md" ]; then ok; else bad "docs/README.$slug.md 不存在(拒绝信息会指向死链)"; fi
done
echo ""
echo "─── E. 新工具 bootstrap 索引内容断言 ───"
T=$(mktemp -d); cd "$T"; node "$INS" --tool cline >/dev/null 2>&1
R="$T/.clinerules/superpowers-zh.md"
[ -f "$R" ] && ok || bad "Cline 索引文件未生成"
head -1 "$R" | grep -q '^---' && bad "Cline 索引不该有 YAML frontmatterCline 只支持 paths 字段)" || ok
rows=$(grep -cE '^\| [a-z][a-z0-9-]+ \|' "$R")
[ "$rows" = "$EXPECT_SKILLS" ] && ok || bad "Cline 索引表 $rows 行,期望 $EXPECT_SKILLS"
grep -qE '^\| [a-z0-9-]+ \|\s*\|$' "$R" && bad "Cline 索引有空 description 的行" || ok
grep -q '\.cline/skills/' "$R" && ok || bad "Cline 索引未指向 .cline/skills/"
cd /; rm -rf "$T"
T=$(mktemp -d); cd "$T"; node "$INS" --tool kilocode >/dev/null 2>&1
R="$T/.kilocode/rules/superpowers-zh.md"
[ -f "$R" ] && ok || bad "Kilo 索引文件未生成"
rows=$(grep -cE '^\| [a-z][a-z0-9-]+ \|' "$R")
[ "$rows" = "$EXPECT_SKILLS" ] && ok || bad "Kilo 索引表 $rows 行,期望 $EXPECT_SKILLS"
grep -q '\.kilocode/skills/' "$R" && ok || bad "Kilo 索引未指向 .kilocode/skills/"
cd /; rm -rf "$T"
echo ""
echo "─── F. PATH 探测健壮性issue #48 新代码)───"
T=$(mktemp -d); cd "$T"
out=$(PATH="" "$NODE" "$INS" 2>&1); rc=$?
[ $rc -eq 1 ] && ok || bad "PATH 为空时应退出码 1得到 $rc"
echo "$out" | grep -q "未在当前目录检测到" && ok || bad "PATH 为空时缺少检测落空提示"
out=$(PATH="/nonexistent:::/also/missing" "$NODE" "$INS" 2>&1); rc=$?
[ $rc -eq 1 ] && ok || bad "PATH 含空段/不存在目录时应优雅退出 1得到 $rc"
echo "$out" | grep -qi "error\|Traceback\|ENOENT" && bad "PATH 异常时输出了未捕获错误" || ok
cd /; rm -rf "$T"
echo ""
echo "═══════════════════════════════════"
echo "✅ PASS: $PASS ❌ FAIL: $FAIL"
if [ $FAIL -gt 0 ]; then
echo ""
echo "失败项:"
for f in "${FAILURES[@]}"; do echo " - $f"; done
exit 1
fi
echo "✅ 深度验证全部通过"