feat(meta): 抽出技能目錄同步與引用查找為共用工具

什麼:新增 tools/sync-skill-manifest.sh(重建 domain README 的「Skills 目錄」區塊並將三份 plugin manifest 的版本一併 bump)與 tools/find-skill-refs.sh(掃描所有 jsc domain 存取庫,列出技能名稱或 /jsc-{domain}:{name} 指令形式的引用檔案),並更新 skill-check、skill-delete、skill-new、skill-update、skillset-update 五支 SKILL.md 改為呼叫這兩支工具。skill-delete 與 skill-update 的 frontmatter description 也一併收斂在此次改動內,配合本次流程調整精簡文字。
為何:五支技能先前各自以近乎相同的內嵌文字描述「同步 README、bump 版本」的機械流程,重複維護成本高且容易漂移;skill-delete 的引用盤點步驟先前只是一段沒有工具、也沒有標明子代理人的裸 grep 指示,稽核時難以驗證涵蓋範圍。
如何:把兩段機械流程各自抽成一支冪等 shell 工具,五支技能的對應步驟改為「子代理人只做內容判斷與修改,機械式同步與版本 bump 一律呼叫工具」;skill-delete 步驟 4 改呼叫 find-skill-refs.sh 取代裸 grep 說明。
負責範圍:jsc-meta 技能組自我管理,skill-check、skill-delete、skill-new、skill-update、skillset-update 五支技能共用的機械流程去重。
This commit is contained in:
2026-08-24 14:49:23 +08:00
parent 720ec6194e
commit 4454d62e82
7 changed files with 217 additions and 10 deletions
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env sh
# find-skill-refs.sh — 掃描一個技能在所有 jsc domain 存取庫裡的引用,供刪除技能前盤點用。
#
# 用法: find-skill-refs.sh <domain> <skill-name>
#
# 掃描樣式:
# 1. 技能名稱字面 {skill-name}
# 2. 指令形式 /jsc-{domain}:{skill-name}
# 這兩個樣式已涵蓋其他 SKILL.md 的引用、domain README 的「Skills 目錄」小節、
# plugins/jsc 兩份 marketplace.json 與其在各 domain 存取庫的同步副本、tools/ 腳本、
# jsc-hooks 接線等位置——只要檔案裡出現技能名稱或指令形式就會被列出。
#
# 掃描範圍: /root/plugins 底下所有 domain 存取庫,排除 .git 目錄。
# 輸出: 命中檔案清單(去重、排序),一行一個路徑;exit 0 一律成功,即使沒有命中。
set -eu
usage() {
echo 'usage: find-skill-refs.sh <domain> <skill-name>' >&2
exit 2
}
[ "$#" -eq 2 ] || usage
DOMAIN=$1
SKILL=$2
ROOT=/root/plugins
[ -d "$ROOT" ] || { echo "找不到 plugins 根目錄: $ROOT" >&2; exit 1; }
PATTERN1="$SKILL"
PATTERN2="/jsc-${DOMAIN}:${SKILL}"
grep -rlE --exclude-dir=.git -e "$PATTERN1" -e "$PATTERN2" "$ROOT" 2>/dev/null | sort -u
exit 0
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env sh
# sync-skill-manifest.sh — 同步 domain 的 README「Skills 目錄」區塊,並把該 domain 所有
# plugin manifest 的 version 各 bump 一個 patch。
#
# 用法: sync-skill-manifest.sh <domain-path>
#
# README 區塊標記: <!-- JSC-SKILLS:START --> ... <!-- JSC-SKILLS:END -->
# 每個技能一個「### `{name}`」小節。若小節已存在就保留原文——人工撰寫的 STE100 繁中說明
# 是唯一真實來源,本工具不覆寫既有文字;只新增缺少技能的小節(暫填 SKILL.md 的 description,
# 需要人工補譯成 STE100 繁中),並移除已刪除技能的小節。
#
# manifest: 依序找出 domain 下存在的 plugin.json、.claude-plugin/plugin.json、
# .codex-plugin/plugin.json,version 一律 bump 成同一個新值(以第一份找到的 manifest 版本為準,
# patch 位加一;major.minor 不變)。
#
# 輸出: 變更摘要——README 新增/移除的技能小節、各 manifest 的舊版本 -> 新版本。
set -eu
usage() {
echo 'usage: sync-skill-manifest.sh <domain-path>' >&2
exit 2
}
[ "$#" -eq 1 ] || usage
DOMAIN=$1
DOMAIN=${DOMAIN%/}
[ -d "$DOMAIN" ] || { echo "找不到目錄: $DOMAIN" >&2; exit 1; }
[ -d "$DOMAIN/skills" ] || { echo "找不到 skills/: $DOMAIN/skills" >&2; exit 1; }
README="$DOMAIN/README.md"
[ -f "$README" ] || { echo "找不到 README.md: $README" >&2; exit 1; }
START='<!-- JSC-SKILLS:START -->'
END='<!-- JSC-SKILLS:END -->'
grep -qF "$START" "$README" && grep -qF "$END" "$README" || {
echo "README.md 缺少 $START / $END 標記: $README" >&2
exit 1
}
TAB="$(printf '\t')"
WORKDIR=$(mktemp -d)
trap 'rm -rf "$WORKDIR"' EXIT
# ---- 1. 收集目前的技能清單 (name<TAB>description),寫入檔案以避免 shell 變數處理多行字串 ----
SKILLS_TSV="$WORKDIR/skills.tsv"
: > "$SKILLS_TSV"
for f in "$DOMAIN"/skills/*/SKILL.md; do
[ -f "$f" ] || continue
name=$(awk 'BEGIN{f=0} /^---$/{f++; next} f==1 && /^name:/{sub(/^name: */,""); print; exit}' "$f")
desc=$(awk 'BEGIN{f=0} /^---$/{f++; next} f==1 && /^description:/{sub(/^description: */,""); print; exit}' "$f")
[ -n "$name" ] || continue
printf '%s\t%s\n' "$name" "$desc" >> "$SKILLS_TSV"
done
[ -s "$SKILLS_TSV" ] || { echo "找不到任何技能: $DOMAIN/skills/*/SKILL.md" >&2; exit 1; }
NAMES_ONLY="$WORKDIR/names.txt"
cut -f1 "$SKILLS_TSV" > "$NAMES_ONLY"
# ---- 2. 取出既有 README 區塊 ----
OLD_BLOCK="$WORKDIR/old-block.md"
awk -v s="$START" -v e="$END" 'index($0,s){f=1;next} index($0,e){f=0} f' "$README" > "$OLD_BLOCK"
OLD_NAMES="$WORKDIR/old-names.txt"
awk -F'`' '/^### `/{print $2}' "$OLD_BLOCK" > "$OLD_NAMES"
# ---- 3. 逐技能組出新區塊內容 ----
# 順序規則:保留 README 既有小節的原順序(人工排過的邏輯順序是唯一真實來源),
# 只在既有順序之後補上新增的技能;不因掃描順序(字母序)打亂既有排列。
NEW_BLOCK="$WORKDIR/new-block.md"
: > "$NEW_BLOCK"
ADDED_FILE="$WORKDIR/added.txt"
: > "$ADDED_FILE"
emit_skill() {
name="$1"
desc=$(awk -F"$TAB" -v n="$name" '$1==n{print $2; exit}' "$SKILLS_TSV")
BODY="$WORKDIR/body.md"
awk -v n="$name" '
BEGIN{hdr="### `" n "`"; found=0}
$0==hdr {found=1; print; next}
found && /^### `/{exit}
found{print}
' "$OLD_BLOCK" > "$BODY"
if [ -s "$BODY" ]; then
cat "$BODY" >> "$NEW_BLOCK"
else
printf '### `%s`\n\n%s\n' "$name" "$desc" >> "$NEW_BLOCK"
echo "$name" >> "$ADDED_FILE"
fi
printf '\n' >> "$NEW_BLOCK"
}
# 3a. 先照舊順序輸出仍存在的技能
while read -r name; do
[ -n "$name" ] || continue
grep -qxF "$name" "$NAMES_ONLY" || continue
emit_skill "$name"
done < "$OLD_NAMES"
# 3b. 再補上舊區塊沒有的新技能(依掃描順序)
while IFS="$TAB" read -r name desc; do
[ -n "$name" ] || continue
grep -qxF "$name" "$OLD_NAMES" && continue
emit_skill "$name"
done < "$SKILLS_TSV"
# 擠掉連續空白行,只留單一空白行
SQUEEZED="$WORKDIR/squeezed.md"
awk 'BEGIN{blank=0} /^[ \t]*$/{blank++; if(blank>1) next} !/^[ \t]*$/{blank=0} {print}' "$NEW_BLOCK" > "$SQUEEZED"
REMOVED_FILE="$WORKDIR/removed.txt"
: > "$REMOVED_FILE"
while read -r n; do
[ -n "$n" ] || continue
if ! grep -qxF "$n" "$NAMES_ONLY"; then
echo "$n" >> "$REMOVED_FILE"
fi
done < "$OLD_NAMES"
# ---- 4. 寫回 README(區塊前後各留一行空白,比照既有格式) ----
NEW_README="$WORKDIR/README.md"
awk -v s="$START" -v e="$END" -v blockfile="$SQUEEZED" '
index($0,s){
print
print ""
while ((getline line < blockfile) > 0) print line
f=1
next
}
index($0,e){f=0}
!f{print}
' "$README" > "$NEW_README"
mv "$NEW_README" "$README"
# ---- 5. bump manifest version(三份一致) ----
MANIFESTS=""
for m in "$DOMAIN/plugin.json" "$DOMAIN/.claude-plugin/plugin.json" "$DOMAIN/.codex-plugin/plugin.json"; do
[ -f "$m" ] && MANIFESTS="$MANIFESTS $m"
done
[ -n "$MANIFESTS" ] || { echo "找不到任何 plugin manifest: $DOMAIN" >&2; exit 1; }
first=""
for m in $MANIFESTS; do first=$m; break; done
old=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' "$first" | head -1)
[ -n "$old" ] || { echo "找不到 version 欄位: $first" >&2; exit 1; }
major=$(printf '%s' "$old" | cut -d. -f1)
minor=$(printf '%s' "$old" | cut -d. -f2)
patch=$(printf '%s' "$old" | cut -d. -f3)
new="$major.$minor.$((patch + 1))"
echo "== manifest version =="
for m in $MANIFESTS; do
cur=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' "$m" | head -1)
sed -i.bak "s/\"version\": *\"$cur\"/\"version\": \"$new\"/" "$m"
rm -f "$m.bak"
printf '%s: %s -> %s\n' "$m" "$cur" "$new"
done
echo "== README Skills 目錄 (${README}) =="
if [ -s "$ADDED_FILE" ]; then
printf '新增小節:'; tr '\n' ' ' < "$ADDED_FILE"; printf '\n'
else
echo '新增小節: 無'
fi
if [ -s "$REMOVED_FILE" ]; then
printf '移除小節:'; tr '\n' ' ' < "$REMOVED_FILE"; printf '\n'
else
echo '移除小節: 無'
fi