## Why Plugin and marketplace management had no governed authoring path. Creating or updating a plugin required knowing the dual-manifest convention, version parity rules, and directory skeleton by memory — nothing enforced consistency or guided the process. `/plugin-author` closes that gap by owning the full plugin scaffold lifecycle: create, update, rename, and release. `/marketplace-author` handles the marketplace-facing side: register, deregister, and update plugin entries in `marketplace.json`. ADR-0016 codifies the version parity convention (identical `version` in both `plugin.json` and `.claude-plugin/plugin.json`) that `/plugin-author` now enforces. The two plugin.json files in this repo are backfilled to comply (keys also sorted to pass the pretty-format-json hook). CONTEXT.md gains glossary entries for "plugin scaffold" and "version parity" so future agents have shared vocabulary for these concepts. ## Implementation Notes `/plugin-author` ships a `scripts/new-plugin.sh` scaffold script that generates the directory skeleton and both manifests in one shot; the skill calls the script rather than generating files ad hoc so the scaffold is reviewable and repeatable. Version parity is an invariant, not a suggestion — the skill will fail loudly on create/update if the two versions would diverge. ADR: docs/adr/0016-plugin-version-parity.md
148 lines
4.2 KiB
Bash
Executable File
148 lines
4.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: new-plugin.sh <plugin-name> <repo-root>
|
|
|
|
Scaffold a new plugin directory with both manifests and skeleton dirs.
|
|
|
|
Arguments:
|
|
plugin-name Kebab-case plugin identifier (e.g. my-tools, data-tools).
|
|
Must be lowercase letters, numbers, and hyphens only.
|
|
No leading, trailing, or consecutive hyphens.
|
|
repo-root Absolute or relative path to the repository root.
|
|
The plugin is created at <repo-root>/plugins/<plugin-name>/.
|
|
|
|
Created structure:
|
|
<repo-root>/plugins/<plugin-name>/
|
|
plugin.json Copilot CLI manifest (FILL_IN_* placeholders)
|
|
.claude-plugin/
|
|
plugin.json Claude Code manifest (FILL_IN_* placeholders)
|
|
skills/ Empty skeleton directory
|
|
agents/ Empty skeleton directory
|
|
hooks/ Empty skeleton directory
|
|
bin/ Empty skeleton directory
|
|
|
|
Each file and directory is a no-op if it already exists.
|
|
Does NOT touch marketplace.json.
|
|
|
|
Exit codes:
|
|
0 Files created or already existed (no-op)
|
|
1 Invalid arguments or missing root
|
|
EOF
|
|
}
|
|
|
|
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
|
usage
|
|
exit 0
|
|
fi
|
|
|
|
if [[ $# -lt 2 ]]; then
|
|
echo "Error: plugin-name and repo-root are required." >&2
|
|
echo "" >&2
|
|
usage >&2
|
|
exit 1
|
|
fi
|
|
|
|
PLUGIN_NAME="$1"
|
|
REPO_ROOT="$2"
|
|
|
|
# Validate plugin name format
|
|
if ! echo "$PLUGIN_NAME" | grep -qE '^[a-z0-9]+(-[a-z0-9]+)*$'; then
|
|
echo "Error: plugin-name must use lowercase letters, numbers, and hyphens only." >&2
|
|
echo " No leading, trailing, or consecutive hyphens." >&2
|
|
echo " Received: '$PLUGIN_NAME'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Expand tilde
|
|
REPO_ROOT="${REPO_ROOT/#\~/$HOME}"
|
|
|
|
# Resolve to absolute path
|
|
REPO_ROOT="$(cd "$REPO_ROOT" 2>/dev/null && pwd)" || {
|
|
echo "Error: repo-root directory '$2' does not exist." >&2
|
|
exit 1
|
|
}
|
|
|
|
PLUGIN_DIR="$REPO_ROOT/plugins/$PLUGIN_NAME"
|
|
CC_DIR="$PLUGIN_DIR/.claude-plugin"
|
|
COPILOT_MANIFEST="$PLUGIN_DIR/plugin.json"
|
|
CC_MANIFEST="$CC_DIR/plugin.json"
|
|
|
|
# Create directory skeleton
|
|
created_any=false
|
|
|
|
create_dir_if_missing() {
|
|
local dir="$1"
|
|
if [[ -d "$dir" ]]; then
|
|
echo "Skipping directory '$dir' — already exists." >&2
|
|
else
|
|
mkdir -p "$dir"
|
|
echo "Created directory: $dir" >&2
|
|
created_any=true
|
|
fi
|
|
}
|
|
|
|
create_dir_if_missing "$PLUGIN_DIR"
|
|
create_dir_if_missing "$CC_DIR"
|
|
create_dir_if_missing "$PLUGIN_DIR/skills"
|
|
create_dir_if_missing "$PLUGIN_DIR/agents"
|
|
create_dir_if_missing "$PLUGIN_DIR/hooks"
|
|
create_dir_if_missing "$PLUGIN_DIR/bin"
|
|
|
|
# Create Copilot manifest (plugin.json)
|
|
if [[ -f "$COPILOT_MANIFEST" ]]; then
|
|
echo "Skipping '$COPILOT_MANIFEST' — already exists." >&2
|
|
else
|
|
cat > "$COPILOT_MANIFEST" <<COPILOT_JSON
|
|
{
|
|
"name": "$PLUGIN_NAME",
|
|
"description": "FILL_IN_DESCRIPTION",
|
|
"version": "1.0.0",
|
|
"author": { "name": "FILL_IN_AUTHOR_NAME", "email": "FILL_IN_AUTHOR_EMAIL" },
|
|
"license": "MIT",
|
|
"keywords": [],
|
|
"agents": "agents/",
|
|
"skills": ["skills/"],
|
|
"hooks": "hooks.json",
|
|
"mcpServers": ".mcp.json"
|
|
}
|
|
COPILOT_JSON
|
|
echo "Created: $COPILOT_MANIFEST" >&2
|
|
created_any=true
|
|
fi
|
|
|
|
# Create Claude Code manifest (.claude-plugin/plugin.json)
|
|
if [[ -f "$CC_MANIFEST" ]]; then
|
|
echo "Skipping '$CC_MANIFEST' — already exists." >&2
|
|
else
|
|
cat > "$CC_MANIFEST" <<CC_JSON
|
|
{
|
|
"name": "$PLUGIN_NAME",
|
|
"displayName": "FILL_IN_DISPLAY_NAME",
|
|
"description": "FILL_IN_DESCRIPTION",
|
|
"version": "1.0.0",
|
|
"author": { "name": "FILL_IN_AUTHOR_NAME", "url": "FILL_IN_AUTHOR_URL" },
|
|
"license": "MIT",
|
|
"keywords": []
|
|
}
|
|
CC_JSON
|
|
echo "Created: $CC_MANIFEST" >&2
|
|
created_any=true
|
|
fi
|
|
|
|
if [[ "$created_any" == false ]]; then
|
|
echo "All files already exist — nothing to do." >&2
|
|
else
|
|
echo "" >&2
|
|
echo "Plugin: $PLUGIN_NAME" >&2
|
|
echo "Location: $PLUGIN_DIR" >&2
|
|
echo "" >&2
|
|
echo "Next steps:" >&2
|
|
echo " 1. Fill in $COPILOT_MANIFEST — replace all FILL_IN_* placeholders" >&2
|
|
echo " 2. Fill in $CC_MANIFEST — replace all FILL_IN_* placeholders" >&2
|
|
echo " 3. Verify version is identical in both manifests (version parity — ADR-0016)" >&2
|
|
echo " 4. Add plugin content: skills in skills/, agents in agents/, etc." >&2
|
|
fi
|