#!/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). For a plugin with an .apm/ # directory, this script no longer checks those pointer fields itself; that's # scripts/sync-plugin-content.sh --check's job (drift between .apm/ and the flat # plugin-root mirror), wired as its own pre-push hook. # # sync-plugin-content.sh --check explicitly skips any plugin directory lacking .apm/ # (an apm-native package it has nothing to compile), so that delegation leaves a real # gap for a non-apm plugin whose hand-authored plugin.json still uses the old # skills/hooks/mcpServers/agents pointer-field convention: nothing would check whether # those paths resolve. The fallback block below restores that check, but only for # plugins without .apm/ -- apm-native plugins keep relying on the delegation above so # the two checks don't duplicate (and disagree) on the same manifest. 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 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=lib/marketplace-plugins.sh source "$SCRIPT_DIR/lib/marketplace-plugins.sh" while IFS=$'\t' read -r name plugin_dir; do source_rel="${plugin_dir#"$REPO_ROOT"/}" if [[ ! -d "$plugin_dir" ]]; then err "plugin '$name': source directory not found: $source_rel" continue fi manifest="$plugin_dir/.claude-plugin/plugin.json" if [[ ! -f "$manifest" ]]; then err "plugin '$name': .claude-plugin/plugin.json not found in $source_rel" continue fi # apm-native plugin: pointer-field validation is sync-plugin-content.sh --check's # job (see header comment above). [[ -d "$plugin_dir/.apm" ]] && continue # Fallback for a non-apm plugin: validate that any skills/hooks/mcpServers/agents # pointer fields in its hand-authored plugin.json still resolve to real paths. skill_count=$(jq '.skills | if . then length else 0 end' "$manifest") for ((s = 0; s < skill_count; s++)); do skill_path=$(jq -r ".skills[$s]" "$manifest") full_path="$plugin_dir/$skill_path" full_path="${full_path%/}" if [[ ! -d "$full_path" ]]; then err "plugin '$name': skills path not found: $skill_path" fi done for field in hooks mcpServers agents; do ref=$(jq -r ".${field} // empty" "$manifest") [[ -z "$ref" ]] && continue full_path="$plugin_dir/$ref" full_path="${full_path%/}" if [[ ! -e "$full_path" ]]; then err "plugin '$name': $field path not found: $ref" fi done done < <(list_marketplace_local_plugins "$REPO_ROOT" "$MARKETPLACE") if [[ $FAIL -gt 0 ]]; then echo "Manifest check failed: $FAIL error(s)" >&2 exit 1 fi