From 3ae218599b6c1e90a26da0ee7b38088d4a23f794 Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Thu, 12 Mar 2026 11:15:25 +0800 Subject: [PATCH 1/3] feat: add automated plugin validation scripts Adds scripts/validate-plugin.sh and scripts/validate-all.sh to give contributors and CI a fast, zero-dependency way to check plugin health. Closes #18. Co-Authored-By: Claude Sonnet 4.6 --- scripts/validate-all.sh | 145 ++++++++++++++++++++ scripts/validate-plugin.sh | 272 +++++++++++++++++++++++++++++++++++++ 2 files changed, 417 insertions(+) create mode 100755 scripts/validate-all.sh create mode 100755 scripts/validate-plugin.sh diff --git a/scripts/validate-all.sh b/scripts/validate-all.sh new file mode 100755 index 0000000..962cc85 --- /dev/null +++ b/scripts/validate-all.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# validate-all.sh +# Runs validate-plugin.sh against every plugin directory in the repository. +# +# Usage: +# ./scripts/validate-all.sh [repo-root] +# +# If repo-root is omitted, the parent directory of this script is used. +# +# Exit codes: +# 0 All plugins passed validation +# 1 One or more plugins failed validation + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Color helpers +# --------------------------------------------------------------------------- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BOLD='\033[1m' +RESET='\033[0m' + +# --------------------------------------------------------------------------- +# Locate paths +# --------------------------------------------------------------------------- +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="${1:-"$(dirname "$SCRIPT_DIR")"}" +VALIDATE_SCRIPT="$SCRIPT_DIR/validate-plugin.sh" + +if [[ ! -x "$VALIDATE_SCRIPT" ]]; then + echo -e "${RED}ERROR:${RESET} validate-plugin.sh not found or not executable at:" >&2 + echo " $VALIDATE_SCRIPT" >&2 + exit 1 +fi + +if [[ ! -d "$REPO_ROOT" ]]; then + echo -e "${RED}ERROR:${RESET} Repo root '$REPO_ROOT' is not a directory." >&2 + exit 1 +fi + +# --------------------------------------------------------------------------- +# Discover plugin directories +# A directory is considered a plugin if it contains .claude-plugin/plugin.json +# We skip the repo root itself. +# --------------------------------------------------------------------------- +PLUGIN_DIRS=() +while IFS= read -r -d '' plugin_json; do + plugin_dir="$(dirname "$(dirname "$plugin_json")")" + # Skip the repo root + if [[ "$plugin_dir" == "$REPO_ROOT" ]]; then + continue + fi + PLUGIN_DIRS+=("$plugin_dir") +done < <(find "$REPO_ROOT" -name "plugin.json" -path "*/.claude-plugin/plugin.json" -print0 2>/dev/null) + +# Sort for consistent output +IFS=$'\n' PLUGIN_DIRS=($(sort <<<"${PLUGIN_DIRS[*]}")); unset IFS + +if [[ ${#PLUGIN_DIRS[@]} -eq 0 ]]; then + echo -e "${YELLOW}No plugins found in '$REPO_ROOT'.${RESET}" + exit 0 +fi + +echo "" +echo -e "${BOLD}Plugin Validation — $(date '+%Y-%m-%d %H:%M:%S')${RESET}" +echo -e "Repository: $REPO_ROOT" +echo -e "Plugins found: ${#PLUGIN_DIRS[@]}" +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +# --------------------------------------------------------------------------- +# Run validate-plugin.sh for each plugin, capture pass/fail +# --------------------------------------------------------------------------- +PASSED=() +FAILED=() +WARNED=() + +for plugin_dir in "${PLUGIN_DIRS[@]}"; do + plugin_name="$(basename "$plugin_dir")" + + # Run the validator; capture exit code without aborting the loop + if output="$("$VALIDATE_SCRIPT" "$plugin_dir" 2>&1)"; then + echo "$output" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + # Distinguish clean pass vs pass-with-warnings + if echo "$output" | grep -q "\[WARN\]"; then + WARNED+=("$plugin_name") + else + PASSED+=("$plugin_name") + fi + else + echo "$output" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + FAILED+=("$plugin_name") + fi +done + +# --------------------------------------------------------------------------- +# Final summary table +# --------------------------------------------------------------------------- +TOTAL=${#PLUGIN_DIRS[@]} +NUM_PASSED=$(( ${#PASSED[@]} + ${#WARNED[@]} )) +NUM_FAILED=${#FAILED[@]} + +echo "" +echo -e "${BOLD}Overall Results${RESET}" +echo "" +echo -e " Total plugins validated : $TOTAL" +echo -e " ${GREEN}Passed${RESET} : $NUM_PASSED" +echo -e " ${RED}Failed${RESET} : $NUM_FAILED" +echo "" + +if [[ ${#PASSED[@]} -gt 0 ]]; then + echo -e " ${GREEN}Clean passes:${RESET}" + for p in "${PASSED[@]}"; do + echo -e " ${GREEN}✓${RESET} $p" + done + echo "" +fi + +if [[ ${#WARNED[@]} -gt 0 ]]; then + echo -e " ${YELLOW}Passed with warnings:${RESET}" + for p in "${WARNED[@]}"; do + echo -e " ${YELLOW}~${RESET} $p" + done + echo "" +fi + +if [[ ${#FAILED[@]} -gt 0 ]]; then + echo -e " ${RED}Failed:${RESET}" + for p in "${FAILED[@]}"; do + echo -e " ${RED}✗${RESET} $p" + done + echo "" + echo -e "${RED}Validation failed for ${NUM_FAILED} plugin(s). See output above for details.${RESET}" + echo "" + exit 1 +fi + +echo -e "${GREEN}All plugins passed validation.${RESET}" +echo "" +exit 0 diff --git a/scripts/validate-plugin.sh b/scripts/validate-plugin.sh new file mode 100755 index 0000000..e26cdfe --- /dev/null +++ b/scripts/validate-plugin.sh @@ -0,0 +1,272 @@ +#!/usr/bin/env bash +# validate-plugin.sh +# Validates a single plugin directory against the expected structure and content rules. +# +# Usage: +# ./scripts/validate-plugin.sh +# +# Exit codes: +# 0 All checks passed (or only warnings) +# 1 One or more checks failed + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Color helpers +# --------------------------------------------------------------------------- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BOLD='\033[1m' +RESET='\033[0m' + +pass() { echo -e " ${GREEN}[PASS]${RESET} $*"; } +fail() { echo -e " ${RED}[FAIL]${RESET} $*"; FAIL_COUNT=$((FAIL_COUNT + 1)); } +warn() { echo -e " ${YELLOW}[WARN]${RESET} $*"; WARN_COUNT=$((WARN_COUNT + 1)); } +info() { echo -e " $*"; } + +FAIL_COUNT=0 +WARN_COUNT=0 + +# --------------------------------------------------------------------------- +# Argument parsing +# --------------------------------------------------------------------------- +if [[ $# -lt 1 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +PLUGIN_DIR="${1%/}" # strip trailing slash + +if [[ ! -d "$PLUGIN_DIR" ]]; then + echo -e "${RED}ERROR:${RESET} '$PLUGIN_DIR' is not a directory." >&2 + exit 1 +fi + +PLUGIN_NAME="$(basename "$PLUGIN_DIR")" + +echo "" +echo -e "${BOLD}Validating plugin: ${PLUGIN_NAME}${RESET}" +echo -e " Path: $PLUGIN_DIR" +echo "" + +# --------------------------------------------------------------------------- +# Helper: check YAML frontmatter contains a required field +# Usage: has_frontmatter_field +# Returns 0 if found, 1 if not +# --------------------------------------------------------------------------- +has_frontmatter_field() { + local file="$1" + local field="$2" + # frontmatter is between the first and second '---' lines + awk '/^---/{c++; if(c==2) exit} c==1 && /^'"$field"'\s*:/' "$file" | grep -q . +} + +# --------------------------------------------------------------------------- +# 1. .claude-plugin/plugin.json — required +# --------------------------------------------------------------------------- +echo -e "${BOLD}[1] .claude-plugin/plugin.json${RESET}" + +PLUGIN_JSON="$PLUGIN_DIR/.claude-plugin/plugin.json" + +if [[ ! -f "$PLUGIN_JSON" ]]; then + fail ".claude-plugin/plugin.json not found (required)" +else + pass ".claude-plugin/plugin.json exists" + + # Must be valid JSON + if ! python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$PLUGIN_JSON" 2>/dev/null; then + fail ".claude-plugin/plugin.json is not valid JSON" + else + pass ".claude-plugin/plugin.json is valid JSON" + + # Required fields: name, version, description + for field in name version description; do + value="$(python3 -c " +import json,sys +d=json.load(open(sys.argv[1])) +print(d.get(sys.argv[2], '')) +" "$PLUGIN_JSON" "$field" 2>/dev/null)" + + if [[ -z "$value" ]]; then + fail "plugin.json missing required field: '$field'" + else + pass "plugin.json has '$field': $value" + fi + done + fi +fi + +echo "" + +# --------------------------------------------------------------------------- +# 2. commands/*.md — YAML frontmatter with 'description' +# --------------------------------------------------------------------------- +echo -e "${BOLD}[2] commands/*.md — YAML frontmatter${RESET}" + +COMMANDS_DIR="$PLUGIN_DIR/commands" + +if [[ ! -d "$COMMANDS_DIR" ]]; then + info "No commands/ directory (optional — skipping)" +else + CMD_FILES=() + while IFS= read -r -d '' f; do + CMD_FILES+=("$f") + done < <(find "$COMMANDS_DIR" -maxdepth 1 -name "*.md" -print0 2>/dev/null) + + if [[ ${#CMD_FILES[@]} -eq 0 ]]; then + info "commands/ directory is empty (optional)" + else + for cmd_file in "${CMD_FILES[@]}"; do + rel="${cmd_file#"$PLUGIN_DIR/"}" + + # Check file starts with --- (has frontmatter at all) + first_line="$(head -1 "$cmd_file")" + if [[ "$first_line" != "---" ]]; then + fail "$rel: missing YAML frontmatter (file should start with ---)" + continue + fi + + if has_frontmatter_field "$cmd_file" "description"; then + pass "$rel: has 'description' in frontmatter" + else + fail "$rel: frontmatter missing required 'description' field" + fi + done + fi +fi + +echo "" + +# --------------------------------------------------------------------------- +# 3. skills/*/SKILL.md — YAML frontmatter with 'name' and 'description' +# --------------------------------------------------------------------------- +echo -e "${BOLD}[3] skills/*/SKILL.md — YAML frontmatter${RESET}" + +SKILLS_DIR="$PLUGIN_DIR/skills" + +if [[ ! -d "$SKILLS_DIR" ]]; then + info "No skills/ directory (optional — skipping)" +else + SKILL_FILES=() + while IFS= read -r -d '' f; do + SKILL_FILES+=("$f") + done < <(find "$SKILLS_DIR" -name "SKILL.md" -print0 2>/dev/null) + + if [[ ${#SKILL_FILES[@]} -eq 0 ]]; then + info "skills/ directory has no SKILL.md files (optional)" + else + for skill_file in "${SKILL_FILES[@]}"; do + rel="${skill_file#"$PLUGIN_DIR/"}" + + first_line="$(head -1 "$skill_file")" + if [[ "$first_line" != "---" ]]; then + fail "$rel: missing YAML frontmatter (file should start with ---)" + continue + fi + + skill_ok=true + for field in name description; do + if has_frontmatter_field "$skill_file" "$field"; then + pass "$rel: has '$field' in frontmatter" + else + fail "$rel: frontmatter missing required '$field' field" + skill_ok=false + fi + done + done + fi +fi + +echo "" + +# --------------------------------------------------------------------------- +# 4. .mcp.json — valid JSON if present +# --------------------------------------------------------------------------- +echo -e "${BOLD}[4] .mcp.json (optional)${RESET}" + +MCP_JSON="$PLUGIN_DIR/.mcp.json" + +if [[ ! -f "$MCP_JSON" ]]; then + info ".mcp.json not found (optional — skipping)" +else + if ! python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$MCP_JSON" 2>/dev/null; then + fail ".mcp.json is not valid JSON" + else + pass ".mcp.json is valid JSON" + + # Warn if mcpServers key is missing (expected structure) + has_mcp_servers="$(python3 -c " +import json,sys +d=json.load(open(sys.argv[1])) +print('yes' if 'mcpServers' in d else 'no') +" "$MCP_JSON" 2>/dev/null)" + + if [[ "$has_mcp_servers" != "yes" ]]; then + warn ".mcp.json does not contain a top-level 'mcpServers' key (expected)" + else + pass ".mcp.json has 'mcpServers' key" + fi + fi +fi + +echo "" + +# --------------------------------------------------------------------------- +# 5. ~~ placeholder check +# Files that contain ~~ placeholders should have a CONNECTORS.md nearby +# --------------------------------------------------------------------------- +echo -e "${BOLD}[5] ~~ placeholder consistency${RESET}" + +# Collect all .md files in the plugin (excluding CONNECTORS.md itself) +ALL_MD_FILES=() +while IFS= read -r -d '' f; do + ALL_MD_FILES+=("$f") +done < <(find "$PLUGIN_DIR" -name "*.md" ! -name "CONNECTORS.md" -print0 2>/dev/null) + +PLACEHOLDER_FILES=() +for md_file in "${ALL_MD_FILES[@]}"; do + if grep -q "~~" "$md_file" 2>/dev/null; then + PLACEHOLDER_FILES+=("$md_file") + fi +done + +if [[ ${#PLACEHOLDER_FILES[@]} -eq 0 ]]; then + info "No ~~ placeholders found in .md files" +else + CONNECTORS_MD="$PLUGIN_DIR/CONNECTORS.md" + if [[ ! -f "$CONNECTORS_MD" ]]; then + for pf in "${PLACEHOLDER_FILES[@]}"; do + rel="${pf#"$PLUGIN_DIR/"}" + warn "$rel: contains ~~ placeholders but CONNECTORS.md is missing" + done + else + for pf in "${PLACEHOLDER_FILES[@]}"; do + rel="${pf#"$PLUGIN_DIR/"}" + pass "$rel: contains ~~ placeholders and CONNECTORS.md exists" + done + fi +fi + +echo "" + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +echo -e "${BOLD}Summary for ${PLUGIN_NAME}${RESET}" + +if [[ $FAIL_COUNT -eq 0 && $WARN_COUNT -eq 0 ]]; then + echo -e " ${GREEN}All checks passed.${RESET}" +elif [[ $FAIL_COUNT -eq 0 ]]; then + echo -e " ${GREEN}All checks passed${RESET} with ${YELLOW}${WARN_COUNT} warning(s)${RESET}." +else + echo -e " ${RED}${FAIL_COUNT} check(s) failed${RESET}, ${YELLOW}${WARN_COUNT} warning(s)${RESET}." +fi + +echo "" + +if [[ $FAIL_COUNT -gt 0 ]]; then + exit 1 +fi + +exit 0 From fc46bb7e61e7b1a6c126e59c428452d577b8088d Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Thu, 12 Mar 2026 11:29:42 +0800 Subject: [PATCH 2/3] fix: use printf instead of echo -e and POSIX awk classes for portability - Replace echo -e with printf for reliable escape sequence handling - Replace \s with [[:space:]] in awk regex for BSD awk compatibility - Use printf %s/%d format specifiers instead of variable interpolation Addresses review feedback on PR #112. --- scripts/validate-all.sh | 36 ++++++++++++++++++------------------ scripts/validate-plugin.sh | 34 +++++++++++++++++----------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/scripts/validate-all.sh b/scripts/validate-all.sh index 962cc85..a96329b 100755 --- a/scripts/validate-all.sh +++ b/scripts/validate-all.sh @@ -30,13 +30,13 @@ REPO_ROOT="${1:-"$(dirname "$SCRIPT_DIR")"}" VALIDATE_SCRIPT="$SCRIPT_DIR/validate-plugin.sh" if [[ ! -x "$VALIDATE_SCRIPT" ]]; then - echo -e "${RED}ERROR:${RESET} validate-plugin.sh not found or not executable at:" >&2 + printf "${RED}ERROR:${RESET} validate-plugin.sh not found or not executable at:\n" >&2 echo " $VALIDATE_SCRIPT" >&2 exit 1 fi if [[ ! -d "$REPO_ROOT" ]]; then - echo -e "${RED}ERROR:${RESET} Repo root '$REPO_ROOT' is not a directory." >&2 + printf "${RED}ERROR:${RESET} Repo root '%s' is not a directory.\n" "$REPO_ROOT" >&2 exit 1 fi @@ -59,14 +59,14 @@ done < <(find "$REPO_ROOT" -name "plugin.json" -path "*/.claude-plugin/plugin.js IFS=$'\n' PLUGIN_DIRS=($(sort <<<"${PLUGIN_DIRS[*]}")); unset IFS if [[ ${#PLUGIN_DIRS[@]} -eq 0 ]]; then - echo -e "${YELLOW}No plugins found in '$REPO_ROOT'.${RESET}" + printf "${YELLOW}No plugins found in '%s'.${RESET}\n" "$REPO_ROOT" exit 0 fi echo "" -echo -e "${BOLD}Plugin Validation — $(date '+%Y-%m-%d %H:%M:%S')${RESET}" -echo -e "Repository: $REPO_ROOT" -echo -e "Plugins found: ${#PLUGIN_DIRS[@]}" +printf "${BOLD}Plugin Validation — $(date '+%Y-%m-%d %H:%M:%S')${RESET}\n" +printf "Repository: %s\n" "$REPO_ROOT" +printf "Plugins found: %d\n" "${#PLUGIN_DIRS[@]}" echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @@ -106,40 +106,40 @@ NUM_PASSED=$(( ${#PASSED[@]} + ${#WARNED[@]} )) NUM_FAILED=${#FAILED[@]} echo "" -echo -e "${BOLD}Overall Results${RESET}" +printf "${BOLD}Overall Results${RESET}\n" echo "" -echo -e " Total plugins validated : $TOTAL" -echo -e " ${GREEN}Passed${RESET} : $NUM_PASSED" -echo -e " ${RED}Failed${RESET} : $NUM_FAILED" +printf " Total plugins validated : %d\n" "$TOTAL" +printf " ${GREEN}Passed${RESET} : %d\n" "$NUM_PASSED" +printf " ${RED}Failed${RESET} : %d\n" "$NUM_FAILED" echo "" if [[ ${#PASSED[@]} -gt 0 ]]; then - echo -e " ${GREEN}Clean passes:${RESET}" + printf " ${GREEN}Clean passes:${RESET}\n" for p in "${PASSED[@]}"; do - echo -e " ${GREEN}✓${RESET} $p" + printf " ${GREEN}✓${RESET} %s\n" "$p" done echo "" fi if [[ ${#WARNED[@]} -gt 0 ]]; then - echo -e " ${YELLOW}Passed with warnings:${RESET}" + printf " ${YELLOW}Passed with warnings:${RESET}\n" for p in "${WARNED[@]}"; do - echo -e " ${YELLOW}~${RESET} $p" + printf " ${YELLOW}~${RESET} %s\n" "$p" done echo "" fi if [[ ${#FAILED[@]} -gt 0 ]]; then - echo -e " ${RED}Failed:${RESET}" + printf " ${RED}Failed:${RESET}\n" for p in "${FAILED[@]}"; do - echo -e " ${RED}✗${RESET} $p" + printf " ${RED}✗${RESET} %s\n" "$p" done echo "" - echo -e "${RED}Validation failed for ${NUM_FAILED} plugin(s). See output above for details.${RESET}" + printf "${RED}Validation failed for %d plugin(s). See output above for details.${RESET}\n" "$NUM_FAILED" echo "" exit 1 fi -echo -e "${GREEN}All plugins passed validation.${RESET}" +printf "${GREEN}All plugins passed validation.${RESET}\n" echo "" exit 0 diff --git a/scripts/validate-plugin.sh b/scripts/validate-plugin.sh index e26cdfe..4e7b860 100755 --- a/scripts/validate-plugin.sh +++ b/scripts/validate-plugin.sh @@ -20,10 +20,10 @@ YELLOW='\033[1;33m' BOLD='\033[1m' RESET='\033[0m' -pass() { echo -e " ${GREEN}[PASS]${RESET} $*"; } -fail() { echo -e " ${RED}[FAIL]${RESET} $*"; FAIL_COUNT=$((FAIL_COUNT + 1)); } -warn() { echo -e " ${YELLOW}[WARN]${RESET} $*"; WARN_COUNT=$((WARN_COUNT + 1)); } -info() { echo -e " $*"; } +pass() { printf " ${GREEN}[PASS]${RESET} %s\n" "$*"; } +fail() { printf " ${RED}[FAIL]${RESET} %s\n" "$*"; FAIL_COUNT=$((FAIL_COUNT + 1)); } +warn() { printf " ${YELLOW}[WARN]${RESET} %s\n" "$*"; WARN_COUNT=$((WARN_COUNT + 1)); } +info() { printf " %s\n" "$*"; } FAIL_COUNT=0 WARN_COUNT=0 @@ -39,15 +39,15 @@ fi PLUGIN_DIR="${1%/}" # strip trailing slash if [[ ! -d "$PLUGIN_DIR" ]]; then - echo -e "${RED}ERROR:${RESET} '$PLUGIN_DIR' is not a directory." >&2 + printf "${RED}ERROR:${RESET} '%s' is not a directory.\n" "$PLUGIN_DIR" >&2 exit 1 fi PLUGIN_NAME="$(basename "$PLUGIN_DIR")" echo "" -echo -e "${BOLD}Validating plugin: ${PLUGIN_NAME}${RESET}" -echo -e " Path: $PLUGIN_DIR" +printf "${BOLD}Validating plugin: ${PLUGIN_NAME}${RESET}\n" +printf " Path: %s\n" "$PLUGIN_DIR" echo "" # --------------------------------------------------------------------------- @@ -59,13 +59,13 @@ has_frontmatter_field() { local file="$1" local field="$2" # frontmatter is between the first and second '---' lines - awk '/^---/{c++; if(c==2) exit} c==1 && /^'"$field"'\s*:/' "$file" | grep -q . + awk '/^---/{c++; if(c==2) exit} c==1 && /^'"$field"'[[:space:]]*:/' "$file" | grep -q . } # --------------------------------------------------------------------------- # 1. .claude-plugin/plugin.json — required # --------------------------------------------------------------------------- -echo -e "${BOLD}[1] .claude-plugin/plugin.json${RESET}" +printf "${BOLD}[1] .claude-plugin/plugin.json${RESET}\n" PLUGIN_JSON="$PLUGIN_DIR/.claude-plugin/plugin.json" @@ -102,7 +102,7 @@ echo "" # --------------------------------------------------------------------------- # 2. commands/*.md — YAML frontmatter with 'description' # --------------------------------------------------------------------------- -echo -e "${BOLD}[2] commands/*.md — YAML frontmatter${RESET}" +printf "${BOLD}[2] commands/*.md — YAML frontmatter${RESET}\n" COMMANDS_DIR="$PLUGIN_DIR/commands" @@ -141,7 +141,7 @@ echo "" # --------------------------------------------------------------------------- # 3. skills/*/SKILL.md — YAML frontmatter with 'name' and 'description' # --------------------------------------------------------------------------- -echo -e "${BOLD}[3] skills/*/SKILL.md — YAML frontmatter${RESET}" +printf "${BOLD}[3] skills/*/SKILL.md — YAML frontmatter${RESET}\n" SKILLS_DIR="$PLUGIN_DIR/skills" @@ -183,7 +183,7 @@ echo "" # --------------------------------------------------------------------------- # 4. .mcp.json — valid JSON if present # --------------------------------------------------------------------------- -echo -e "${BOLD}[4] .mcp.json (optional)${RESET}" +printf "${BOLD}[4] .mcp.json (optional)${RESET}\n" MCP_JSON="$PLUGIN_DIR/.mcp.json" @@ -216,7 +216,7 @@ echo "" # 5. ~~ placeholder check # Files that contain ~~ placeholders should have a CONNECTORS.md nearby # --------------------------------------------------------------------------- -echo -e "${BOLD}[5] ~~ placeholder consistency${RESET}" +printf "${BOLD}[5] ~~ placeholder consistency${RESET}\n" # Collect all .md files in the plugin (excluding CONNECTORS.md itself) ALL_MD_FILES=() @@ -253,14 +253,14 @@ echo "" # --------------------------------------------------------------------------- # Summary # --------------------------------------------------------------------------- -echo -e "${BOLD}Summary for ${PLUGIN_NAME}${RESET}" +printf "${BOLD}Summary for ${PLUGIN_NAME}${RESET}\n" if [[ $FAIL_COUNT -eq 0 && $WARN_COUNT -eq 0 ]]; then - echo -e " ${GREEN}All checks passed.${RESET}" + printf " ${GREEN}All checks passed.${RESET}\n" elif [[ $FAIL_COUNT -eq 0 ]]; then - echo -e " ${GREEN}All checks passed${RESET} with ${YELLOW}${WARN_COUNT} warning(s)${RESET}." + printf " ${GREEN}All checks passed${RESET} with ${YELLOW}%d warning(s)${RESET}.\n" "$WARN_COUNT" else - echo -e " ${RED}${FAIL_COUNT} check(s) failed${RESET}, ${YELLOW}${WARN_COUNT} warning(s)${RESET}." + printf " ${RED}%d check(s) failed${RESET}, ${YELLOW}%d warning(s)${RESET}.\n" "$FAIL_COUNT" "$WARN_COUNT" fi echo "" From f60c82405040fca328205ab97205867abf882882 Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Thu, 12 Mar 2026 12:07:10 +0800 Subject: [PATCH 3/3] fix: address security and robustness review feedback --- scripts/validate-all.sh | 6 +++--- scripts/validate-plugin.sh | 8 +++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/scripts/validate-all.sh b/scripts/validate-all.sh index a96329b..d902af5 100755 --- a/scripts/validate-all.sh +++ b/scripts/validate-all.sh @@ -55,14 +55,14 @@ while IFS= read -r -d '' plugin_json; do PLUGIN_DIRS+=("$plugin_dir") done < <(find "$REPO_ROOT" -name "plugin.json" -path "*/.claude-plugin/plugin.json" -print0 2>/dev/null) -# Sort for consistent output -IFS=$'\n' PLUGIN_DIRS=($(sort <<<"${PLUGIN_DIRS[*]}")); unset IFS - if [[ ${#PLUGIN_DIRS[@]} -eq 0 ]]; then printf "${YELLOW}No plugins found in '%s'.${RESET}\n" "$REPO_ROOT" exit 0 fi +# Sort for consistent output +IFS=$'\n' PLUGIN_DIRS=($(sort <<<"${PLUGIN_DIRS[*]}")); unset IFS + echo "" printf "${BOLD}Plugin Validation — $(date '+%Y-%m-%d %H:%M:%S')${RESET}\n" printf "Repository: %s\n" "$REPO_ROOT" diff --git a/scripts/validate-plugin.sh b/scripts/validate-plugin.sh index 4e7b860..d43d2ed 100755 --- a/scripts/validate-plugin.sh +++ b/scripts/validate-plugin.sh @@ -46,7 +46,7 @@ fi PLUGIN_NAME="$(basename "$PLUGIN_DIR")" echo "" -printf "${BOLD}Validating plugin: ${PLUGIN_NAME}${RESET}\n" +printf "${BOLD}Validating plugin: %s${RESET}\n" "$PLUGIN_NAME" printf " Path: %s\n" "$PLUGIN_DIR" echo "" @@ -59,7 +59,7 @@ has_frontmatter_field() { local file="$1" local field="$2" # frontmatter is between the first and second '---' lines - awk '/^---/{c++; if(c==2) exit} c==1 && /^'"$field"'[[:space:]]*:/' "$file" | grep -q . + awk -v f="$field" '/^---/{c++; if(c==2) exit} c==1 && $0 ~ "^"f"[[:space:]]*:"' "$file" | grep -q . } # --------------------------------------------------------------------------- @@ -165,13 +165,11 @@ else continue fi - skill_ok=true for field in name description; do if has_frontmatter_field "$skill_file" "$field"; then pass "$rel: has '$field' in frontmatter" else fail "$rel: frontmatter missing required '$field' field" - skill_ok=false fi done done @@ -253,7 +251,7 @@ echo "" # --------------------------------------------------------------------------- # Summary # --------------------------------------------------------------------------- -printf "${BOLD}Summary for ${PLUGIN_NAME}${RESET}\n" +printf "${BOLD}Summary for %s${RESET}\n" "$PLUGIN_NAME" if [[ $FAIL_COUNT -eq 0 && $WARN_COUNT -eq 0 ]]; then printf " ${GREEN}All checks passed.${RESET}\n"