feat(scripts): 本機 CI/CD + registry-to-KBDB 同步 (跳過 GH Actions)
對應 leo 2026-05-16 GH Actions 配額被停 + LI SDD M3.4。
scripts/local-deploy.sh — 本機 deploy 取代 GH Actions
- 複製 .github/workflows/deploy.yml 的 discover 邏輯
- git diff 過濾,只 deploy 改到的 worker
- 連動:registry/components/{X} 改了,重 build .component-builds/{X}/component.wasm
- 分 tier1 (components) / tier2 (orchestration),依序 deploy
- 支援:--all / --dry-run / --base <ref> / 指定 target 名稱
- 部署前 wrangler whoami 健康檢查
- 失敗清單彙整不中斷其他 deploy
scripts/sync-registry-to-kbdb.py — LI SDD M3.4
- registry/examples/ → KBDB type=workflow-example (page_name=example-{slug})
- registry/skills/ → KBDB type=agent-skill (page_name=skill-{slug})
- 走 kbdb-upsert-block.arcrun.dev (idempotent,page_name 為 key)
- 從 ARCRUN_API_KEY env var 或 polaris/mira/.env 取金鑰
- 支援:--dry-run / --examples-only / --skills-only
實測:
- sync 第一次:10 examples + 5 skills 全 created
- 第二次:全 updated (idempotent ✓)
- KBDB type=workflow-example / agent-skill 都看得到完整 content + tags
意義:
- 申訴 GH Actions 期間,leo / Claude Code 可本機 deploy 不卡
- 即使 GH 永久不通也能 ship
- sync script 之後也能加進 cron 或 git hook 自動化
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,216 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# scripts/local-deploy.sh — 本機 deploy 取代 GH Actions
|
||||||
|
#
|
||||||
|
# 對應 LI SDD M5.6 / 2026-05-16 leo GH Actions 被停用後的 fallback
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# bash scripts/local-deploy.sh # 偵測 git diff,deploy 改到的 worker
|
||||||
|
# bash scripts/local-deploy.sh --all # 全部 worker deploy
|
||||||
|
# bash scripts/local-deploy.sh cypher-executor registry # 指定 worker
|
||||||
|
# bash scripts/local-deploy.sh --base HEAD~3 # 改 diff base(預設 HEAD~1)
|
||||||
|
# bash scripts/local-deploy.sh --dry-run # 只 list 不 deploy
|
||||||
|
#
|
||||||
|
# 邏輯模擬 .github/workflows/deploy.yml discover job
|
||||||
|
# - 掃所有 wrangler.toml 目錄
|
||||||
|
# - git diff base..HEAD 找改動
|
||||||
|
# - 對改到的 worker 跑 pnpm exec wrangler deploy
|
||||||
|
# - 若改到 registry/components/{name}/,連動 deploy .component-builds/{name}/
|
||||||
|
#
|
||||||
|
# 要求:
|
||||||
|
# - 已 wrangler login(pnpm exec wrangler whoami 確認)
|
||||||
|
# - 在 arcrun/ 根目錄執行
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ARCRUN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ARCRUN_ROOT"
|
||||||
|
|
||||||
|
# ── Parse args ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
BASE_REF="HEAD~1"
|
||||||
|
DRY_RUN=false
|
||||||
|
DEPLOY_ALL=false
|
||||||
|
EXPLICIT_TARGETS=()
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--all) DEPLOY_ALL=true; shift ;;
|
||||||
|
--base) BASE_REF="$2"; shift 2 ;;
|
||||||
|
--dry-run) DRY_RUN=true; shift ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '2,20p' "$0"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) EXPLICIT_TARGETS+=("$1"); shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── 1. 列所有可 deploy 的 worker 目錄 ─────────────────────────────────────────
|
||||||
|
# 排除 node_modules、wrangler.test.toml、Pages 專案
|
||||||
|
echo "🔍 Scanning worker directories..."
|
||||||
|
|
||||||
|
mapfile -t ALL_DIRS < <(
|
||||||
|
find . -type f -name 'wrangler.toml' \
|
||||||
|
-not -path '*/node_modules/*' \
|
||||||
|
-not -name 'wrangler.test.toml' \
|
||||||
|
| while read f; do
|
||||||
|
if grep -q 'pages_build_output_dir' "$f"; then continue; fi
|
||||||
|
dirname "$f"
|
||||||
|
done \
|
||||||
|
| sort -u \
|
||||||
|
| sed 's|^\./||'
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Found ${#ALL_DIRS[@]} worker dirs"
|
||||||
|
|
||||||
|
# ── 2. 決定 targets ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
declare -a TARGETS=()
|
||||||
|
|
||||||
|
if [[ ${#EXPLICIT_TARGETS[@]} -gt 0 ]]; then
|
||||||
|
echo "🎯 Explicit targets: ${EXPLICIT_TARGETS[*]}"
|
||||||
|
for req in "${EXPLICIT_TARGETS[@]}"; do
|
||||||
|
matched=false
|
||||||
|
for d in "${ALL_DIRS[@]}"; do
|
||||||
|
if [[ "$d" == "$req" || "$(basename "$d")" == "$req" ]]; then
|
||||||
|
TARGETS+=("$d")
|
||||||
|
matched=true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ "$matched" == false ]]; then
|
||||||
|
echo "⚠️ '$req' not found in worker list (跳過)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
elif [[ "$DEPLOY_ALL" == true ]]; then
|
||||||
|
echo "🎯 Mode: deploy all"
|
||||||
|
TARGETS=("${ALL_DIRS[@]}")
|
||||||
|
else
|
||||||
|
# git diff 過濾
|
||||||
|
echo "🎯 Mode: git diff $BASE_REF..HEAD"
|
||||||
|
|
||||||
|
if ! git rev-parse "$BASE_REF" >/dev/null 2>&1; then
|
||||||
|
echo "❌ base ref '$BASE_REF' not found. 改用 --all 或 --base <valid-ref>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mapfile -t CHANGED < <(git diff --name-only "$BASE_REF" HEAD)
|
||||||
|
echo "Changed files (${#CHANGED[@]}):"
|
||||||
|
for f in "${CHANGED[@]}"; do echo " $f"; done
|
||||||
|
|
||||||
|
for d in "${ALL_DIRS[@]}"; do
|
||||||
|
hit=0
|
||||||
|
for f in "${CHANGED[@]}"; do
|
||||||
|
if [[ "$f" == "$d"/* ]]; then hit=1; break; fi
|
||||||
|
done
|
||||||
|
# 連動:改 registry/components/{name}/ 也要 deploy .component-builds/{name}/
|
||||||
|
if [[ $hit -eq 0 && "$d" == .component-builds/* ]]; then
|
||||||
|
name="${d#.component-builds/}"
|
||||||
|
for f in "${CHANGED[@]}"; do
|
||||||
|
if [[ "$f" == "registry/components/$name"/* ]]; then hit=1; break; fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if [[ $hit -eq 1 ]]; then TARGETS+=("$d"); fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#TARGETS[@]} -eq 0 ]]; then
|
||||||
|
echo "✨ Nothing to deploy. All up-to-date."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 3. 分 tier (component builds 先,orchestration 後) ────────────────────────
|
||||||
|
|
||||||
|
declare -a TIER1=() TIER2=()
|
||||||
|
for t in "${TARGETS[@]}"; do
|
||||||
|
if [[ "$t" == .component-builds/* ]]; then
|
||||||
|
TIER1+=("$t")
|
||||||
|
else
|
||||||
|
TIER2+=("$t")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "📦 Deploy plan:"
|
||||||
|
echo " Tier 1 (components, ${#TIER1[@]}):"
|
||||||
|
for t in "${TIER1[@]}"; do echo " - $t"; done
|
||||||
|
echo " Tier 2 (orchestration, ${#TIER2[@]}):"
|
||||||
|
for t in "${TIER2[@]}"; do echo " - $t"; done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
echo "(dry-run,不實際 deploy)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 4. wrangler whoami 健康檢查 ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
# 隨便挑一個 worker dir 跑 whoami(pnpm 需要在 npm package 內)
|
||||||
|
SAMPLE_DIR=""
|
||||||
|
for d in "${TARGETS[@]}"; do
|
||||||
|
if [[ -f "$d/package.json" ]]; then SAMPLE_DIR="$d"; break; fi
|
||||||
|
done
|
||||||
|
if [[ -z "$SAMPLE_DIR" ]]; then
|
||||||
|
echo "❌ 找不到任何 target 有 package.json (wrangler whoami 需要)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "🔑 wrangler whoami:"
|
||||||
|
(cd "$SAMPLE_DIR" && pnpm exec wrangler whoami 2>&1 | grep -E "logged in|email" | head -2) || {
|
||||||
|
echo "❌ wrangler 沒登入。先跑:cd $SAMPLE_DIR && pnpm exec wrangler login"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── 5. 依序 deploy ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
deploy_one() {
|
||||||
|
local dir="$1"
|
||||||
|
echo ""
|
||||||
|
echo "▶ Deploying $dir ..."
|
||||||
|
pushd "$dir" >/dev/null
|
||||||
|
|
||||||
|
# 若是 component build,先確認有 component.wasm (從 registry build 出來)
|
||||||
|
if [[ "$dir" == .component-builds/* ]]; then
|
||||||
|
name="${dir#.component-builds/}"
|
||||||
|
if [[ ! -f "component.wasm" ]] && [[ -d "../../registry/components/$name" ]]; then
|
||||||
|
echo " ⚙️ rebuild WASM from registry/components/$name ..."
|
||||||
|
(cd "../../registry/components/$name" && tinygo build -target=wasi -o "$name.wasm" main.go && cp "$name.wasm" "../../../.component-builds/$name/component.wasm") || {
|
||||||
|
echo " ❌ TinyGo build failed for $name"
|
||||||
|
popd >/dev/null
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# pnpm install 若沒 node_modules(首次)
|
||||||
|
if [[ ! -d "node_modules" ]] && [[ -f "package.json" ]]; then
|
||||||
|
echo " 📥 pnpm install (first time)..."
|
||||||
|
pnpm install --frozen-lockfile 2>&1 | tail -3
|
||||||
|
fi
|
||||||
|
|
||||||
|
if pnpm exec wrangler deploy 2>&1 | tail -4; then
|
||||||
|
echo " ✅ Done"
|
||||||
|
else
|
||||||
|
echo " ❌ Failed"
|
||||||
|
popd >/dev/null
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
popd >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
FAILED=()
|
||||||
|
for d in "${TIER1[@]}"; do
|
||||||
|
deploy_one "$d" || FAILED+=("$d")
|
||||||
|
done
|
||||||
|
for d in "${TIER2[@]}"; do
|
||||||
|
deploy_one "$d" || FAILED+=("$d")
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "════════════════════════════════════════════════════════════"
|
||||||
|
if [[ ${#FAILED[@]} -eq 0 ]]; then
|
||||||
|
echo "✅ All ${#TARGETS[@]} workers deployed successfully"
|
||||||
|
else
|
||||||
|
echo "⚠️ ${#FAILED[@]}/${#TARGETS[@]} workers failed:"
|
||||||
|
for f in "${FAILED[@]}"; do echo " ❌ $f"; done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""sync-registry-to-kbdb.py — 把 registry/examples + registry/skills 同步進 KBDB
|
||||||
|
|
||||||
|
對應 LI SDD M3.4。examples / skills 在 git 是 source of truth,
|
||||||
|
KBDB 是「給 AI 搜尋 / get」的 query-friendly mirror。
|
||||||
|
|
||||||
|
對 KBDB block:
|
||||||
|
- examples → type=workflow-example
|
||||||
|
content = workflow.yaml 全文
|
||||||
|
metadata_json = { description, tags }
|
||||||
|
tags_json = [...tags.json]
|
||||||
|
page_name = example-{slug} (idempotency key,重複 sync 走 upsert)
|
||||||
|
|
||||||
|
- skills → type=agent-skill
|
||||||
|
content = {slug}.md 全文
|
||||||
|
page_name = skill-{slug} (idempotency key)
|
||||||
|
tags_json = ["agent-skill", "skill:{slug}"]
|
||||||
|
|
||||||
|
執行:
|
||||||
|
cd matrix/arcrun
|
||||||
|
python3 scripts/sync-registry-to-kbdb.py # 上傳所有
|
||||||
|
python3 scripts/sync-registry-to-kbdb.py --dry-run # 只 list 不寫
|
||||||
|
|
||||||
|
需求:
|
||||||
|
- mira tools/_kbdb_client.py 風格 (urllib + ak_)
|
||||||
|
- ARCRUN_API_KEY 從 .env 或 env var
|
||||||
|
- 走 kbdb-*.arcrun.dev 零件 worker endpoints (符合 mira CLAUDE.md §1.7)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ARCRUN_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
EXAMPLES_DIR = ARCRUN_ROOT / "registry" / "examples"
|
||||||
|
SKILLS_DIR = ARCRUN_ROOT / "registry" / "skills"
|
||||||
|
|
||||||
|
KBDB_UPSERT_URL = "https://kbdb-upsert-block.arcrun.dev/"
|
||||||
|
USER_AGENT = "arcrun-registry-sync/1.0"
|
||||||
|
USER_ID = "inkstone_platform_registry" # 需符合 KBDB partner namespace prefix(inkstone_*)
|
||||||
|
SOURCE = "registry-git-sync"
|
||||||
|
|
||||||
|
|
||||||
|
def get_api_key() -> str:
|
||||||
|
"""從 env var 或 polaris/mira/.env 取 ARCRUN_API_KEY。"""
|
||||||
|
key = os.environ.get("ARCRUN_API_KEY", "")
|
||||||
|
if key:
|
||||||
|
return key
|
||||||
|
# fallback:找 polaris/mira/.env(leo 既有約定位置)
|
||||||
|
mira_env = ARCRUN_ROOT.parent.parent / "polaris" / "mira" / ".env"
|
||||||
|
if mira_env.exists():
|
||||||
|
for line in mira_env.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("ARCRUN_API_KEY="):
|
||||||
|
return line.split("=", 1)[1].strip()
|
||||||
|
raise SystemExit(
|
||||||
|
"ARCRUN_API_KEY 未設定。export ARCRUN_API_KEY=ak_... 或加到 polaris/mira/.env"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def kbdb_upsert(api_key: str, payload: dict, dry_run: bool) -> dict:
|
||||||
|
"""POST kbdb-upsert-block.arcrun.dev — page_name 當 idempotency key"""
|
||||||
|
if dry_run:
|
||||||
|
return {"dry_run": True, "would_upsert": payload.get("page_name")}
|
||||||
|
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||||
|
req = urllib.request.Request(
|
||||||
|
KBDB_UPSERT_URL,
|
||||||
|
data=data,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": USER_AGENT,
|
||||||
|
},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||||
|
return json.loads(resp.read().decode("utf-8"))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
body = e.read().decode("utf-8", errors="replace")
|
||||||
|
return {"error": f"HTTP {e.code}: {body[:200]}"}
|
||||||
|
|
||||||
|
|
||||||
|
def sync_examples(api_key: str, dry_run: bool) -> tuple[int, int]:
|
||||||
|
"""同步 registry/examples/{slug}/ 進 KBDB"""
|
||||||
|
if not EXAMPLES_DIR.exists():
|
||||||
|
print(f"⚠️ {EXAMPLES_DIR} 不存在,跳過 examples 同步")
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
|
ok, fail = 0, 0
|
||||||
|
for slug_dir in sorted(EXAMPLES_DIR.iterdir()):
|
||||||
|
if not slug_dir.is_dir():
|
||||||
|
continue
|
||||||
|
slug = slug_dir.name
|
||||||
|
workflow_yaml = slug_dir / "workflow.yaml"
|
||||||
|
description_md = slug_dir / "description.md"
|
||||||
|
tags_json = slug_dir / "tags.json"
|
||||||
|
|
||||||
|
if not workflow_yaml.exists():
|
||||||
|
print(f" ⚠️ {slug}: 缺 workflow.yaml,跳過")
|
||||||
|
continue
|
||||||
|
|
||||||
|
yaml_content = workflow_yaml.read_text(encoding="utf-8")
|
||||||
|
description = (
|
||||||
|
description_md.read_text(encoding="utf-8") if description_md.exists() else ""
|
||||||
|
)
|
||||||
|
tags = (
|
||||||
|
json.loads(tags_json.read_text(encoding="utf-8")) if tags_json.exists() else []
|
||||||
|
)
|
||||||
|
|
||||||
|
# content = workflow YAML(讓 AI semantic search 命中 YAML 內容)
|
||||||
|
# metadata_json = description + tags 結構化
|
||||||
|
payload = {
|
||||||
|
"api_key": api_key,
|
||||||
|
"type": "workflow-example",
|
||||||
|
"page_name": f"example-{slug}",
|
||||||
|
"source": SOURCE,
|
||||||
|
"user_id": USER_ID,
|
||||||
|
"content": yaml_content,
|
||||||
|
"metadata_json": json.dumps(
|
||||||
|
{
|
||||||
|
"slug": slug,
|
||||||
|
"description_md": description,
|
||||||
|
"tags": tags,
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
),
|
||||||
|
"tags_json": json.dumps(
|
||||||
|
["workflow-example", f"example:{slug}", *tags],
|
||||||
|
ensure_ascii=False,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
result = kbdb_upsert(api_key, payload, dry_run)
|
||||||
|
if "error" in result:
|
||||||
|
print(f" ❌ {slug}: {result['error']}")
|
||||||
|
fail += 1
|
||||||
|
else:
|
||||||
|
action = result.get("data", {}).get("action", "?") if isinstance(result.get("data"), dict) else "?"
|
||||||
|
print(f" ✅ {slug} → {action}")
|
||||||
|
ok += 1
|
||||||
|
|
||||||
|
return ok, fail
|
||||||
|
|
||||||
|
|
||||||
|
def sync_skills(api_key: str, dry_run: bool) -> tuple[int, int]:
|
||||||
|
"""同步 registry/skills/*.md 進 KBDB"""
|
||||||
|
if not SKILLS_DIR.exists():
|
||||||
|
print(f"⚠️ {SKILLS_DIR} 不存在,跳過 skills 同步")
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
|
ok, fail = 0, 0
|
||||||
|
for md_file in sorted(SKILLS_DIR.glob("*.md")):
|
||||||
|
if md_file.name == "README.md":
|
||||||
|
continue
|
||||||
|
slug = md_file.stem
|
||||||
|
content = md_file.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
# 簡單抓首行 # X 當 title
|
||||||
|
title = slug
|
||||||
|
for line in content.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("# "):
|
||||||
|
title = line[2:].strip()
|
||||||
|
break
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"api_key": api_key,
|
||||||
|
"type": "agent-skill",
|
||||||
|
"page_name": f"skill-{slug}",
|
||||||
|
"source": SOURCE,
|
||||||
|
"user_id": USER_ID,
|
||||||
|
"content": content,
|
||||||
|
"metadata_json": json.dumps(
|
||||||
|
{"slug": slug, "title": title},
|
||||||
|
ensure_ascii=False,
|
||||||
|
),
|
||||||
|
"tags_json": json.dumps(
|
||||||
|
["agent-skill", f"skill:{slug}"],
|
||||||
|
ensure_ascii=False,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
result = kbdb_upsert(api_key, payload, dry_run)
|
||||||
|
if "error" in result:
|
||||||
|
print(f" ❌ {slug}: {result['error']}")
|
||||||
|
fail += 1
|
||||||
|
else:
|
||||||
|
action = result.get("data", {}).get("action", "?") if isinstance(result.get("data"), dict) else "?"
|
||||||
|
print(f" ✅ {slug} → {action}")
|
||||||
|
ok += 1
|
||||||
|
|
||||||
|
return ok, fail
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser(description="Sync registry/examples + skills → KBDB")
|
||||||
|
p.add_argument("--dry-run", action="store_true", help="只 list 不寫")
|
||||||
|
p.add_argument("--examples-only", action="store_true")
|
||||||
|
p.add_argument("--skills-only", action="store_true")
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
api_key = get_api_key()
|
||||||
|
print(f"🔑 api_key: {api_key[:12]}... (len={len(api_key)})")
|
||||||
|
print(f"📂 root: {ARCRUN_ROOT}")
|
||||||
|
if args.dry_run:
|
||||||
|
print("(dry-run,不實際寫 KBDB)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
examples_ok = examples_fail = 0
|
||||||
|
skills_ok = skills_fail = 0
|
||||||
|
|
||||||
|
if not args.skills_only:
|
||||||
|
print("📋 Syncing examples → type=workflow-example ...")
|
||||||
|
examples_ok, examples_fail = sync_examples(api_key, args.dry_run)
|
||||||
|
print(f" examples: {examples_ok} ok / {examples_fail} fail\n")
|
||||||
|
|
||||||
|
if not args.examples_only:
|
||||||
|
print("📋 Syncing skills → type=agent-skill ...")
|
||||||
|
skills_ok, skills_fail = sync_skills(api_key, args.dry_run)
|
||||||
|
print(f" skills: {skills_ok} ok / {skills_fail} fail\n")
|
||||||
|
|
||||||
|
total_fail = examples_fail + skills_fail
|
||||||
|
if total_fail > 0:
|
||||||
|
print(f"⚠️ 共 {total_fail} 個項目失敗")
|
||||||
|
sys.exit(1)
|
||||||
|
print(f"✅ Done. examples={examples_ok}, skills={skills_ok}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user