#!/usr/bin/env bash set -euo pipefail # Validates that marketplace.json's local plugin entries resolve to a real directory # containing a .claude-plugin/plugin.json. Run from repo root or pass REPO_ROOT as arg. # # Per ADR-0015, apm.yml is the authoring source and .claude-plugin/plugin.json is # compiled output with no skills/hooks/mcpServers/agents pointer fields (apm's plugin.json # builder deliberately omits them -- Claude Code auto-discovers those convention # directories, so listing them would be redundant/invalid). This script no longer checks # those fields; that's now scripts/sync-plugin-content.sh --check's job (drift between # .apm/ and the flat plugin-root mirror), wired as its own pre-push hook. REPO_ROOT="${1:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}" FAIL=0 err() { echo " FAIL: $1" >&2; FAIL=$((FAIL + 1)); } if ! command -v jq &>/dev/null; then echo "Error: jq is required but not installed" >&2 exit 1 fi MARKETPLACE="$REPO_ROOT/.claude-plugin/marketplace.json" if [[ ! -f "$MARKETPLACE" ]]; then exit 0 fi plugin_count=$(jq '.plugins | length' "$MARKETPLACE") for ((i = 0; i < plugin_count; i++)); do name=$(jq -r ".plugins[$i].name" "$MARKETPLACE") source_type=$(jq -r ".plugins[$i].source | type" "$MARKETPLACE") # Remote sources (github, git, npm objects) have no local directory to check if [[ "$source_type" != "string" ]]; then continue fi source=$(jq -r ".plugins[$i].source" "$MARKETPLACE") source="${source#./}" plugin_dir="$REPO_ROOT/$source" if [[ ! -d "$plugin_dir" ]]; then err "plugin '$name': source directory not found: $source" continue fi manifest="$plugin_dir/.claude-plugin/plugin.json" if [[ ! -f "$manifest" ]]; then err "plugin '$name': .claude-plugin/plugin.json not found in $source" continue fi done if [[ $FAIL -gt 0 ]]; then echo "Manifest check failed: $FAIL error(s)" >&2 exit 1 fi