From fd69bee5fe0c0b6785a6ec3e078b51bec87e278e Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Mon, 22 Dec 2025 13:17:18 -0700 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=85=20Add=20automated=20marketplace?= =?UTF-8?q?=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validates keyword consistency between marketplace.json tags and plugin.json keywords on every PR. This prevents the discoverability issues that were manually caught in PR #16. - scripts/validate-marketplace.sh: Local validation with clear error output - .github/workflows/validate-marketplace.yml: CI on PRs touching plugin files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/validate-marketplace.yml | 19 ++++ scripts/validate-marketplace.sh | 100 +++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 .github/workflows/validate-marketplace.yml create mode 100755 scripts/validate-marketplace.sh diff --git a/.github/workflows/validate-marketplace.yml b/.github/workflows/validate-marketplace.yml new file mode 100644 index 0000000..b17be18 --- /dev/null +++ b/.github/workflows/validate-marketplace.yml @@ -0,0 +1,19 @@ +name: Validate Marketplace + +on: + pull_request: + paths: + - ".claude-plugin/**" + - "plugins/**/.claude-plugin/**" + - "scripts/validate-marketplace.sh" + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate marketplace configuration + run: ./scripts/validate-marketplace.sh diff --git a/scripts/validate-marketplace.sh b/scripts/validate-marketplace.sh new file mode 100755 index 0000000..14722da --- /dev/null +++ b/scripts/validate-marketplace.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# +# Validates marketplace.json consistency with individual plugin.json files +# - Checks that tags in marketplace.json match keywords in plugin.json +# - Validates JSON syntax +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" +MARKETPLACE_FILE="$ROOT_DIR/.claude-plugin/marketplace.json" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +errors=0 + +echo "Validating marketplace configuration..." +echo "" + +# Check jq is available +if ! command -v jq &> /dev/null; then + echo -e "${RED}Error: jq is required but not installed${NC}" + exit 1 +fi + +# Validate marketplace.json syntax +echo "Checking JSON syntax..." +if ! jq empty "$MARKETPLACE_FILE" 2>/dev/null; then + echo -e "${RED}Error: Invalid JSON in marketplace.json${NC}" + exit 1 +fi +echo -e "${GREEN}JSON syntax valid${NC}" +echo "" + +# Get plugin root from metadata +PLUGIN_ROOT=$(jq -r '.metadata.pluginRoot // "."' "$MARKETPLACE_FILE") + +# Validate each plugin's keyword consistency +echo "Checking keyword consistency..." +echo "" + +# Get plugin count +plugin_count=$(jq '.plugins | length' "$MARKETPLACE_FILE") + +for i in $(seq 0 $((plugin_count - 1))); do + # Get plugin info from marketplace + plugin_name=$(jq -r ".plugins[$i].name" "$MARKETPLACE_FILE") + plugin_source=$(jq -r ".plugins[$i].source" "$MARKETPLACE_FILE") + marketplace_tags=$(jq -c ".plugins[$i].tags // []" "$MARKETPLACE_FILE") + + # Construct path to plugin.json + plugin_json_path="$ROOT_DIR/$PLUGIN_ROOT/$plugin_source/.claude-plugin/plugin.json" + + if [[ ! -f "$plugin_json_path" ]]; then + echo -e "${RED} $plugin_name: plugin.json not found at $plugin_json_path${NC}" + ((errors++)) + continue + fi + + # Get keywords from plugin.json + plugin_keywords=$(jq -c '.keywords // []' "$plugin_json_path") + + # Sort both arrays for comparison + sorted_tags=$(echo "$marketplace_tags" | jq -c 'sort') + sorted_keywords=$(echo "$plugin_keywords" | jq -c 'sort') + + if [[ "$sorted_tags" == "$sorted_keywords" ]]; then + echo -e "${GREEN} $plugin_name: keywords match${NC}" + else + echo -e "${RED} $plugin_name: keyword mismatch${NC}" + echo -e " marketplace.json tags: $marketplace_tags" + echo -e " plugin.json keywords: $plugin_keywords" + + # Show the diff + tags_only=$(echo "$marketplace_tags" | jq -c --argjson kw "$plugin_keywords" '. - $kw') + keywords_only=$(echo "$plugin_keywords" | jq -c --argjson tags "$marketplace_tags" '. - $tags') + + if [[ "$tags_only" != "[]" ]]; then + echo -e "${YELLOW} Only in marketplace tags: $tags_only${NC}" + fi + if [[ "$keywords_only" != "[]" ]]; then + echo -e "${YELLOW} Only in plugin keywords: $keywords_only${NC}" + fi + ((errors++)) + fi +done + +echo "" +if [[ $errors -gt 0 ]]; then + echo -e "${RED}Validation failed with $errors error(s)${NC}" + exit 1 +else + echo -e "${GREEN}All validations passed${NC}" + exit 0 +fi From 0420b7db60f97f1d1cb394f5c9d3cac934408d6e Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Mon, 22 Dec 2025 13:06:45 -0800 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=90=9B=20Fix=20marketplace=20schema?= =?UTF-8?q?=20validation=20and=20source=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The validator now catches Claude Code schema requirements like source paths needing "./" prefix. This would have caught the issue before it reached production. Changes: - Add schema validation for required fields and source format - Fix all plugin source paths to start with "./" - Bump marketplace version to 5.1.1 - Improve validator output with clear sections and checkmarks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .claude-plugin/marketplace.json | 4 +- scripts/validate-marketplace.sh | 126 ++++++++++++++++++++++++++------ 2 files changed, 107 insertions(+), 23 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 67372cc..b6d43e1 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,7 +6,7 @@ }, "metadata": { "description": "Professional AI coding configurations, agents, skills, and personalities for Claude Code and Cursor", - "version": "5.1.1", + "version": "6.0.0", "license": "MIT", "repository": "https://github.com/TechNickAI/ai-coding-config", "pluginRoot": "./plugins" @@ -16,7 +16,7 @@ "name": "ai-coding-config", "source": "./core", "description": "Commands, agents, skills, and context for AI-assisted development workflows", - "version": "5.1.0", + "version": "6.0.0", "tags": ["commands", "agents", "skills", "workflows", "essential"] }, { diff --git a/scripts/validate-marketplace.sh b/scripts/validate-marketplace.sh index 14722da..77de5c2 100755 --- a/scripts/validate-marketplace.sh +++ b/scripts/validate-marketplace.sh @@ -1,8 +1,16 @@ #!/usr/bin/env bash # -# Validates marketplace.json consistency with individual plugin.json files -# - Checks that tags in marketplace.json match keywords in plugin.json -# - Validates JSON syntax +# Validates marketplace.json against Claude Code's schema requirements +# and checks consistency with individual plugin.json files. +# +# Schema validations: +# - Required fields: name, source, description, version +# - source must start with "./" +# - plugins array must not be empty +# +# Consistency validations: +# - tags in marketplace.json should match keywords in plugin.json +# - plugin.json must exist at the source path # set -euo pipefail @@ -29,38 +37,113 @@ if ! command -v jq &> /dev/null; then fi # Validate marketplace.json syntax -echo "Checking JSON syntax..." +echo "=== JSON Syntax ===" if ! jq empty "$MARKETPLACE_FILE" 2>/dev/null; then - echo -e "${RED}Error: Invalid JSON in marketplace.json${NC}" + echo -e "${RED}✗ Invalid JSON in marketplace.json${NC}" exit 1 fi -echo -e "${GREEN}JSON syntax valid${NC}" +echo -e "${GREEN}✓ JSON syntax valid${NC}" echo "" -# Get plugin root from metadata -PLUGIN_ROOT=$(jq -r '.metadata.pluginRoot // "."' "$MARKETPLACE_FILE") +# Validate required top-level fields +echo "=== Marketplace Schema ===" -# Validate each plugin's keyword consistency -echo "Checking keyword consistency..." -echo "" +# Check name field +name=$(jq -r '.name // empty' "$MARKETPLACE_FILE") +if [[ -z "$name" ]]; then + echo -e "${RED}✗ Missing required field: name${NC}" + ((errors++)) +else + echo -e "${GREEN}✓ name: $name${NC}" +fi + +# Check metadata.pluginRoot +plugin_root=$(jq -r '.metadata.pluginRoot // empty' "$MARKETPLACE_FILE") +if [[ -z "$plugin_root" ]]; then + echo -e "${YELLOW}⚠ Missing metadata.pluginRoot (using default '.')${NC}" + plugin_root="." +else + echo -e "${GREEN}✓ metadata.pluginRoot: $plugin_root${NC}" +fi -# Get plugin count +# Check plugins array exists and is not empty plugin_count=$(jq '.plugins | length' "$MARKETPLACE_FILE") +if [[ "$plugin_count" -eq 0 ]]; then + echo -e "${RED}✗ plugins array is empty${NC}" + ((errors++)) +else + echo -e "${GREEN}✓ plugins: $plugin_count entries${NC}" +fi +echo "" + +# Validate each plugin's schema +echo "=== Plugin Schema Validation ===" +echo "" + +for i in $(seq 0 $((plugin_count - 1))); do + plugin_name=$(jq -r ".plugins[$i].name // empty" "$MARKETPLACE_FILE") + plugin_source=$(jq -r ".plugins[$i].source // empty" "$MARKETPLACE_FILE") + plugin_desc=$(jq -r ".plugins[$i].description // empty" "$MARKETPLACE_FILE") + plugin_version=$(jq -r ".plugins[$i].version // empty" "$MARKETPLACE_FILE") + + plugin_errors=0 + echo "Plugin: ${plugin_name:-"(unnamed)"}" + + # Check required fields + if [[ -z "$plugin_name" ]]; then + echo -e " ${RED}✗ Missing required field: name${NC}" + ((plugin_errors++)) + fi + + if [[ -z "$plugin_source" ]]; then + echo -e " ${RED}✗ Missing required field: source${NC}" + ((plugin_errors++)) + elif [[ "$plugin_source" != ./* ]]; then + echo -e " ${RED}✗ source must start with './' (got: $plugin_source)${NC}" + ((plugin_errors++)) + fi + + if [[ -z "$plugin_desc" ]]; then + echo -e " ${RED}✗ Missing required field: description${NC}" + ((plugin_errors++)) + fi + + if [[ -z "$plugin_version" ]]; then + echo -e " ${RED}✗ Missing required field: version${NC}" + ((plugin_errors++)) + fi + + if [[ $plugin_errors -eq 0 ]]; then + echo -e " ${GREEN}✓ Schema valid${NC}" + else + ((errors += plugin_errors)) + fi +done +echo "" + +# Validate plugin paths and keyword consistency +echo "=== Plugin Consistency ===" +echo "" for i in $(seq 0 $((plugin_count - 1))); do - # Get plugin info from marketplace plugin_name=$(jq -r ".plugins[$i].name" "$MARKETPLACE_FILE") plugin_source=$(jq -r ".plugins[$i].source" "$MARKETPLACE_FILE") marketplace_tags=$(jq -c ".plugins[$i].tags // []" "$MARKETPLACE_FILE") + # Strip leading "./" from source for path construction + source_path="${plugin_source#./}" + # Construct path to plugin.json - plugin_json_path="$ROOT_DIR/$PLUGIN_ROOT/$plugin_source/.claude-plugin/plugin.json" + plugin_json_path="$ROOT_DIR/$plugin_root/$source_path/.claude-plugin/plugin.json" + + echo "Plugin: $plugin_name" if [[ ! -f "$plugin_json_path" ]]; then - echo -e "${RED} $plugin_name: plugin.json not found at $plugin_json_path${NC}" + echo -e " ${RED}✗ plugin.json not found at: $plugin_json_path${NC}" ((errors++)) continue fi + echo -e " ${GREEN}✓ plugin.json exists${NC}" # Get keywords from plugin.json plugin_keywords=$(jq -c '.keywords // []' "$plugin_json_path") @@ -70,27 +153,28 @@ for i in $(seq 0 $((plugin_count - 1))); do sorted_keywords=$(echo "$plugin_keywords" | jq -c 'sort') if [[ "$sorted_tags" == "$sorted_keywords" ]]; then - echo -e "${GREEN} $plugin_name: keywords match${NC}" + echo -e " ${GREEN}✓ Keywords match${NC}" else - echo -e "${RED} $plugin_name: keyword mismatch${NC}" - echo -e " marketplace.json tags: $marketplace_tags" - echo -e " plugin.json keywords: $plugin_keywords" + echo -e " ${RED}✗ Keyword mismatch${NC}" + echo -e " marketplace tags: $marketplace_tags" + echo -e " plugin keywords: $plugin_keywords" # Show the diff tags_only=$(echo "$marketplace_tags" | jq -c --argjson kw "$plugin_keywords" '. - $kw') keywords_only=$(echo "$plugin_keywords" | jq -c --argjson tags "$marketplace_tags" '. - $tags') if [[ "$tags_only" != "[]" ]]; then - echo -e "${YELLOW} Only in marketplace tags: $tags_only${NC}" + echo -e " ${YELLOW}Only in marketplace: $tags_only${NC}" fi if [[ "$keywords_only" != "[]" ]]; then - echo -e "${YELLOW} Only in plugin keywords: $keywords_only${NC}" + echo -e " ${YELLOW}Only in plugin: $keywords_only${NC}" fi ((errors++)) fi done echo "" +echo "=== Summary ===" if [[ $errors -gt 0 ]]; then echo -e "${RED}Validation failed with $errors error(s)${NC}" exit 1 From c19998e9018eb6d70eb7d07df97105257d183414 Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Mon, 22 Dec 2025 13:11:37 -0800 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=90=9B=20Fix=20validator=20script=20e?= =?UTF-8?q?xiting=20early=20on=20first=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed bash arithmetic that was incompatible with set -e. Changed `((errors++))` to `errors=$((errors + 1))` throughout. The former returns exit status 1 when errors is 0, causing the script to exit immediately instead of collecting all validation errors. Now the script correctly accumulates all errors and reports them at the end. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- scripts/validate-marketplace.sh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/validate-marketplace.sh b/scripts/validate-marketplace.sh index 77de5c2..5cf8eb2 100755 --- a/scripts/validate-marketplace.sh +++ b/scripts/validate-marketplace.sh @@ -52,7 +52,7 @@ echo "=== Marketplace Schema ===" name=$(jq -r '.name // empty' "$MARKETPLACE_FILE") if [[ -z "$name" ]]; then echo -e "${RED}✗ Missing required field: name${NC}" - ((errors++)) + errors=$((errors + 1)) else echo -e "${GREEN}✓ name: $name${NC}" fi @@ -70,7 +70,7 @@ fi plugin_count=$(jq '.plugins | length' "$MARKETPLACE_FILE") if [[ "$plugin_count" -eq 0 ]]; then echo -e "${RED}✗ plugins array is empty${NC}" - ((errors++)) + errors=$((errors + 1)) else echo -e "${GREEN}✓ plugins: $plugin_count entries${NC}" fi @@ -92,31 +92,31 @@ for i in $(seq 0 $((plugin_count - 1))); do # Check required fields if [[ -z "$plugin_name" ]]; then echo -e " ${RED}✗ Missing required field: name${NC}" - ((plugin_errors++)) + plugin_errors=$((plugin_errors + 1)) fi if [[ -z "$plugin_source" ]]; then echo -e " ${RED}✗ Missing required field: source${NC}" - ((plugin_errors++)) + plugin_errors=$((plugin_errors + 1)) elif [[ "$plugin_source" != ./* ]]; then echo -e " ${RED}✗ source must start with './' (got: $plugin_source)${NC}" - ((plugin_errors++)) + plugin_errors=$((plugin_errors + 1)) fi if [[ -z "$plugin_desc" ]]; then echo -e " ${RED}✗ Missing required field: description${NC}" - ((plugin_errors++)) + plugin_errors=$((plugin_errors + 1)) fi if [[ -z "$plugin_version" ]]; then echo -e " ${RED}✗ Missing required field: version${NC}" - ((plugin_errors++)) + plugin_errors=$((plugin_errors + 1)) fi if [[ $plugin_errors -eq 0 ]]; then echo -e " ${GREEN}✓ Schema valid${NC}" else - ((errors += plugin_errors)) + errors=$((errors + plugin_errors)) fi done echo "" @@ -140,7 +140,7 @@ for i in $(seq 0 $((plugin_count - 1))); do if [[ ! -f "$plugin_json_path" ]]; then echo -e " ${RED}✗ plugin.json not found at: $plugin_json_path${NC}" - ((errors++)) + errors=$((errors + 1)) continue fi echo -e " ${GREEN}✓ plugin.json exists${NC}" @@ -169,7 +169,7 @@ for i in $(seq 0 $((plugin_count - 1))); do if [[ "$keywords_only" != "[]" ]]; then echo -e " ${YELLOW}Only in plugin: $keywords_only${NC}" fi - ((errors++)) + errors=$((errors + 1)) fi done