Adds a new skill for creating, managing, and adopting plugins across Claude Code and GitHub Copilot CLI marketplaces. Includes three Bash scripts (inventory, gen_manifests, validate), three reference docs (cross-compat, claude-code, copilot-cli), a test harness with 18 passing tests, and an eval.yaml. Also adds the `marketplace` category to CATEGORIES.md and commits the plugin marketplace architecture research doc that informed the skill design. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
195 lines
5.9 KiB
Bash
Executable File
195 lines
5.9 KiB
Bash
Executable File
#!/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 <repo-root> --marketplace-name <name> [options]
|
|
#
|
|
# Options:
|
|
# --marketplace-name <name> kebab-case marketplace identifier (required)
|
|
# --author "Name <email>" author string (default: "Unknown <unknown@example.com>")
|
|
# --plugins-dir <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 <unknown@example.com>"
|
|
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 <repo-root> --marketplace-name <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
|