fix(meta): 補齊稽核缺失並修掉護欄失效
What:依 jsc-meta:skill-check 的稽核結果修正技能與工具——補上每個步驟的可檢核完成條件、 把留在內文的標準輸入輸出流程下放 tools/、修正查表與退碼路由造成的誤判。 Why:稽核發現這些缺失會讓技能在實際執行時走錯分支或靜默通過。 完成條件缺漏是最常被違反的一項;退碼誤判與查表錯誤則會讓良性狀況被當成失敗。 How:逐項對照 references/guidelines.md 的審核檢查清單修正,新增的工具都有 documented exit codes,並以真實執行驗證每條路徑。 Who:jsc-meta:skill-check 例行稽核(2026-08-25)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,15 +4,28 @@
|
||||
# 用法: find-skill-refs.sh <domain> <skill-name>
|
||||
#
|
||||
# 掃描樣式:
|
||||
# 1. 技能名稱字面 {skill-name}
|
||||
# 1. 技能名稱字面 {skill-name}——**純子字串比對**,不看邊界。
|
||||
# 所以短名稱也會命中「名稱+後綴」的其他技能名,以及純敘述文字裡的同一個詞。
|
||||
# 這是故意的:盤點寧可多列,漏一個引用會讓刪除技能少改檔案。
|
||||
# 呼叫端必須逐檔看過命中內容再判斷,不可把清單當成「一定要改的檔案」。
|
||||
# 2. 指令形式 /jsc-{domain}:{skill-name}
|
||||
# 這兩個樣式已涵蓋其他 SKILL.md 的引用、domain README 的「Skills 目錄」小節、
|
||||
# plugins/meta 兩份 marketplace.json 與其在各 domain 存取庫的同步副本、tools/ 腳本、
|
||||
# jsc-hooks 接線等位置——只要檔案裡出現技能名稱或指令形式就會被列出。
|
||||
#
|
||||
# 掃描範圍: /root/plugins 底下所有 domain 存取庫,排除 .git 目錄。
|
||||
# 輸出: 命中檔案清單(去重、排序),一行一個路徑;exit 0 一律成功,即使沒有命中。
|
||||
set -eu
|
||||
# 掃描範圍: **只掃正本 marketplace 上的 domain 存取庫**。
|
||||
# 工作區可能還放著不屬於技能組的存取庫(例 shared、persona、code、doc)與
|
||||
# 點開頭的目錄(例 .jsc-monorepo-archive、.kiro),掃進去會讓刪除技能改到
|
||||
# 技能組以外的檔案。domain 清單取自 marketplace.json,不寫死。
|
||||
# 每個 domain 先找 {root}/{domain},再找 {root}/jsc-{domain};兩者都沒有就略過。
|
||||
# 每個存取庫內排除 .git 目錄。
|
||||
# 根目錄預設取本腳本位置的上兩層(meta/tools -> meta -> 根),換機器不必改腳本;
|
||||
# 用 JSC_PLUGINS_ROOT 覆寫,指向別處的技能組工作目錄。
|
||||
# 輸出: 命中檔案清單(去重、排序),一行一個路徑(stdout);掃描摘要走 stderr。
|
||||
# 結束碼: 0=有命中 1=掃完但零命中 2=用法錯誤
|
||||
# 3=找不到根目錄、讀不到 domain 清單、本機一個 domain 存取庫都沒有,或掃描失敗
|
||||
# 零命中與掃描失敗必須分開:拿掃描失敗當「沒有引用」會讓刪除技能少改檔案。
|
||||
set -u
|
||||
|
||||
usage() {
|
||||
echo 'usage: find-skill-refs.sh <domain> <skill-name>' >&2
|
||||
@@ -22,13 +35,51 @@ usage() {
|
||||
[ "$#" -eq 2 ] || usage
|
||||
DOMAIN=$1
|
||||
SKILL=$2
|
||||
[ -n "$DOMAIN" ] && [ -n "$SKILL" ] || usage
|
||||
|
||||
ROOT=/root/plugins
|
||||
[ -d "$ROOT" ] || { echo "找不到 plugins 根目錄: $ROOT" >&2; exit 1; }
|
||||
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
ROOT="${JSC_PLUGINS_ROOT:-$(CDPATH= cd -- "$HERE/../.." && pwd)}"
|
||||
[ -d "$ROOT" ] || { echo "找不到 plugins 根目錄:$ROOT(可用 JSC_PLUGINS_ROOT 指定)" >&2; exit 3; }
|
||||
|
||||
# 正本 domain 清單:優先讀本機任一份 marketplace 副本(每個 repo 都帶同一份)。
|
||||
mkt=""
|
||||
for cand in "$HERE/../.claude-plugin/marketplace.json" "$ROOT"/*/.claude-plugin/marketplace.json; do
|
||||
[ -f "$cand" ] && { mkt="$cand"; break; }
|
||||
done
|
||||
[ -n "$mkt" ] || { echo "找不到 marketplace.json,無法判定 domain 清單" >&2; exit 3; }
|
||||
DOMAINS=$(sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"jsc-\([a-z0-9-]*\)".*/\1/p' "$mkt" | sort -u)
|
||||
[ -n "$DOMAINS" ] || { echo "marketplace.json 裡沒有 jsc-{domain} 條目:$mkt" >&2; exit 3; }
|
||||
|
||||
# 只收本機存在、且不是點開頭目錄的 domain 存取庫路徑。
|
||||
TARGETS=""
|
||||
for d in $DOMAINS; do
|
||||
for cand in "$ROOT/$d" "$ROOT/jsc-$d"; do
|
||||
[ -d "$cand" ] || continue
|
||||
TARGETS="$TARGETS $cand"
|
||||
break
|
||||
done
|
||||
done
|
||||
[ -n "$TARGETS" ] || { echo "本機找不到任何 domain 存取庫:$ROOT(先跑 sync-domains.sh)" >&2; exit 3; }
|
||||
|
||||
PATTERN1="$SKILL"
|
||||
PATTERN2="/jsc-${DOMAIN}:${SKILL}"
|
||||
|
||||
grep -rlE --exclude-dir=.git -e "$PATTERN1" -e "$PATTERN2" "$ROOT" 2>/dev/null | sort -u
|
||||
TMP=$(mktemp) || { echo "無法建立暫存檔" >&2; exit 3; }
|
||||
trap 'rm -f "$TMP"' EXIT
|
||||
# grep 的結束碼要單獨看:接管線的話 sort 永遠回 0,掃描失敗就被吃掉了。
|
||||
# shellcheck disable=SC2086
|
||||
grep -rlI --exclude-dir=.git -e "$PATTERN1" -e "$PATTERN2" $TARGETS > "$TMP" 2>/dev/null
|
||||
status=$?
|
||||
[ "$status" -le 1 ] || { echo "掃描失敗:$ROOT(grep 結束碼 $status)" >&2; exit 3; }
|
||||
OUT=$(sort -u "$TMP")
|
||||
|
||||
count=$(printf '%s' "$TARGETS" | wc -w | tr -d ' ')
|
||||
if [ -z "$OUT" ]; then
|
||||
echo "掃描完成,零命中:$count 個 domain 存取庫" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "$OUT"
|
||||
printf '掃描完成,命中 %s 個檔案(掃了 %s 個 domain 存取庫;技能名稱為純子字串比對,請逐檔確認)\n' \
|
||||
"$(printf '%s\n' "$OUT" | wc -l | tr -d ' ')" "$count" >&2
|
||||
exit 0
|
||||
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env sh
|
||||
# list-skills.sh — 列出本機所有 domain 存取庫的技能,供選技能與稽核用。
|
||||
#
|
||||
# 用法: list-skills.sh(不吃參數)
|
||||
#
|
||||
# 掃描: {root}/*/skills/*/SKILL.md,讀 frontmatter 的 name 與 description。
|
||||
# domain 取存取庫目錄名,開頭的 jsc- 會去掉。
|
||||
# **只列正本 marketplace 上的 domain**:工作區可能還放著不屬於技能組的
|
||||
# 存取庫(例 shared、persona、code、doc),列出來會讓選技能的流程
|
||||
# 誤把它們當成技能組的一部分。domain 清單取自 marketplace.json,不寫死。
|
||||
# 清單只反映本機檔案;要確保檔案齊全請先跑 sync-domains.sh。
|
||||
#
|
||||
# 根目錄: 預設取本腳本位置的上兩層(meta/tools -> meta -> 根)。
|
||||
# 用 JSC_PLUGINS_ROOT 覆寫,換機器不必改腳本。
|
||||
#
|
||||
# 輸出: 一行一個技能,格式 {domain}<TAB>{name}<TAB>{description},依 domain、name 排序。
|
||||
# 結束碼: 0=至少列出一個技能 1=找不到根目錄、讀不到 domain 清單,或一個技能都沒有
|
||||
set -u
|
||||
|
||||
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
ROOT="${JSC_PLUGINS_ROOT:-$(CDPATH= cd -- "$HERE/../.." && pwd)}"
|
||||
|
||||
[ -d "$ROOT" ] || { echo "找不到 plugins 根目錄:$ROOT(可用 JSC_PLUGINS_ROOT 指定)" >&2; exit 1; }
|
||||
|
||||
# 正本 domain 清單:優先讀本機任一份 marketplace 副本(每個 repo 都帶同一份)。
|
||||
mkt=""
|
||||
for cand in "$HERE/../.claude-plugin/marketplace.json" "$ROOT"/*/.claude-plugin/marketplace.json; do
|
||||
[ -f "$cand" ] && { mkt="$cand"; break; }
|
||||
done
|
||||
[ -n "$mkt" ] || { echo "找不到 marketplace.json,無法判定 domain 清單" >&2; exit 1; }
|
||||
DOMAINS=$(sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"jsc-\([a-z0-9-]*\)".*/\1/p' "$mkt" | sort -u)
|
||||
[ -n "$DOMAINS" ] || { echo "marketplace.json 裡沒有 jsc-{domain} 條目:$mkt" >&2; exit 1; }
|
||||
|
||||
OUT=$(
|
||||
for f in "$ROOT"/*/skills/*/SKILL.md; do
|
||||
[ -f "$f" ] || continue
|
||||
repo=${f#"$ROOT"/}
|
||||
repo=${repo%%/*}
|
||||
domain=${repo#jsc-}
|
||||
# 不在正本清單上的存取庫直接跳過。
|
||||
printf '%s\n' "$DOMAINS" | grep -qx "$domain" || 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\t%s\n' "$domain" "$name" "$desc"
|
||||
done | sort -t"$(printf '\t')" -k1,1 -k2,2
|
||||
)
|
||||
|
||||
[ -n "$OUT" ] || { echo "掃不到任何 SKILL.md:$ROOT/*/skills/*/SKILL.md(先跑 sync-domains.sh)" >&2; exit 1; }
|
||||
|
||||
printf '%s\n' "$OUT"
|
||||
count=$(printf '%s\n' "$OUT" | wc -l | tr -d ' ')
|
||||
echo "共 $count 個技能" >&2
|
||||
exit 0
|
||||
+25
-2
@@ -5,15 +5,36 @@
|
||||
# 1. 中國用語(references/ste100.md 的替換表左欄)
|
||||
# 2. 中文句內的半形標點(, ; ! ? 緊鄰中日韓字元)
|
||||
# 3. 常見 AI 套話(總的來說、綜上所述、希望這對你有幫助 等)
|
||||
# 4. 簡體字(只收沒有繁體正當用法的字,避免誤報)
|
||||
# 5. 中文並列項用斜線(半形 / 或全形 / 夾在中日韓字元之間),應改頓號「、」
|
||||
# 輸出: {檔案}:{行號}:{類別}:{命中內容};全部通過 exit 0,有命中 exit 1。
|
||||
# 限制: 只掃 .md 檔;程式碼圍欄內的內容可能誤報,人工複核。
|
||||
# 限制: 掃 .md 與 .json 檔;程式碼圍欄內的內容可能誤報,人工複核。
|
||||
# references/ste100.md 本身跳過——規則文件裡的詞是被討論,不是被使用。
|
||||
# 並列斜線會誤報兩類:含中文的路徑或分支名範例(例 feat/報表/P2),
|
||||
# 以及英文項目並列(準則允許)。命中後先判斷是不是真的並列項。
|
||||
set -u
|
||||
TERMS='默認|支持某|兼容|信息|數據庫|服務器|軟件|硬件|網絡|質量|卸載|反饋|視頻|屏幕|鼠標|打印|立馬|靠譜|賦能|閉環|抓手|復盤'
|
||||
CLICHES='總的來說|綜上所述|希望這對你有幫助|好問題|在當今|瞬息萬變|標誌著|奠定了基礎|體現了|不僅僅是|讓我們一起'
|
||||
# 刻意排除繁體也在用的字(后、台、干、只、里、面、制、志),只留簡化後才出現的字形。
|
||||
SIMPLIFIED='应|为|这|说|发|国|过|对|开|关|问|题|东|车|马|鸟|龙|飞|见|无|产|业|务|书|写|学|习|报|纸|认|识|证|际|网|络|计|划|实|现|给|条|约|级|组|织|变|换|电|脑|两|双|单|构|价|钱|众|议|论|传|统|么|儿|们|从|来|时|间|长|门|闻|声|员|图|团|转|输|达|运|进|远|连|边|还|经|该|营|规|则|范|围|参|数|类|结|态|设|备|辑|译|码|库|档'
|
||||
hit=0
|
||||
# 不帶參數會空跑並回 0,看起來像「全部通過」。這種假通過比不檢查更危險,所以擋掉。
|
||||
if [ "$#" -eq 0 ]; then
|
||||
echo '用法: ste100-lint.sh <file|dir> [...]' >&2
|
||||
echo '沒有給檢查對象。空跑會回 0,看起來像通過,所以這裡直接視為錯誤。' >&2
|
||||
exit 2
|
||||
fi
|
||||
files() {
|
||||
for p in "$@"; do
|
||||
if [ -d "$p" ]; then find "$p" -name '*.md' -not -path '*/.git/*'; else echo "$p"; fi
|
||||
if [ -d "$p" ]; then
|
||||
find "$p" \( -name '*.md' -o -name '*.json' \) \
|
||||
-not -path '*/.git/*' -not -path '*/references/ste100.md'
|
||||
else
|
||||
case "$p" in
|
||||
*/references/ste100.md|references/ste100.md) ;;
|
||||
*) echo "$p" ;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
}
|
||||
check() { # $1=檔案 $2=類別 $3=grep 模式 $4=grep 旗標
|
||||
@@ -24,5 +45,7 @@ for f in $(files "$@"); do
|
||||
check "$f" "中國用語" "$TERMS" E
|
||||
check "$f" "半形標點" '[\x{4e00}-\x{9fff}][,;!?]|[,;][\x{4e00}-\x{9fff}]' P
|
||||
check "$f" "AI 套話" "$CLICHES" E
|
||||
check "$f" "簡體字" "$SIMPLIFIED" E
|
||||
check "$f" "並列斜線" '[\x{4e00}-\x{9fff}][ ]?[//][ ]?[\x{4e00}-\x{9fff}]' P
|
||||
done
|
||||
exit $hit
|
||||
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env sh
|
||||
# sync-domains.sh — 依 Gitea 正本 marketplace 把所有 domain 存取庫同步到本機。
|
||||
#
|
||||
# 用法: sync-domains.sh(不吃參數)
|
||||
#
|
||||
# 流程:
|
||||
# 1. 透過 jsc-gitea/tools/gitea.sh 讀正本 marketplace
|
||||
# (plugins/meta 的 .claude-plugin/marketplace.json),取得權威 domain 清單。
|
||||
# 2. 本機缺少的 domain:用 gitea.sh clone-url plugins/{domain} 取網址後 clone。
|
||||
# 3. 本機已有的 domain:git pull。工作區有未提交變更就跳過 pull,只在 stderr 提醒——
|
||||
# 正在改的 repo 不該被自動 pull 覆蓋。
|
||||
#
|
||||
# 根目錄: 預設取本腳本位置的上兩層(meta/tools -> meta -> 根)。
|
||||
# 用 JSC_PLUGINS_ROOT 覆寫,換機器不必改腳本。
|
||||
# 存取庫目錄名: 先找 {root}/{domain},再找 {root}/jsc-{domain};都沒有才 clone 成 {root}/{domain}。
|
||||
#
|
||||
# 輸出: 一行一個 domain,格式 {domain}<TAB>{path}(stdout);警告與失敗說明走 stderr。
|
||||
# 結束碼: 0=正本讀到、每個 domain 都在本機,而且每個既有存取庫都更新到最新
|
||||
# 1=讀不到正本 marketplace 或找不到 gitea.sh(不輸出任何 domain)
|
||||
# 2=正本讀到,但有 domain 沒能取得或 clone 失敗(已輸出其餘 domain)
|
||||
# 3=每個 domain 都在本機,但有存取庫沒更新到最新:工作區脏而跳過 pull,
|
||||
# 或 pull 失敗。stderr 會逐一列出這些路徑。**exit 0 才代表「全部最新」**;
|
||||
# 拿 3 當乾淨會讓後續步驟改在落後的檔案上。
|
||||
# 同時發生時回較嚴重的那個:2 蓋過 3。
|
||||
# 環境變數: JSC_PLUGINS_ROOT(根目錄)
|
||||
# JSC_SYNC_DRY_RUN=1(只印出會做什麼,不動檔案;不執行 pull/clone,
|
||||
# 所以也不因為「沒更新」回 3)
|
||||
set -u
|
||||
|
||||
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
ROOT="${JSC_PLUGINS_ROOT:-$(CDPATH= cd -- "$HERE/../.." && pwd)}"
|
||||
DRY="${JSC_SYNC_DRY_RUN:-}"
|
||||
|
||||
[ -d "$ROOT" ] || { echo "找不到 plugins 根目錄:$ROOT(可用 JSC_PLUGINS_ROOT 指定)" >&2; exit 1; }
|
||||
|
||||
GITEA=""
|
||||
for g in "$ROOT/gitea/tools/gitea.sh" "$ROOT/jsc-gitea/tools/gitea.sh"; do
|
||||
[ -f "$g" ] && { GITEA="$g"; break; }
|
||||
done
|
||||
[ -n "$GITEA" ] || { echo "找不到 gitea.sh:請先取得 jsc-gitea 存取庫" >&2; exit 1; }
|
||||
|
||||
mkt=$(sh "$GITEA" api GET /repos/plugins/meta/raw/.claude-plugin/marketplace.json 2>/dev/null) || mkt=""
|
||||
[ -n "$mkt" ] || { echo "讀不到正本 marketplace:檢查 GITEA_HOST、GITEA_TOKEN 或 tea 登入" >&2; exit 1; }
|
||||
|
||||
DOMAINS=$(printf '%s\n' "$mkt" | sed -n 's/.*"name": *"jsc-\([a-z0-9-]*\)".*/\1/p' | sort -u)
|
||||
[ -n "$DOMAINS" ] || { echo "正本 marketplace 裡沒有 jsc-{domain} 條目" >&2; exit 1; }
|
||||
|
||||
repo_path() { # $1=domain;印出既有路徑(return 0)或預設路徑(return 1)
|
||||
for d in "$ROOT/$1" "$ROOT/jsc-$1"; do
|
||||
[ -d "$d" ] && { printf '%s' "$d"; return 0; }
|
||||
done
|
||||
printf '%s' "$ROOT/$1"
|
||||
return 1
|
||||
}
|
||||
|
||||
rc=0
|
||||
stale=0
|
||||
for domain in $DOMAINS; do
|
||||
if path=$(repo_path "$domain"); then
|
||||
if [ -n "$(git -C "$path" status --porcelain 2>/dev/null)" ]; then
|
||||
echo "工作區有未提交變更,跳過 pull,未更新到最新:$path" >&2
|
||||
stale=1
|
||||
elif [ "$DRY" = 1 ]; then
|
||||
echo "[dry-run] git -C $path pull --ff-only" >&2
|
||||
elif ! git -C "$path" pull --quiet --ff-only >/dev/null 2>&1; then
|
||||
echo "pull 失敗,保留本機版本,未更新到最新:$path" >&2
|
||||
stale=1
|
||||
fi
|
||||
else
|
||||
url=$(sh "$GITEA" clone-url "plugins/$domain" 2>/dev/null) || url=""
|
||||
if [ -z "$url" ]; then
|
||||
echo "取不到 clone 網址,略過:plugins/$domain" >&2
|
||||
rc=2
|
||||
continue
|
||||
fi
|
||||
if [ "$DRY" = 1 ]; then
|
||||
echo "[dry-run] git clone $url $path" >&2
|
||||
elif ! git clone --quiet "$url" "$path" >/dev/null 2>&1; then
|
||||
echo "clone 失敗:$url" >&2
|
||||
rc=2
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
printf '%s\t%s\n' "$domain" "$path"
|
||||
done
|
||||
|
||||
# 2(取不到或 clone 失敗)比 3(沒更新到最新)嚴重,優先回報。
|
||||
[ "$rc" -eq 0 ] && [ "$stale" -eq 1 ] && rc=3
|
||||
exit $rc
|
||||
Executable
+123
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env sh
|
||||
# sync-marketplace.sh — 把一個 plugin 條目寫進正本 marketplace,再同步到所有 domain 存取庫。
|
||||
#
|
||||
# 用法: sync-marketplace.sh <domain> <repo-url> <description>
|
||||
# 例: 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。
|
||||
#
|
||||
# 根目錄: 預設取本腳本位置的上兩層(meta/tools -> meta -> 根)。
|
||||
# 用 JSC_PLUGINS_ROOT 覆寫,換機器不必改腳本。
|
||||
# 存取庫目錄名: 先找 {root}/{domain},再找 {root}/jsc-{domain}。
|
||||
#
|
||||
# 輸出: 一行一個實際寫入的檔案路徑(stdout);略過與失敗說明走 stderr。
|
||||
# 結束碼: 0=全部寫入且逐檔一致 2=用法錯誤
|
||||
# 1=缺 python3、正本讀寫失敗,或有檔案比對不一致
|
||||
# 3=寫入成功,但有 domain 存取庫不在本機(先跑 sync-domains.sh 再重跑)
|
||||
set -u
|
||||
|
||||
usage() {
|
||||
echo 'usage: sync-marketplace.sh <domain> <repo-url> <description>' >&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)
|
||||
ROOT="${JSC_PLUGINS_ROOT:-$(CDPATH= cd -- "$HERE/../.." && pwd)}"
|
||||
|
||||
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
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env sh
|
||||
# verify-skill-removed.sh — 刪除技能後,實地檢查各 CLI 的磁碟上有沒有殘留。
|
||||
#
|
||||
# 用法: verify-skill-removed.sh <domain> <skill-name>
|
||||
#
|
||||
# 行為: 先用 jsc-cli/tools/detect-clis.sh 找出已安裝的 CLI,再逐一 grep 該 CLI 的技能快取
|
||||
# 與 hook 設定,找技能名稱字面與指令形式 /jsc-{domain}:{name}。
|
||||
# 未安裝的 CLI 不檢查;設定位置不存在就跳過(安靜降級,不算殘留)。
|
||||
#
|
||||
# 檢查位置(每個 CLI 的技能快取與 hook 設定):
|
||||
# claude — ~/.claude/plugins/、~/.claude/settings.json、~/.claude/settings.local.json
|
||||
# codex — ${CODEX_HOME:-~/.codex}/(config.toml 的 notify、AGENTS.md、plugins/)
|
||||
# copilot — ~/.config/copilot/
|
||||
# antigravity — ~/.antigravity/、~/.config/antigravity/
|
||||
# kiro — ~/.kiro/、工作目錄的 .kiro/hooks/
|
||||
#
|
||||
# 輸出: 檢查過的位置一行一個(stderr),殘留一行一個 {file}:{line}:{內容}(stdout)。
|
||||
# 沒有殘留會印「無殘留」到 stderr,區別於「什麼都沒檢查」。
|
||||
# 結束碼: 0=沒有殘留 1=有殘留 2=用法錯誤 3=沒偵測到任何 CLI 或找不到可檢查的位置
|
||||
set -u
|
||||
|
||||
usage() {
|
||||
echo 'usage: verify-skill-removed.sh <domain> <skill-name>' >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
[ "$#" -eq 2 ] || usage
|
||||
DOMAIN=$1
|
||||
SKILL=$2
|
||||
[ -n "$DOMAIN" ] && [ -n "$SKILL" ] || usage
|
||||
|
||||
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
ROOT="${JSC_PLUGINS_ROOT:-$(CDPATH= cd -- "$HERE/../.." && pwd)}"
|
||||
|
||||
DETECT=""
|
||||
for d in "$ROOT/cli/tools/detect-clis.sh" "$ROOT/jsc-cli/tools/detect-clis.sh"; do
|
||||
[ -f "$d" ] && { DETECT="$d"; break; }
|
||||
done
|
||||
[ -n "$DETECT" ] || { echo "找不到 detect-clis.sh:請先取得 jsc-cli 存取庫" >&2; exit 3; }
|
||||
|
||||
CLIS=$(sh "$DETECT" 2>/dev/null | cut -f1)
|
||||
[ -n "$CLIS" ] || { echo "沒偵測到任何已安裝的 CLI,無處可查" >&2; exit 3; }
|
||||
|
||||
cli_paths() { # $1=CLI 代號;印出該 CLI 的技能快取與 hook 設定位置
|
||||
case "$1" in
|
||||
claude)
|
||||
printf '%s\n' "$HOME/.claude/plugins" "$HOME/.claude/settings.json" \
|
||||
"$HOME/.claude/settings.local.json" ;;
|
||||
codex)
|
||||
printf '%s\n' "${CODEX_HOME:-$HOME/.codex}" ;;
|
||||
copilot)
|
||||
printf '%s\n' "$HOME/.config/copilot" ;;
|
||||
antigravity)
|
||||
printf '%s\n' "$HOME/.antigravity" "$HOME/.config/antigravity" ;;
|
||||
kiro)
|
||||
printf '%s\n' "$HOME/.kiro" "./.kiro" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
checked=0
|
||||
hit=0
|
||||
for cli in $CLIS; do
|
||||
for p in $(cli_paths "$cli"); do
|
||||
[ -e "$p" ] || continue
|
||||
checked=$((checked + 1))
|
||||
echo "檢查 $cli:$p" >&2
|
||||
out=$(grep -rnI --exclude-dir=.git -e "$SKILL" -e "/jsc-${DOMAIN}:${SKILL}" "$p" 2>/dev/null)
|
||||
if [ -n "$out" ]; then
|
||||
printf '%s\n' "$out"
|
||||
hit=1
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
[ "$checked" -gt 0 ] || { echo "偵測到 CLI,但沒有任何設定位置存在,無處可查" >&2; exit 3; }
|
||||
[ "$hit" -eq 0 ] && echo "無殘留:已檢查 $checked 個位置" >&2
|
||||
exit $hit
|
||||
Reference in New Issue
Block a user