#!/usr/bin/env sh # sync-marketplace.sh — 把一個 plugin 條目寫進正本 marketplace,再同步到所有 domain 存取庫。 # # 用法: sync-marketplace.sh # 例: sync-marketplace.sh doc https://gitea.jsc.idv.tw/plugins/doc.git "文件產出與同步" # # 行為: # 1. 在 plugins/meta 的兩份正本(.claude-plugin/marketplace.json 與 # .agents/plugins/marketplace.json)插入或更新 jsc-{domain} 條目;已存在就覆寫網址與描述。 # 條目依 name 排序,格式維持兩格縮排、不轉義非 ASCII、結尾一個換行。 # 2. 兩份正本與所有副本都由同一份算好的內容複製過去,保證位元組完全一致。 # 3. 逐一複製到每個 domain 存取庫的相同兩個路徑(含新 domain 自己)。 # 4. 全部寫完後逐檔比對,有任何一份不一致就報錯。 # # 依賴: **python3**(只用標準函式庫的 json 模組)。 # 本 repo 的腳本一律 shell 優先,這裡是唯一例外:條目的插入、排序與重新序列化要動 JSON, # 純 shell 拼 JSON 會壞掉(縮排、逸出、非 ASCII 描述)。所以保留 python3。 # python3 不在 PATH 時:腳本不寫任何檔案,印出缺少 python3 的訊息,回 1。 # 呼叫端要先裝 python3 再重跑,不可改用 sed 手動改 marketplace。 # # 根目錄: 交給 tools/plugins-root.sh 推導(JSC_PLUGINS_ROOT -> 從 $PWD 往上找 -> # 腳本位置上兩層 -> $HOME/plugins)。以 plugin 形式安裝時,「腳本位置上兩層」會落在 # plugin 快取目錄,單靠它一定推錯,所以推導規則抽成共用腳本。 # 存取庫目錄名: 先找 {root}/{domain},再找 {root}/jsc-{domain}。 # # 輸出: 一行一個實際寫入的檔案路徑(stdout);略過與失敗說明走 stderr。 # 結束碼: 0=全部寫入且逐檔一致 2=用法錯誤 # 1=推導不出根目錄、缺 python3、正本讀寫失敗、建不了暫存目錄, # 或有檔案比對不一致 # 3=寫入成功,但有 domain 存取庫不在本機(先跑 sync-domains.sh 再重跑) # 環境變數: JSC_PLUGINS_ROOT(根目錄,見 plugins-root.sh) set -u usage() { echo 'usage: sync-marketplace.sh ' >&2 exit 2 } [ "$#" -eq 3 ] || usage DOMAIN=$1 URL=$2 DESC=$3 [ -n "$DOMAIN" ] && [ -n "$URL" ] && [ -n "$DESC" ] || usage HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) META=$(CDPATH= cd -- "$HERE/.." && pwd) . "$HERE/plugins-root.sh" ROOT=$(jsc_plugins_root) || exit 1 REL_CLAUDE='.claude-plugin/marketplace.json' REL_AGENTS='.agents/plugins/marketplace.json' CANON="$META/$REL_CLAUDE" [ -f "$CANON" ] || { echo "找不到正本 marketplace:$CANON" >&2; exit 1; } # 先擋掉缺 python3 的環境,才不會寫到一半才失敗。 command -v python3 >/dev/null 2>&1 || { echo '找不到 python3:本腳本用 python3 的 json 模組改 marketplace 條目。請先安裝 python3 再重跑。' >&2 exit 1 } WORKDIR=$(mktemp -d) || exit 1 trap 'rm -rf "$WORKDIR"' EXIT NEW="$WORKDIR/marketplace.json" # 條目的插入與更新交給 json 解析器;用 sed 拼 JSON 會壞掉。 JSC_MK_DOMAIN="$DOMAIN" JSC_MK_URL="$URL" JSC_MK_DESC="$DESC" \ JSC_MK_IN="$CANON" JSC_MK_OUT="$NEW" python3 - <<'PY' || { echo "更新正本內容失敗:$CANON" >&2; exit 1; } import json, os name = "jsc-" + os.environ["JSC_MK_DOMAIN"] entry = { "name": name, "source": {"source": "url", "url": os.environ["JSC_MK_URL"]}, "description": os.environ["JSC_MK_DESC"], } with open(os.environ["JSC_MK_IN"], encoding="utf-8") as fh: data = json.load(fh) plugins = [p for p in data.get("plugins", []) if p.get("name") != name] plugins.append(entry) data["plugins"] = sorted(plugins, key=lambda p: p.get("name", "")) with open(os.environ["JSC_MK_OUT"], "w", encoding="utf-8") as fh: fh.write(json.dumps(data, indent=2, ensure_ascii=False) + "\n") PY rc=0 write_copy() { # $1=目標檔案 target=$1 dir=$(dirname "$target") mkdir -p "$dir" 2>/dev/null || { echo "無法建立目錄:$dir" >&2; rc=1; return 1; } cp "$NEW" "$target" 2>/dev/null || { echo "無法寫入:$target" >&2; rc=1; return 1; } printf '%s\n' "$target" } # ---- 1. 兩份正本 ---- write_copy "$META/$REL_CLAUDE" write_copy "$META/$REL_AGENTS" # ---- 2. 每個 domain 存取庫的副本 ---- DOMAINS=$(sed -n 's/.*"name": *"jsc-\([a-z0-9-]*\)".*/\1/p' "$NEW" | sort -u) for d in $DOMAINS; do path="" for cand in "$ROOT/$d" "$ROOT/jsc-$d"; do [ -d "$cand" ] && { path="$cand"; break; } done if [ -z "$path" ]; then echo "存取庫不在本機,略過:$d(先跑 sync-domains.sh)" >&2 [ "$rc" -eq 0 ] && rc=3 continue fi [ "$path" = "$META" ] && continue write_copy "$path/$REL_CLAUDE" write_copy "$path/$REL_AGENTS" done # ---- 3. 逐檔比對,確認位元組一致 ---- for d in "$META" $(for x in $DOMAINS; do for cand in "$ROOT/$x" "$ROOT/jsc-$x"; do [ -d "$cand" ] && { echo "$cand"; break; }; done done); do for rel in "$REL_CLAUDE" "$REL_AGENTS"; do [ -f "$d/$rel" ] || continue cmp -s "$NEW" "$d/$rel" || { echo "內容不一致:$d/$rel" >&2; rc=1; } done done exit $rc