mirror of
https://github.com/jnMetaCode/superpowers-zh.git
synced 2026-09-02 22:54:06 +08:00
审核 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
165 lines
7.3 KiB
Bash
Executable File
165 lines
7.3 KiB
Bash
Executable File
#!/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. --global:7 款应成功,其余应明确拒绝且退出码 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 frontmatter(Cline 只支持 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 "✅ 深度验证全部通过"
|