feat: add marketplace-architect skill with Bash scripts and evals
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>
This commit is contained in:
194
.agents/skills/marketplace-architect/scripts/gen_manifests.sh
Executable file
194
.agents/skills/marketplace-architect/scripts/gen_manifests.sh
Executable file
@@ -0,0 +1,194 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user