#!/usr/bin/env bash # Generate plugin.json (in both locations) and marketplace.json. # Dry-run by default; pass --write to apply. # # Usage: # gen_manifests.sh --marketplace-name [options] # # Options: # --marketplace-name kebab-case marketplace identifier (required) # --author "Name " author string (default: "Unknown ") # --plugins-dir subdir containing plugin folders (default: plugins) # --mirror-github also write to .github/plugin/marketplace.json # --write apply changes (default is dry run) set -euo pipefail RESERVED_NAMES="claude-code-marketplace claude-code-plugins claude-plugins-official claude-plugins-community claude-community anthropic-marketplace anthropic-plugins agent-skills anthropic-agent-skills knowledge-work-plugins life-sciences claude-for-legal claude-for-financial-services financial-services-plugins" RESERVED_PATTERNS="official-claude anthropic-tools claude-official" is_kebab_case() { [[ "$1" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]] } is_reserved() { local name="$1" for n in $RESERVED_NAMES; do [[ "$name" == "$n" ]] && return 0 done for p in $RESERVED_PATTERNS; do [[ "$name" == "$p"* ]] && return 0 done return 1 } validate_name() { local name="$1" context="$2" local ok=true if ! is_kebab_case "$name"; then echo "ERROR: $context: name '$name' is not kebab-case (lowercase, digits, hyphens only)." >&2 ok=false fi if is_reserved "$name"; then echo "ERROR: $context: name '$name' is reserved for official Anthropic use." >&2 ok=false fi [[ "$ok" == "true" ]] } write_json() { local path="$1" content="$2" dry_run="$3" if [[ "$dry_run" == "true" ]]; then echo "" echo "--- $path (dry run) ---" echo "$content" else mkdir -p "$(dirname "$path")" echo "$content" > "$path" echo " Written: $path" fi } # ── parse args ─────────────────────────────────────────────────────────────── ROOT="" MARKETPLACE_NAME="" AUTHOR="Unknown " PLUGINS_DIR="plugins" MIRROR_GITHUB=false WRITE=false while [[ $# -gt 0 ]]; do case "$1" in --marketplace-name) MARKETPLACE_NAME="$2"; shift 2 ;; --author) AUTHOR="$2"; shift 2 ;; --plugins-dir) PLUGINS_DIR="$2"; shift 2 ;; --mirror-github) MIRROR_GITHUB=true; shift ;; --write) WRITE=true; shift ;; -*) echo "Unknown option: $1" >&2; exit 1 ;; *) ROOT="$1"; shift ;; esac done if [[ -z "$ROOT" || -z "$MARKETPLACE_NAME" ]]; then echo "Usage: gen_manifests.sh --marketplace-name [--write]" >&2 exit 1 fi ROOT="$(cd "$ROOT" && pwd)" DRY_RUN=$( [[ "$WRITE" == "true" ]] && echo "false" || echo "true" ) [[ "$DRY_RUN" == "true" ]] && echo "DRY RUN — pass --write to apply changes" # Parse author AUTHOR_NAME="${AUTHOR%% <*}" AUTHOR_EMAIL="" if [[ "$AUTHOR" =~ \<(.+)\> ]]; then AUTHOR_EMAIL="${BASH_REMATCH[1]}" fi # Validate marketplace name validate_name "$MARKETPLACE_NAME" "marketplace" || exit 1 # Discover plugins PLUGINS_PATH="$ROOT/$PLUGINS_DIR" if [[ ! -d "$PLUGINS_PATH" ]]; then echo "No plugins directory found at $PLUGINS_PATH" >&2 exit 1 fi mapfile -t PLUGIN_DIRS < <(find "$PLUGINS_PATH" -mindepth 1 -maxdepth 1 -type d ! -name '.*' | sort) if [[ ${#PLUGIN_DIRS[@]} -eq 0 ]]; then echo "No plugin directories found in $PLUGINS_PATH" >&2 exit 1 fi echo "Found ${#PLUGIN_DIRS[@]} plugin(s)" # Build plugins array for marketplace.json PLUGINS_JSON="[]" for pd in "${PLUGIN_DIRS[@]}"; do pname="$(basename "$pd")" validate_name "$pname" "plugin '$pname'" || exit 1 # Read existing plugin.json if present existing_claude="$pd/.claude-plugin/plugin.json" existing_root="$pd/plugin.json" existing_desc="Plugin: $pname" existing_name="$pname" for existing in "$existing_claude" "$existing_root"; do if [[ -f "$existing" ]] && jq -e . "$existing" >/dev/null 2>&1; then d=$(jq -r '.description // empty' "$existing") n=$(jq -r '.name // empty' "$existing") [[ -n "$d" ]] && existing_desc="$d" [[ -n "$n" ]] && existing_name="$n" break fi done # Warn on version for existing in "$existing_claude" "$existing_root"; do if [[ -f "$existing" ]] && jq -e '.version' "$existing" >/dev/null 2>&1; then echo " WARNING: plugin '$pname' sets version in plugin.json. Do not also set it in the marketplace entry — plugin.json wins silently." break fi done # Build plugin.json plugin_json=$(jq -n \ --arg name "$existing_name" \ --arg desc "$existing_desc" \ --arg aname "$AUTHOR_NAME" \ --arg aemail "$AUTHOR_EMAIL" \ '{name: $name, description: $desc, author: {name: $aname, email: $aemail}}') write_json "$pd/plugin.json" "$plugin_json" "$DRY_RUN" write_json "$pd/.claude-plugin/plugin.json" "$plugin_json" "$DRY_RUN" source="./$PLUGINS_DIR/$pname" PLUGINS_JSON=$(echo "$PLUGINS_JSON" | jq \ --arg name "$existing_name" \ --arg src "$source" \ --arg desc "$existing_desc" \ '. + [{name: $name, source: $src, description: $desc}]') done # Build marketplace.json marketplace_json=$(jq -n \ --arg name "$MARKETPLACE_NAME" \ --arg aname "$AUTHOR_NAME" \ --arg aemail "$AUTHOR_EMAIL" \ --arg desc "$MARKETPLACE_NAME plugin marketplace" \ --argjson plugins "$PLUGINS_JSON" \ '{name: $name, owner: {name: $aname, email: $aemail}, description: $desc, plugins: $plugins}') write_json "$ROOT/.claude-plugin/marketplace.json" "$marketplace_json" "$DRY_RUN" if [[ "$MIRROR_GITHUB" == "true" ]]; then write_json "$ROOT/.github/plugin/marketplace.json" "$marketplace_json" "$DRY_RUN" fi if [[ "$DRY_RUN" == "true" ]]; then echo "" echo "--- End dry run. Pass --write to apply. ---" else echo "" echo "Done. Run scripts/validate.sh to verify." fi