feat: 重组为标准 skill 包结构 + 修复 verify.sh bash 3.2 兼容
- 新增 .claude/skills/new-project/SKILL.md(标准 skill 格式) - install-skill.sh 安装时组装 SKILL.md + verify.sh,单一源无重复 - verify.sh 改用索引数组替代 declare -A,兼容 macOS bash 3.2 - log_pass/log_skip 显式 return 0,避免 set -e 下非 verbose 误退出 - README 改为 skill 优先;补全 copier.yml/scripts/template 入库
This commit is contained in:
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# 把 new-project skill 安装到 ~/.claude/skills/(全局,任意项目可用)。
|
||||
#
|
||||
# 单一源原则:verify.sh 只在仓库根 scripts/ 维护一份,
|
||||
# 本脚本在安装时把它和 SKILL.md 组装进 ~/.claude/skills/new-project/,
|
||||
# 使安装后的 skill 自包含(无需仓库即可运行验证)。
|
||||
#
|
||||
# 用法:
|
||||
# curl -fsSL https://raw.githubusercontent.com/baozaotumao2025/scaffold/main/scripts/install-skill.sh | bash
|
||||
# 或在已克隆的仓库内:
|
||||
# ./scripts/install-skill.sh
|
||||
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"
|
||||
|
||||
# 在仓库内运行时优先用本地文件,否则从 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"
|
||||
|
||||
fetch() { # fetch <url> <dest>
|
||||
if command -v curl &>/dev/null; then
|
||||
curl -fsSL "$1" -o "$2"
|
||||
elif command -v wget &>/dev/null; then
|
||||
wget -qO "$2" "$1"
|
||||
else
|
||||
echo "错误:需要 curl 或 wget" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
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"
|
||||
else
|
||||
echo "→ 从 GitHub 组装安装"
|
||||
fetch "$SKILL_MD_URL" "$DEST/SKILL.md"
|
||||
fetch "$VERIFY_URL" "$DEST/scripts/verify.sh"
|
||||
fi
|
||||
|
||||
chmod +x "$DEST/scripts/verify.sh"
|
||||
|
||||
echo "✓ 已安装 skill 到 $DEST"
|
||||
echo " 在任意项目中打开 Claude Code,输入 /$SKILL_NAME 或描述\"新建项目\"即可触发"
|
||||
Executable
+330
@@ -0,0 +1,330 @@
|
||||
#!/usr/bin/env bash
|
||||
# retrofit.sh — 对未通过 copier 创建的已有项目补装工具链
|
||||
#
|
||||
# 工作方式:用 copier copy 将 scaffold 模版叠加到已有项目。
|
||||
# - _skip_if_exists 保护已有 src/ 代码,不会覆盖
|
||||
# - 工具安装步骤全部幂等,重复运行安全
|
||||
#
|
||||
# 用法:
|
||||
# ./scripts/retrofit.sh [OPTIONS] [PROJECT_PATH]
|
||||
#
|
||||
# 前置依赖:
|
||||
# pip install copier # 或 uv tool install copier
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── 默认值 ────────────────────────────────────────────────────────────────────
|
||||
SCAFFOLD_URL="${SCAFFOLD_URL:-git+https://github.com/your-org/scaffold.git}"
|
||||
TARGET="."
|
||||
DRY_RUN=0
|
||||
FORCE=0
|
||||
NO_DEPS=0
|
||||
|
||||
# ── 步骤状态跟踪 ──────────────────────────────────────────────────────────────
|
||||
declare -a STEP_NAMES=()
|
||||
declare -a STEP_STATUS=() # ok / skip / warn / fail / dry
|
||||
|
||||
# ── 颜色 ──────────────────────────────────────────────────────────────────────
|
||||
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'
|
||||
C_DIM='\033[2m'
|
||||
else
|
||||
C_GREEN=''; C_YELLOW=''; C_RED=''; C_CYAN=''; C_BOLD=''; C_RESET=''; C_DIM=''
|
||||
fi
|
||||
|
||||
# ── 帮助 ──────────────────────────────────────────────────────────────────────
|
||||
usage() {
|
||||
cat <<EOF
|
||||
${C_BOLD}用法:${C_RESET} $(basename "$0") [OPTIONS] [PROJECT_PATH]
|
||||
|
||||
将 scaffold 工具链叠加到已有项目,幂等安全,可重复运行。
|
||||
已有的 src/ 代码不会被覆盖(copier _skip_if_exists 保护)。
|
||||
|
||||
${C_BOLD}选项:${C_RESET}
|
||||
-h, --help 显示此帮助信息
|
||||
-n, --dry-run 预览所有操作,不实际执行任何命令
|
||||
-f, --force 强制覆盖(移除 _skip_if_exists 保护,慎用)
|
||||
--no-deps 跳过依赖安装(uv sync / pnpm install),仅运行 copier
|
||||
--scaffold URL 指定 scaffold 来源(覆盖 \$SCAFFOLD_URL)
|
||||
|
||||
${C_BOLD}环境变量:${C_RESET}
|
||||
SCAFFOLD_URL scaffold 来源(默认:${SCAFFOLD_URL})
|
||||
|
||||
${C_BOLD}参数:${C_RESET}
|
||||
PROJECT_PATH 目标项目目录(默认:当前目录)
|
||||
|
||||
${C_BOLD}示例:${C_RESET}
|
||||
# 补装工具链到当前目录
|
||||
$(basename "$0")
|
||||
|
||||
# 预览将执行哪些步骤(不实际运行)
|
||||
$(basename "$0") --dry-run /path/to/project
|
||||
|
||||
# 使用本地 scaffold 补装到指定目录
|
||||
SCAFFOLD_URL=./scaffold $(basename "$0") /path/to/project
|
||||
|
||||
# 补装后用 verify.sh 验证
|
||||
$(basename "$0") . && ./scripts/verify.sh .
|
||||
|
||||
${C_BOLD}幂等性:${C_RESET}
|
||||
所有步骤均已做存在检查,重复运行不会造成副作用:
|
||||
- copier copy 使用 _skip_if_exists 保护已有代码
|
||||
- uv sync / pnpm install 天然幂等
|
||||
- pre-commit install 在已装时输出 "already installed"
|
||||
EOF
|
||||
}
|
||||
|
||||
# ── 参数解析 ──────────────────────────────────────────────────────────────────
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-n|--dry-run) DRY_RUN=1; shift ;;
|
||||
-f|--force) FORCE=1; shift ;;
|
||||
--no-deps) NO_DEPS=1; shift ;;
|
||||
--scaffold) SCAFFOLD_URL="$2"; shift 2 ;;
|
||||
--scaffold=*) SCAFFOLD_URL="${1#--scaffold=}"; shift ;;
|
||||
-*) printf "未知选项: %s\n" "$1" >&2; usage >&2; exit 2 ;;
|
||||
*) TARGET="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
TARGET="$(cd "$TARGET" 2>/dev/null && pwd)" \
|
||||
|| { printf "${C_RED}错误:目标路径不存在: %s${C_RESET}\n" "$TARGET" >&2; exit 2; }
|
||||
|
||||
# ── 日志辅助 ──────────────────────────────────────────────────────────────────
|
||||
TS() { date '+%H:%M:%S'; }
|
||||
|
||||
log_info() { printf "${C_DIM}[%s]${C_RESET} ${C_CYAN}[INFO]${C_RESET} %s\n" "$(TS)" "$*"; }
|
||||
log_ok() { printf "${C_DIM}[%s]${C_RESET} ${C_GREEN}[OK]${C_RESET} %s\n" "$(TS)" "$*"; }
|
||||
log_skip() { printf "${C_DIM}[%s]${C_RESET} ${C_YELLOW}[SKIP]${C_RESET} %s\n" "$(TS)" "$*"; }
|
||||
log_warn() { printf "${C_DIM}[%s]${C_RESET} ${C_YELLOW}[WARN]${C_RESET} %s\n" "$(TS)" "$*"; }
|
||||
log_error() { printf "${C_DIM}[%s]${C_RESET} ${C_RED}[ERROR]${C_RESET} %s\n" "$(TS)" "$*" >&2; }
|
||||
log_dry() { printf "${C_DIM}[%s]${C_RESET} ${C_CYAN}[DRY]${C_RESET} %s\n" "$(TS)" "$*"; }
|
||||
|
||||
record_step() {
|
||||
STEP_NAMES+=("$1")
|
||||
STEP_STATUS+=("$2")
|
||||
}
|
||||
|
||||
# 执行命令(dry-run 时只打印)
|
||||
run_cmd() {
|
||||
local desc="$1"; shift
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
log_dry "$desc: $*"
|
||||
else
|
||||
log_info "$desc: $*"
|
||||
if ! "$@"; then
|
||||
log_error "$desc 失败(退出码 $?)"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查命令是否存在,不存在时返回 1(不会因 set -e 退出)
|
||||
require_cmd() {
|
||||
local cmd="$1" install_hint="${2:-}"
|
||||
if command -v "$cmd" &>/dev/null; then
|
||||
local ver
|
||||
ver=$("$cmd" --version 2>/dev/null | head -1 || echo "版本未知")
|
||||
log_info "$cmd 已安装: $ver"
|
||||
return 0
|
||||
else
|
||||
if [ -n "$install_hint" ]; then
|
||||
log_warn "$cmd 未安装。安装方式: $install_hint"
|
||||
else
|
||||
log_warn "$cmd 未安装,将跳过相关步骤"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ── 前置检查 ──────────────────────────────────────────────────────────────────
|
||||
printf "\n${C_BOLD}═══ Retrofit: %s ═══${C_RESET}\n" "$TARGET"
|
||||
[ "$DRY_RUN" -eq 1 ] && printf "${C_YELLOW}${C_BOLD}⚠ DRY-RUN 模式:不执行任何实际操作${C_RESET}\n"
|
||||
[ "$FORCE" -eq 1 ] && printf "${C_YELLOW}${C_BOLD}⚠ FORCE 模式:将覆盖已有配置文件${C_RESET}\n"
|
||||
printf "\n"
|
||||
|
||||
log_info "scaffold 来源: $SCAFFOLD_URL"
|
||||
log_info "目标目录: $TARGET"
|
||||
printf "\n"
|
||||
|
||||
printf "${C_BOLD}[ 前置依赖检查 ]${C_RESET}\n"
|
||||
HAS_COPIER=0; require_cmd copier "pip install copier 或 uv tool install copier" && HAS_COPIER=1 || true
|
||||
HAS_UV=0; require_cmd uv "curl -LsSf https://astral.sh/uv/install.sh | sh" && HAS_UV=1 || true
|
||||
HAS_PNPM=0; require_cmd pnpm "npm install -g pnpm" && HAS_PNPM=1 || true
|
||||
HAS_PC=0; require_cmd pre-commit "pip install pre-commit" && HAS_PC=1 || true
|
||||
|
||||
if [ "$HAS_COPIER" -eq 0 ] && [ "$DRY_RUN" -eq 0 ]; then
|
||||
log_error "copier 是必须依赖,无法继续。请先安装:pip install copier"
|
||||
exit 1
|
||||
fi
|
||||
printf "\n"
|
||||
|
||||
# ── 步骤 1:Copier 模版叠加 ───────────────────────────────────────────────────
|
||||
printf "${C_BOLD}[ 步骤 1/5 ] Copier 模版叠加${C_RESET}\n"
|
||||
COPIER_ARGS=(--trust)
|
||||
[ "$FORCE" -eq 0 ] && COPIER_ARGS+=(--overwrite) || COPIER_ARGS+=(--overwrite --skip-if-exists)
|
||||
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
log_dry "copier copy ${COPIER_ARGS[*]} \"$SCAFFOLD_URL\" \"$TARGET\""
|
||||
record_step "Copier 模版叠加" "dry"
|
||||
else
|
||||
log_info "运行 copier copy,src/ 代码不会被覆盖..."
|
||||
if copier copy "${COPIER_ARGS[@]}" "$SCAFFOLD_URL" "$TARGET"; then
|
||||
log_ok "Copier 模版叠加完成"
|
||||
record_step "Copier 模版叠加" "ok"
|
||||
else
|
||||
log_error "Copier 模版叠加失败"
|
||||
record_step "Copier 模版叠加" "fail"
|
||||
printf "\n${C_RED}致命错误,终止后续步骤${C_RESET}\n"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
printf "\n"
|
||||
|
||||
# ── 步骤 2:Backend uv sync ───────────────────────────────────────────────────
|
||||
printf "${C_BOLD}[ 步骤 2/5 ] Backend 依赖 (uv sync)${C_RESET}\n"
|
||||
if [ "$NO_DEPS" -eq 1 ]; then
|
||||
log_skip "--no-deps 已设置,跳过"
|
||||
record_step "Backend uv sync" "skip"
|
||||
elif [ ! -f "$TARGET/backend/pyproject.toml" ]; then
|
||||
log_skip "backend/ 不存在,跳过"
|
||||
record_step "Backend uv sync" "skip"
|
||||
elif [ "$HAS_UV" -eq 0 ]; then
|
||||
log_warn "uv 未安装,跳过 backend uv sync"
|
||||
record_step "Backend uv sync" "warn"
|
||||
else
|
||||
if run_cmd "backend uv sync" uv sync --frozen --directory "$TARGET/backend"; then
|
||||
log_ok "backend 依赖安装完成(--frozen 保证锁文件一致)"
|
||||
record_step "Backend uv sync" "ok"
|
||||
else
|
||||
log_warn "backend uv sync 失败,继续其余步骤"
|
||||
record_step "Backend uv sync" "warn"
|
||||
fi
|
||||
fi
|
||||
printf "\n"
|
||||
|
||||
# ── 步骤 3:Agent uv sync ─────────────────────────────────────────────────────
|
||||
printf "${C_BOLD}[ 步骤 3/5 ] Agent 依赖 (uv sync)${C_RESET}\n"
|
||||
if [ "$NO_DEPS" -eq 1 ]; then
|
||||
log_skip "--no-deps 已设置,跳过"
|
||||
record_step "Agent uv sync" "skip"
|
||||
elif [ ! -f "$TARGET/agent/pyproject.toml" ]; then
|
||||
log_skip "agent/ 不存在,跳过"
|
||||
record_step "Agent uv sync" "skip"
|
||||
elif [ "$HAS_UV" -eq 0 ]; then
|
||||
log_warn "uv 未安装,跳过 agent uv sync"
|
||||
record_step "Agent uv sync" "warn"
|
||||
else
|
||||
if run_cmd "agent uv sync" uv sync --frozen --directory "$TARGET/agent"; then
|
||||
log_ok "agent 依赖安装完成"
|
||||
record_step "Agent uv sync" "ok"
|
||||
else
|
||||
log_warn "agent uv sync 失败,继续其余步骤"
|
||||
record_step "Agent uv sync" "warn"
|
||||
fi
|
||||
fi
|
||||
printf "\n"
|
||||
|
||||
# ── 步骤 4:Frontend pnpm install ────────────────────────────────────────────
|
||||
printf "${C_BOLD}[ 步骤 4/5 ] Frontend 依赖 (pnpm install)${C_RESET}\n"
|
||||
if [ "$NO_DEPS" -eq 1 ]; then
|
||||
log_skip "--no-deps 已设置,跳过"
|
||||
record_step "Frontend pnpm install" "skip"
|
||||
elif [ ! -f "$TARGET/frontend/package.json" ]; then
|
||||
log_skip "frontend/ 不存在,跳过"
|
||||
record_step "Frontend pnpm install" "skip"
|
||||
elif [ "$HAS_PNPM" -eq 0 ]; then
|
||||
log_warn "pnpm 未安装,跳过 frontend pnpm install"
|
||||
record_step "Frontend pnpm install" "warn"
|
||||
else
|
||||
if run_cmd "pnpm install" pnpm install --frozen-lockfile --dir "$TARGET/frontend"; then
|
||||
log_ok "frontend 依赖安装完成(--frozen-lockfile 保证锁文件一致)"
|
||||
record_step "Frontend pnpm install" "ok"
|
||||
else
|
||||
log_warn "pnpm install 失败,继续其余步骤"
|
||||
record_step "Frontend pnpm install" "warn"
|
||||
fi
|
||||
fi
|
||||
printf "\n"
|
||||
|
||||
# ── 步骤 5:Pre-commit 钩子安装 ───────────────────────────────────────────────
|
||||
printf "${C_BOLD}[ 步骤 5/5 ] Pre-commit 钩子安装${C_RESET}\n"
|
||||
if [ ! -f "$TARGET/.pre-commit-config.yaml" ]; then
|
||||
log_skip ".pre-commit-config.yaml 不存在,跳过"
|
||||
record_step "Pre-commit 钩子" "skip"
|
||||
elif [ "$HAS_PC" -eq 0 ]; then
|
||||
log_warn "pre-commit 未安装,跳过钩子安装"
|
||||
record_step "Pre-commit 钩子" "warn"
|
||||
elif [ ! -d "$TARGET/.git" ]; then
|
||||
log_warn "非 Git 仓库(.git 不存在),跳过钩子安装"
|
||||
record_step "Pre-commit 钩子" "warn"
|
||||
else
|
||||
# 幂等检查:钩子是否已安装
|
||||
if [ "$DRY_RUN" -eq 0 ] && [ -f "$TARGET/.git/hooks/pre-commit" ]; then
|
||||
log_skip "pre-commit 钩子已存在,幂等跳过(使用 --force 强制重装)"
|
||||
record_step "Pre-commit 钩子" "skip"
|
||||
else
|
||||
PC_OK=1
|
||||
run_cmd "pre-commit install" pre-commit install --allow-missing-config \
|
||||
-C "$TARGET" || PC_OK=0
|
||||
run_cmd "pre-commit install commit-msg" pre-commit install \
|
||||
--hook-type commit-msg --allow-missing-config -C "$TARGET" || PC_OK=0
|
||||
if [ "$PC_OK" -eq 1 ]; then
|
||||
log_ok "pre-commit 钩子安装完成(pre-commit + commit-msg)"
|
||||
record_step "Pre-commit 钩子" "ok"
|
||||
else
|
||||
log_warn "pre-commit 钩子安装部分失败"
|
||||
record_step "Pre-commit 钩子" "warn"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
printf "\n"
|
||||
|
||||
# ── 汇总 ──────────────────────────────────────────────────────────────────────
|
||||
SUMMARY_OK=0; SUMMARY_SKIP=0; SUMMARY_WARN=0; SUMMARY_FAIL=0; SUMMARY_DRY=0
|
||||
|
||||
for s in "${STEP_STATUS[@]}"; do
|
||||
case "$s" in
|
||||
ok) SUMMARY_OK=$((SUMMARY_OK+1)) ;;
|
||||
skip) SUMMARY_SKIP=$((SUMMARY_SKIP+1)) ;;
|
||||
warn) SUMMARY_WARN=$((SUMMARY_WARN+1)) ;;
|
||||
fail) SUMMARY_FAIL=$((SUMMARY_FAIL+1)) ;;
|
||||
dry) SUMMARY_DRY=$((SUMMARY_DRY+1)) ;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf "${C_BOLD}═══════════════════════════════════════════════════════════════${C_RESET}\n"
|
||||
printf "${C_BOLD}Retrofit 步骤汇总: %s${C_RESET}\n" "$TARGET"
|
||||
printf "───────────────────────────────────────────────────────────────\n"
|
||||
printf "${C_BOLD}%-30s %s${C_RESET}\n" "步骤" "状态"
|
||||
printf "───────────────────────────────────────────────────────────────\n"
|
||||
for i in "${!STEP_NAMES[@]}"; do
|
||||
name="${STEP_NAMES[$i]}"; status="${STEP_STATUS[$i]}"
|
||||
case "$status" in
|
||||
ok) icon="${C_GREEN}✓ 完成${C_RESET}" ;;
|
||||
skip) icon="${C_CYAN}– 跳过${C_RESET}" ;;
|
||||
warn) icon="${C_YELLOW}△ 警告${C_RESET}" ;;
|
||||
fail) icon="${C_RED}✗ 失败${C_RESET}" ;;
|
||||
dry) icon="${C_CYAN}◌ 预览${C_RESET}" ;;
|
||||
esac
|
||||
printf "%-30s %b\n" "$name" "$icon"
|
||||
done
|
||||
printf "───────────────────────────────────────────────────────────────\n"
|
||||
printf "完成: ${C_GREEN}%d${C_RESET} 跳过: ${C_CYAN}%d${C_RESET} 警告: ${C_YELLOW}%d${C_RESET} 失败: ${C_RED}%d${C_RESET}\n" \
|
||||
"$SUMMARY_OK" "$SUMMARY_SKIP" "$SUMMARY_WARN" "$SUMMARY_FAIL"
|
||||
printf "${C_BOLD}═══════════════════════════════════════════════════════════════${C_RESET}\n"
|
||||
|
||||
# ── 后续建议 ──────────────────────────────────────────────────────────────────
|
||||
printf "\n${C_BOLD}后续步骤:${C_RESET}\n"
|
||||
[ "$DRY_RUN" -eq 1 ] && printf " 1. 移除 --dry-run 后重新运行以实际执行\n"
|
||||
[ "$SUMMARY_WARN" -gt 0 ] && printf " • 检查上方警告项,手动安装缺失工具后重新运行\n"
|
||||
printf " • 运行 ${C_CYAN}./scripts/verify.sh %s${C_RESET} 验证最终结果\n" "$TARGET"
|
||||
[ "$DRY_RUN" -eq 0 ] && [ -d "$TARGET/.git" ] && \
|
||||
printf " • 运行 ${C_CYAN}(cd %s && pre-commit run --all-files)${C_RESET} 验证 hooks\n" "$TARGET"
|
||||
|
||||
printf "\n"
|
||||
|
||||
# ── 退出码 ────────────────────────────────────────────────────────────────────
|
||||
[ "$SUMMARY_FAIL" -gt 0 ] && exit 1 || exit 0
|
||||
Executable
+354
@@ -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
|
||||
Reference in New Issue
Block a user