Files
meta/tools/sync-skill-manifest.sh
T
jiantw83 7dc5c32de3 fix(tools): 改用共用推導取得技能組根目錄,並補齊結束碼宣告
以 plugin 形式安裝時,腳本會落在 CLI 的 plugin 快取目錄。
六支腳本原本一律取「腳本位置的上兩層」當技能組根目錄,這時一定推錯。
這一輪例行稽核的第一步就實際踩到:不先手動設環境變數,腳本根本跑不動,
而叫用它們的技能說明也沒提要設,等於留了一個必炸的預設值。

把推導規則抽成共用腳本,依序試環境變數、從目前目錄往上找、
腳本位置的上兩層、家目錄底下的 plugins,並以 gitea.sh 在不在當判準。
marketplace 每個 domain 存取庫都帶一份,單看它會把 domain 誤判成根。
推不出來就結束並指名要設環境變數,同時列出試過的每一個候選,
呼叫端一眼看得出要設什麼。只 clone 單一存取庫的環境留了逃生門:
環境變數有設且是目錄就照用,並在錯誤輸出提醒。

順手補上語言檢查與 manifest 同步兩支腳本的結束碼宣告與環境變數說明。
沒有宣告,呼叫端只能猜;猜錯就把失敗當成功。
2026-08-31 11:11:12 +08:00

195 lines
6.7 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 一次,且 major 可超過 9;minor 與 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 版本為準,
# 右側數字加一,滿 9 就往左進位;major 不設上限,minor 與 patch 都不超過 9)。
#
# 輸出: 變更摘要——README 新增或移除的技能小節、各 manifest 的舊版本 -> 新版本(stdout);
# 失敗說明走 stderr。
# 結束碼: 0=README 區塊與三份 manifest 都已同步
# 1=domain 路徑不存在、缺 skills/、缺 README.md、README 缺 JSC-SKILLS 標記、
# 掃不到任何 SKILL.md、找不到 plugin manifest,或 manifest 讀不到 version 欄位
# 2=用法錯誤(本腳本只吃一個參數)
# 本腳本是 set -e:上列以外的指令失敗會直接中止,結束碼由該指令決定,
# 呼叫端把「不是 0、1、2」一律當執行環境故障處理,不得視為同步成功。
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)
major=$((major + 0))
minor=$((minor + 0))
patch=$((patch + 1))
while [ "$patch" -gt 9 ]; do
patch=$((patch - 10))
minor=$((minor + 1))
done
while [ "$minor" -gt 9 ]; do
minor=$((minor - 10))
major=$((major + 1))
done
new="$major.$minor.$patch"
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