Files
meta/tools/sync-skill-manifest.sh
T
jiantw83 4454d62e82 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 五支技能共用的機械流程去重。
2026-08-24 14:49:23 +08:00

175 lines
5.8 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 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