#!/usr/bin/env sh # sync-skill-manifest.sh — 同步 domain 的 README「Skills 目錄」區塊,並把該 domain 所有 # plugin manifest 的 version 各 bump 一次,且 major 可超過 9;minor 與 patch 仍是單位數。 # # 用法: sync-skill-manifest.sh # # README 區塊標記: ... # 每個技能一個「### `{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 ' >&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='' 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. 收集目前的技能清單 (namedescription),寫入檔案以避免 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