refactor: skill 改为自包含(符合工业惯例),install 简化为整目录拷贝

- .claude/skills/new-project/ 自带 scripts/verify.sh:拷了即用、可移植,
  不再依赖 install 时从根 scripts/ 组装
- install-skill.sh 简化:本地整目录 cp -R / 远程按清单拉取
- check-docs.sh 增加一致性校验:根 scripts/verify.sh 与 skill 内副本必须相同
  (改了根副本需同步,CI/本地都会报漂移)
- README 更新 skill 自包含说明 + 文件树

取舍:接受 verify.sh 两份小重复,换 skill 自包含可移植——符合 Anthropic skills 惯例,
比'安装时组装'更专业、更好维护;漂移由 CI 兜底。
This commit is contained in:
baozaotumao
2026-06-02 01:07:04 -07:00
parent e07d57eea1
commit 871e0589cd
4 changed files with 394 additions and 27 deletions
+354
View File
@@ -0,0 +1,354 @@
#!/usr/bin/env bash
# verify.sh — 检查项目工具链配置是否完整
#
# 用法:
# ./scripts/verify.sh [OPTIONS] [PROJECT_PATH]
#
# 选项:
# -h, --help 显示帮助信息
# -v, --verbose 详细输出(显示所有通过项和跳过项)
# -s, --strict 严格模式:警告也视为失败
#
# 退出码:
# 0 全部通过(严格模式下无警告且无失败)
# 1 存在失败项
# 2 命令行参数错误
set -euo pipefail
# ── 默认值 ────────────────────────────────────────────────────────────────────
VERBOSE=0
STRICT=0
ROOT="."
# ── 全局计数(按分类) ────────────────────────────────────────────────────────
# 用索引数组而非关联数组(declare -A),以兼容 macOS 自带的 bash 3.2。
CATEGORIES=("根目录" "Backend" "Agent" "Frontend" "Pre-commit 钩子" "Claude 规则文件" "工具链可用性")
CAT_PASS=(); CAT_WARN=(); CAT_FAIL=()
for _i in "${!CATEGORIES[@]}"; do CAT_PASS[$_i]=0; CAT_WARN[$_i]=0; CAT_FAIL[$_i]=0; done
TOTAL_PASS=0
TOTAL_WARN=0
TOTAL_FAIL=0
CURRENT_CAT=""
CURRENT_IDX=-1
# ── 颜色 ──────────────────────────────────────────────────────────────────────
if [ -t 1 ]; then
C_GREEN='\033[32m'; C_YELLOW='\033[33m'; C_RED='\033[31m'
C_CYAN='\033[36m'; C_BOLD='\033[1m'; C_RESET='\033[0m'
else
C_GREEN=''; C_YELLOW=''; C_RED=''; C_CYAN=''; C_BOLD=''; C_RESET=''
fi
# ── 帮助 ──────────────────────────────────────────────────────────────────────
usage() {
cat <<EOF
${C_BOLD}用法:${C_RESET} $(basename "$0") [OPTIONS] [PROJECT_PATH]
检查项目工具链配置是否符合 scaffold 规范。
不修改任何文件,安全幂等,可重复运行。
${C_BOLD}选项:${C_RESET}
-h, --help 显示此帮助信息
-v, --verbose 详细输出(含通过项、跳过项的具体路径)
-s, --strict 严格模式:警告(WARN)也视为失败,退出码 1
${C_BOLD}参数:${C_RESET}
PROJECT_PATH 要检查的项目根目录(默认:当前目录)
${C_BOLD}示例:${C_RESET}
$(basename "$0") # 检查当前目录
$(basename "$0") /path/to/project # 检查指定目录
$(basename "$0") -v -s . # 详细 + 严格模式
${C_BOLD}退出码:${C_RESET}
0 全部通过(严格模式下无警告)
1 存在失败项(或严格模式下存在警告)
2 参数错误
EOF
}
# ── 参数解析 ──────────────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-v|--verbose) VERBOSE=1; shift ;;
-s|--strict) STRICT=1; shift ;;
-vs|-sv) VERBOSE=1; STRICT=1; shift ;;
-*) echo "未知选项: $1" >&2; usage >&2; exit 2 ;;
*) ROOT="$1"; shift ;;
esac
done
ROOT="$(cd "$ROOT" 2>/dev/null && pwd)" || { echo "路径不存在: $ROOT" >&2; exit 2; }
# ── 输出辅助 ──────────────────────────────────────────────────────────────────
log_pass() {
TOTAL_PASS=$((TOTAL_PASS + 1))
[ "$CURRENT_IDX" -ge 0 ] && CAT_PASS[$CURRENT_IDX]=$(( ${CAT_PASS[$CURRENT_IDX]:-0} + 1 ))
[ "$VERBOSE" -eq 1 ] && printf " ${C_GREEN}${C_RESET} %s\n" "$*"
return 0
}
log_warn() {
TOTAL_WARN=$((TOTAL_WARN + 1))
[ "$CURRENT_IDX" -ge 0 ] && CAT_WARN[$CURRENT_IDX]=$(( ${CAT_WARN[$CURRENT_IDX]:-0} + 1 ))
printf " ${C_YELLOW}${C_RESET} %s\n" "$*"
}
log_fail() {
TOTAL_FAIL=$((TOTAL_FAIL + 1))
[ "$CURRENT_IDX" -ge 0 ] && CAT_FAIL[$CURRENT_IDX]=$(( ${CAT_FAIL[$CURRENT_IDX]:-0} + 1 ))
printf " ${C_RED}${C_RESET} %s\n" "$*"
}
log_skip() {
[ "$VERBOSE" -eq 1 ] && printf " ${C_CYAN}${C_RESET} %s\n" "$*"
return 0
}
section() {
CURRENT_CAT="$1"
CURRENT_IDX=-1
local i
for i in "${!CATEGORIES[@]}"; do
[ "${CATEGORIES[$i]}" = "$1" ] && { CURRENT_IDX=$i; break; }
done
printf "\n${C_BOLD}[ %s ]${C_RESET}\n" "$1"
}
# ── 检查函数 ──────────────────────────────────────────────────────────────────
# 检查文件或目录是否存在
check() {
local label="$1" path="$2" severity="${3:-fail}"
if [ -e "$ROOT/$path" ]; then
log_pass "$label"
else
[ "$severity" = "warn" ] && log_warn "$label — 缺失: $path" || log_fail "$label — 缺失: $path"
fi
}
# 检查文件中是否包含指定内容
check_contains() {
local label="$1" path="$2" pattern="$3" severity="${4:-fail}"
if [ ! -e "$ROOT/$path" ]; then
log_skip "$label — 文件不存在,跳过内容检查: $path"
return
fi
if grep -q "$pattern" "$ROOT/$path" 2>/dev/null; then
log_pass "$label"
else
[ "$severity" = "warn" ] \
&& log_warn "$label — 未找到 '$pattern' 在 $path" \
|| log_fail "$label — 未找到 '$pattern' 在 $path"
fi
}
# 检查命令是否可用
check_cmd() {
local label="$1" cmd="$2" severity="${3:-warn}"
if command -v "$cmd" &>/dev/null; then
local ver
ver=$(${cmd} --version 2>/dev/null | head -1 || echo "版本未知")
log_pass "$label ($ver)"
else
[ "$severity" = "warn" ] && log_warn "$label — 未安装: $cmd" || log_fail "$label — 未安装: $cmd"
fi
}
# ── 读取 copier 配置(可选)────────────────────────────────────────────────────
COPIER_ANSWERS="$ROOT/.copier-answers.yml"
LLM_PROVIDER=""
DB_TYPE=""
if [ -f "$COPIER_ANSWERS" ]; then
LLM_PROVIDER=$(grep "^llm_provider:" "$COPIER_ANSWERS" 2>/dev/null | awk '{print $2}' | tr -d "\"'" || true)
DB_TYPE=$(grep "^database:" "$COPIER_ANSWERS" 2>/dev/null | awk '{print $2}' | tr -d "\"'" || true)
fi
# ── 开始检查 ──────────────────────────────────────────────────────────────────
printf "${C_BOLD}═══ 工具链合规检查: %s ═══${C_RESET}\n" "$ROOT"
[ -n "$LLM_PROVIDER" ] && printf "${C_CYAN}配置来源:.copier-answers.yml llm_provider=%s database=%s${C_RESET}\n" \
"${LLM_PROVIDER:-?}" "${DB_TYPE:-?}"
# ── 根目录 ────────────────────────────────────────────────────────────────────
section "根目录"
check "Git 仓库" ".git"
check "Pre-commit 配置" ".pre-commit-config.yaml"
check "Gitignore" ".gitignore"
check "CLAUDE.md" "CLAUDE.md"
check "Docker Compose" "docker-compose.yml" "warn"
check "Copier 答案文件" ".copier-answers.yml" "warn"
# ── Backend ───────────────────────────────────────────────────────────────────
if [ -d "$ROOT/backend" ]; then
section "Backend"
check "pyproject.toml" "backend/pyproject.toml"
check "uv.lock" "backend/uv.lock"
check ".env.example" "backend/.env.example"
check "Dockerfile" "backend/Dockerfile" "warn"
check_contains "ruff 命名规则 N" "backend/pyproject.toml" '"N"'
check_contains "ruff 禁 print T20" "backend/pyproject.toml" '"T20"'
check_contains "mypy strict" "backend/pyproject.toml" "strict = true"
check_contains "pytest-asyncio 配置" "backend/pyproject.toml" "asyncio_mode"
if [ -n "$DB_TYPE" ]; then
local_driver="aiosqlite"
[ "$DB_TYPE" = "postgresql" ] && local_driver="asyncpg"
check_contains "DB driver ($DB_TYPE)" "backend/pyproject.toml" "$local_driver"
fi
else
log_skip "Backend 目录不存在,跳过 Backend 检查"
fi
# ── Agent ─────────────────────────────────────────────────────────────────────
if [ -d "$ROOT/agent" ]; then
section "Agent"
check "pyproject.toml" "agent/pyproject.toml"
check "uv.lock" "agent/uv.lock"
check ".env.example" "agent/.env.example"
check "Dockerfile" "agent/Dockerfile" "warn"
check_contains "ruff 命名规则 N" "agent/pyproject.toml" '"N"'
check_contains "ruff 禁 print T20" "agent/pyproject.toml" '"T20"'
check_contains "mypy strict" "agent/pyproject.toml" "strict = true"
check_contains "pytest-asyncio 配置" "agent/pyproject.toml" "asyncio_mode"
if [ -n "$LLM_PROVIDER" ]; then
case "$LLM_PROVIDER" in
gemini-cli) : ;; # pty 是标准库,无需额外包
openai-api) check_contains "openai SDK" "agent/pyproject.toml" "openai" ;;
anthropic-api) check_contains "anthropic SDK" "agent/pyproject.toml" "anthropic" ;;
esac
fi
else
log_skip "Agent 目录不存在,跳过 Agent 检查"
fi
# ── Frontend ──────────────────────────────────────────────────────────────────
if [ -d "$ROOT/frontend" ]; then
section "Frontend"
check "package.json" "frontend/package.json"
check "pnpm-lock.yaml" "frontend/pnpm-lock.yaml"
check ".env.example" "frontend/.env.example"
check "tsconfig.json" "frontend/tsconfig.json"
check "vite.config.ts" "frontend/vite.config.ts"
check "vitest.config.ts" "frontend/vitest.config.ts" "warn"
check "eslint.config.js" "frontend/eslint.config.js"
check_contains "禁 console.log" "frontend/eslint.config.js" "no-console"
check_contains "命名约定规则" "frontend/eslint.config.js" "naming-convention"
check_contains "TypeScript strict" "frontend/tsconfig.json" '"strict": true'
else
log_skip "Frontend 目录不存在,跳过 Frontend 检查"
fi
# ── Pre-commit 钩子 ───────────────────────────────────────────────────────────
if [ -f "$ROOT/.pre-commit-config.yaml" ]; then
section "Pre-commit 钩子"
check_contains "Conventional Commits" ".pre-commit-config.yaml" "conventional-pre-commit"
check_contains "ruff lint/format" ".pre-commit-config.yaml" "ruff"
check_contains "防密钥泄漏" ".pre-commit-config.yaml" "detect-private-key"
check_contains "gitleaks" ".pre-commit-config.yaml" "gitleaks" "warn"
check "Git hooks 已安装" ".git/hooks/pre-commit" "warn"
fi
# ── Claude 规则文件 ───────────────────────────────────────────────────────────
if [ -d "$ROOT/.claude/rules" ]; then
section "Claude 规则文件"
RULES_DIR="$ROOT/.claude/rules"
# 全局规则(原理层,始终期望存在)
GLOBAL_RULES=(architecture.md ddd.md design-discipline.md communication-protocol.md
testing-tdd.md quality-gates.md security.md config.md
git-workflow.md conventions.md)
for r in "${GLOBAL_RULES[@]}"; do
check "全局规则: $r" ".claude/rules/$r"
done
# Backend 落地层规则
if [ -d "$ROOT/backend" ]; then
check "Backend 规则: backend.md" ".claude/rules/backend.md"
check "Backend 规则: backend-db.md" ".claude/rules/backend-db.md"
check "Backend 规则: backend-concurrency.md" ".claude/rules/backend-concurrency.md"
fi
# Agent 落地层规则
if [ -d "$ROOT/agent" ]; then
check "Agent 规则: agent.md" ".claude/rules/agent.md"
check "Agent 规则: agent-dag.md" ".claude/rules/agent-dag.md"
check "Agent 规则: agent-concurrency.md" ".claude/rules/agent-concurrency.md"
check "Agent 规则: agent-extension-points.md" ".claude/rules/agent-extension-points.md"
# LLM 驱动层规则:按 llm_provider 检查正确的变体
if [ -z "$LLM_PROVIDER" ] || [ "$LLM_PROVIDER" = "gemini-cli" ]; then
check "Agent LLM 驱动规则: agent-llm-pty.md" ".claude/rules/agent-llm-pty.md"
if [ -f "$RULES_DIR/agent-llm-api.md" ]; then
log_warn "Agent LLM 驱动规则冲突 — agent-llm-api.md 不应与 gemini-cli 共存"
fi
else
check "Agent LLM 驱动规则: agent-llm-api.md" ".claude/rules/agent-llm-api.md"
if [ -f "$RULES_DIR/agent-llm-pty.md" ]; then
log_warn "Agent LLM 驱动规则冲突 — agent-llm-pty.md 不应与 $LLM_PROVIDER 共存"
fi
fi
fi
# Frontend 落地层规则
if [ -d "$ROOT/frontend" ]; then
check "Frontend 规则: frontend.md" ".claude/rules/frontend.md"
check "Frontend 规则: frontend-runtime.md" ".claude/rules/frontend-runtime.md"
fi
# 跨服务规则
if [ -d "$ROOT/backend" ] && [ -d "$ROOT/agent" ]; then
check "跨服务规则: data-lifecycle.md" ".claude/rules/data-lifecycle.md"
check "跨服务规则: communication-catalog.md" ".claude/rules/communication-catalog.md" "warn"
check "跨服务规则: error-codes.md" ".claude/rules/error-codes.md" "warn"
fi
else
log_warn ".claude/rules/ 目录不存在 — Claude 规则文件未部署"
fi
# ── 工具链可用性(信息性,不影响退出码)─────────────────────────────────────
section "工具链可用性"
check_cmd "uv" "uv" "warn"
check_cmd "pnpm" "pnpm" "warn"
check_cmd "pre-commit" "pre-commit" "warn"
check_cmd "copier" "copier" "warn"
if [ -d "$ROOT/agent" ]; then
check_cmd "playwright" "playwright" "warn"
fi
# ── 汇总 ──────────────────────────────────────────────────────────────────────
printf "\n${C_BOLD}═══════════════════════════════════════════════════════════════${C_RESET}\n"
printf "${C_BOLD}%-22s %6s %6s %6s${C_RESET}\n" "分类" "通过" "警告" "失败"
printf "───────────────────────────────────────────────────────────────\n"
for i in "${!CATEGORIES[@]}"; do
cat="${CATEGORIES[$i]}"
p=${CAT_PASS[$i]:-0}; w=${CAT_WARN[$i]:-0}; f=${CAT_FAIL[$i]:-0}
[ $((p + w + f)) -eq 0 ] && continue
wc="${C_RESET}"; fc="${C_RESET}"
[ "$w" -gt 0 ] && wc="${C_YELLOW}"
[ "$f" -gt 0 ] && fc="${C_RED}"
printf "%-22s ${C_GREEN}%6d${C_RESET} ${wc}%6d${C_RESET} ${fc}%6d${C_RESET}\n" \
"$cat" "$p" "$w" "$f"
done
printf "───────────────────────────────────────────────────────────────\n"
wc="${C_RESET}"; fc="${C_RESET}"
[ "$TOTAL_WARN" -gt 0 ] && wc="${C_YELLOW}"
[ "$TOTAL_FAIL" -gt 0 ] && fc="${C_RED}"
printf "${C_BOLD}%-22s ${C_GREEN}%6d${C_RESET} ${wc}%6d${C_RESET} ${fc}%6d${C_RESET}${C_BOLD}${C_RESET}\n" \
"合计" "$TOTAL_PASS" "$TOTAL_WARN" "$TOTAL_FAIL"
printf "${C_BOLD}═══════════════════════════════════════════════════════════════${C_RESET}\n"
# 最终判定
if [ "$TOTAL_FAIL" -gt 0 ]; then
printf "\n${C_RED}${C_BOLD}✗ %d 项失败,项目尚不合规${C_RESET}\n" "$TOTAL_FAIL"
[ "$TOTAL_WARN" -gt 0 ] && printf "${C_YELLOW}△ %d 项警告(非阻塞)${C_RESET}\n" "$TOTAL_WARN"
exit 1
elif [ "$STRICT" -eq 1 ] && [ "$TOTAL_WARN" -gt 0 ]; then
printf "\n${C_YELLOW}${C_BOLD}△ 严格模式:%d 项警告视为失败${C_RESET}\n" "$TOTAL_WARN"
exit 1
elif [ "$TOTAL_WARN" -gt 0 ]; then
printf "\n${C_GREEN}${C_BOLD}✓ 核心检查全部通过${C_RESET} ${C_YELLOW}△ %d 项警告(建议修复)${C_RESET}\n" "$TOTAL_WARN"
exit 0
else
printf "\n${C_GREEN}${C_BOLD}✓ 全部 %d 项通过,项目完全合规${C_RESET}\n" "$TOTAL_PASS"
exit 0
fi
+7 -6
View File
@@ -26,7 +26,7 @@ curl -fsSL https://raw.githubusercontent.com/baozaotumao2025/scaffold/main/scrip
可用 preset`principles`(仅规则)· `fullstack` · `fullstack-ai` · `api-only`
> 模板由 copier 从 GitHub 实时拉取。`verify.sh` 在仓库根 `scripts/` 单一维护,安装时由 `install-skill.sh` 组装进 skill,使安装后的 skill 自包含
> 模板由 copier 从 GitHub 实时拉取。skill 目录 `.claude/skills/new-project/` **自包含**SKILL.md + scripts/verify.sh),安装即整目录拷贝,符合 skill 可移植惯例
---
@@ -139,12 +139,13 @@ backend/src/ agent/src/ frontend/sr
scaffold/
├── copier.yml ← 问卷变量 + 条件排除逻辑
├── README.md
├── .claude/skills/new-project/
── SKILL.md ← skill 元数据 + 生成指令verify.sh 安装时组装)
├── .github/workflows/docs-check.yml ← scaffold 自身 CI:防 README 与 template 漂移
├── .claude/skills/new-project/ ← 自包含 skill(拷了即用,符合惯例)
── SKILL.md ← skill 元数据 + 生成指令
│ └── scripts/verify.sh ← skill 自带(与根 scripts/verify.sh 同步,CI 防漂移
├── .github/workflows/docs-check.yml ← scaffold 自身 CI:防文档/verify.sh 漂移
├── scripts/
│ ├── verify.sh ← 工具链合规检查(幂等只读,单一源
│ ├── install-skill.sh ← skill 安装到 ~/.claude/skills/
│ ├── verify.sh ← 工具链合规检查(幂等只读,维护/retrofit 用
│ ├── install-skill.sh ← 整目录拷贝安装 skill 到 ~/.claude/skills/
│ ├── check-docs.sh ← 校验 README 覆盖 template 架构基座目录
│ └── retrofit.sh ← 补装工具链到已有项目
└── template/
+20 -5
View File
@@ -1,7 +1,8 @@
#!/usr/bin/env bash
# 校验 README.md 是否记录了 template/ 的架构基座目录,防止文档随模板改动漂移。
# 仅作用于 scaffold 仓库自身(不进生成项目、不属 skill)。
# 用法:./scripts/check-docs.sh 退出码 0=同步 / 1=有未记录目录
# scaffold 仓库自身的一致性检查(不进生成项目、不属 skill):
# 1) README 是否记录了 template/ 的架构基座目录(防文档漂移)
# 2) skill 自包含的 verify.sh 是否与根 scripts/verify.sh 同步(防两份漂移)
# 用法:./scripts/check-docs.sh 退出码 0=一致 / 1=有问题
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@@ -34,10 +35,24 @@ for entry in ".vscode" ".claude/commands" ".copier-answers.yml" "pnpm-workspace.
fi
done
# ── skill 自包含校验:两份 verify.sh 必须一致 ──────────────────────────────
ROOT_VERIFY="$ROOT/scripts/verify.sh"
SKILL_VERIFY="$ROOT/.claude/skills/new-project/scripts/verify.sh"
if [ -f "$ROOT_VERIFY" ] && [ -f "$SKILL_VERIFY" ]; then
if ! cmp -s "$ROOT_VERIFY" "$SKILL_VERIFY"; then
echo "✗ scripts/verify.sh 与 skill 内副本不一致"
echo " 改了 scripts/verify.sh 后需同步:cp scripts/verify.sh .claude/skills/new-project/scripts/verify.sh"
fail=1
fi
else
echo "✗ verify.sh 缺失(根或 skill 内)"
fail=1
fi
if [ "$fail" -eq 0 ]; then
echo "✓ README 覆盖 template 架构基座目录与关键产物"
echo "✓ README 覆盖 template 架构基座目录skill verify.sh 与根一致"
else
echo ""
echo "→ 请更新 README.md 的「架构基座」章节与「脚手架内容」文件树后重试。"
echo "→ 修复上述问题后重试(文档更新 / 同步 verify.sh。"
exit 1
fi
+13 -16
View File
@@ -1,9 +1,8 @@
#!/usr/bin/env bash
# 把 new-project skill 安装到 ~/.claude/skills/(全局,任意项目可用)。
#
# 单一源原则:verify.sh 只在仓库根 scripts/ 维护一份
# 本脚本在安装时把它和 SKILL.md 组装进 ~/.claude/skills/new-project/
# 使安装后的 skill 自包含(无需仓库即可运行验证)。
# skill 目录是自包含的(SKILL.md + scripts/verify.sh 都在 .claude/skills/new-project/ 内)
# 安装即整目录拷贝——符合 skill 自包含、可移植的惯例,无需"组装"。
#
# 用法:
# curl -fsSL https://raw.githubusercontent.com/baozaotumao2025/scaffold/main/scripts/install-skill.sh | bash
@@ -13,14 +12,12 @@ set -euo pipefail
SKILL_NAME="new-project"
DEST="$HOME/.claude/skills/$SKILL_NAME"
RAW_BASE="https://raw.githubusercontent.com/baozaotumao2025/scaffold/main"
SKILL_MD_URL="$RAW_BASE/.claude/skills/$SKILL_NAME/SKILL.md"
VERIFY_URL="$RAW_BASE/scripts/verify.sh"
RAW_BASE="https://raw.githubusercontent.com/baozaotumao2025/scaffold/main/.claude/skills/$SKILL_NAME"
# skill 自包含的文件清单(新增文件时在此登记)
FILES=("SKILL.md" "scripts/verify.sh")
# 在仓库内运行时优先用本地文件,否则从 GitHub 拉取
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOCAL_SKILL_MD="$SCRIPT_DIR/../.claude/skills/$SKILL_NAME/SKILL.md"
LOCAL_VERIFY="$SCRIPT_DIR/verify.sh"
LOCAL_SKILL="$SCRIPT_DIR/../.claude/skills/$SKILL_NAME"
fetch() { # fetch <url> <dest>
if command -v curl &>/dev/null; then
@@ -35,14 +32,14 @@ fetch() { # fetch <url> <dest>
mkdir -p "$DEST/scripts"
if [[ -f "$LOCAL_SKILL_MD" && -f "$LOCAL_VERIFY" ]]; then
echo "→ 从本地仓库组装安装"
cp "$LOCAL_SKILL_MD" "$DEST/SKILL.md"
cp "$LOCAL_VERIFY" "$DEST/scripts/verify.sh"
if [[ -d "$LOCAL_SKILL" ]]; then
echo "→ 从本地仓库整目录拷贝"
cp -R "$LOCAL_SKILL/." "$DEST/"
else
echo "→ 从 GitHub 组装安装"
fetch "$SKILL_MD_URL" "$DEST/SKILL.md"
fetch "$VERIFY_URL" "$DEST/scripts/verify.sh"
echo "→ 从 GitHub 拉取 skill 文件"
for f in "${FILES[@]}"; do
fetch "$RAW_BASE/$f" "$DEST/$f"
done
fi
chmod +x "$DEST/scripts/verify.sh"