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
6.3 KiB
Bash
Executable File
195 lines
6.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
SCRIPTS_DIR="$(cd "$(dirname "$0")/../scripts" && pwd)"
|
|
PASS=0; FAIL=0
|
|
# Use += to avoid ((var++)) returning 0 exit code when var was 0 under set -e
|
|
|
|
# ── helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
tmpdir() { mktemp -d; }
|
|
|
|
assert_contains() {
|
|
local label="$1" expected="$2" actual="$3"
|
|
if echo "$actual" | grep -qF "$expected"; then
|
|
echo " PASS: $label"
|
|
PASS=$((PASS+1))
|
|
else
|
|
echo " FAIL: $label"
|
|
echo " expected to contain: $expected"
|
|
echo " got: $(echo "$actual" | head -5)"
|
|
FAIL=$((FAIL+1))
|
|
fi
|
|
}
|
|
|
|
assert_not_contains() {
|
|
local label="$1" unexpected="$2" actual="$3"
|
|
if echo "$actual" | grep -qF "$unexpected"; then
|
|
echo " FAIL: $label"
|
|
echo " expected NOT to contain: $unexpected"
|
|
FAIL=$((FAIL+1))
|
|
else
|
|
echo " PASS: $label"
|
|
PASS=$((PASS+1))
|
|
fi
|
|
}
|
|
|
|
assert_exit() {
|
|
local label="$1" expected="$2" actual="$3"
|
|
if [[ "$actual" -eq "$expected" ]]; then
|
|
echo " PASS: $label"
|
|
PASS=$((PASS+1))
|
|
else
|
|
echo " FAIL: $label"
|
|
echo " expected exit $expected, got $actual"
|
|
FAIL=$((FAIL+1))
|
|
fi
|
|
}
|
|
|
|
assert_file_exists() {
|
|
local label="$1" path="$2"
|
|
if [[ -f "$path" ]]; then
|
|
echo " PASS: $label"
|
|
PASS=$((PASS+1))
|
|
else
|
|
echo " FAIL: $label"
|
|
echo " file not found: $path"
|
|
FAIL=$((FAIL+1))
|
|
fi
|
|
}
|
|
|
|
assert_file_absent() {
|
|
local label="$1" path="$2"
|
|
if [[ ! -e "$path" ]]; then
|
|
echo " PASS: $label"
|
|
PASS=$((PASS+1))
|
|
else
|
|
echo " FAIL: $label"
|
|
echo " file should not exist: $path"
|
|
FAIL=$((FAIL+1))
|
|
fi
|
|
}
|
|
|
|
# ── inventory.sh tests ───────────────────────────────────────────────────────
|
|
|
|
echo "=== inventory.sh ==="
|
|
|
|
# 1. SKILL.md classified as skill
|
|
t=$(tmpdir)
|
|
mkdir -p "$t/skills/my-skill"
|
|
echo "---" > "$t/skills/my-skill/SKILL.md"
|
|
out=$("$SCRIPTS_DIR/inventory.sh" "$t")
|
|
assert_contains "SKILL.md classified as skill" "skill" "$out"
|
|
rm -rf "$t"
|
|
|
|
# 2. agents/foo.agent.md classified as agent-copilot
|
|
t=$(tmpdir)
|
|
mkdir -p "$t/agents"
|
|
touch "$t/agents/my-agent.agent.md"
|
|
out=$("$SCRIPTS_DIR/inventory.sh" "$t")
|
|
assert_contains "agents/*.agent.md classified as agent-copilot" "agent-copilot" "$out"
|
|
rm -rf "$t"
|
|
|
|
# 3. ../ in file content produces cross-reference warning
|
|
t=$(tmpdir)
|
|
mkdir -p "$t/skills/my-skill"
|
|
printf -- '---\ndescription: test\n---\nSee ../shared/file.md\n' > "$t/skills/my-skill/SKILL.md"
|
|
out=$("$SCRIPTS_DIR/inventory.sh" "$t")
|
|
assert_contains "../ cross-reference warning emitted" "Cross-reference" "$out"
|
|
assert_contains "../ path shown in warning" "../shared/file.md" "$out"
|
|
rm -rf "$t"
|
|
|
|
# ── gen_manifests.sh tests ───────────────────────────────────────────────────
|
|
|
|
echo ""
|
|
echo "=== gen_manifests.sh ==="
|
|
|
|
# 4. Without --write, no files are created
|
|
t=$(tmpdir)
|
|
mkdir -p "$t/plugins/my-plugin/skills/hello"
|
|
echo "---" > "$t/plugins/my-plugin/skills/hello/SKILL.md"
|
|
"$SCRIPTS_DIR/gen_manifests.sh" "$t" --marketplace-name my-marketplace >/dev/null
|
|
assert_file_absent "dry run: .claude-plugin/marketplace.json not written" "$t/.claude-plugin/marketplace.json"
|
|
assert_file_absent "dry run: plugin.json not written" "$t/plugins/my-plugin/plugin.json"
|
|
rm -rf "$t"
|
|
|
|
# 5. With --write, creates plugin.json in both locations
|
|
t=$(tmpdir)
|
|
mkdir -p "$t/plugins/my-plugin/skills/hello"
|
|
echo "---" > "$t/plugins/my-plugin/skills/hello/SKILL.md"
|
|
"$SCRIPTS_DIR/gen_manifests.sh" "$t" --marketplace-name my-marketplace --write >/dev/null
|
|
assert_file_exists "--write: plugin root plugin.json created" "$t/plugins/my-plugin/plugin.json"
|
|
assert_file_exists "--write: .claude-plugin/plugin.json created" "$t/plugins/my-plugin/.claude-plugin/plugin.json"
|
|
rm -rf "$t"
|
|
|
|
# 6. Reserved name exits 1
|
|
t=$(tmpdir)
|
|
mkdir -p "$t/plugins/my-plugin"
|
|
set +e
|
|
out=$("$SCRIPTS_DIR/gen_manifests.sh" "$t" --marketplace-name claude-plugins-official 2>&1)
|
|
code=$?
|
|
set -e
|
|
assert_exit "reserved name: exit 1" 1 "$code"
|
|
assert_contains "reserved name: error message" "reserved" "$out"
|
|
rm -rf "$t"
|
|
|
|
# ── validate.sh tests ────────────────────────────────────────────────────────
|
|
|
|
echo ""
|
|
echo "=== validate.sh ==="
|
|
|
|
# 7. Invalid JSON exits 1
|
|
t=$(tmpdir)
|
|
mkdir -p "$t/.claude-plugin"
|
|
echo "not json" > "$t/.claude-plugin/marketplace.json"
|
|
set +e
|
|
out=$("$SCRIPTS_DIR/validate.sh" "$t" 2>&1)
|
|
code=$?
|
|
set -e
|
|
assert_exit "invalid JSON: exit 1" 1 "$code"
|
|
assert_contains "invalid JSON: error message" "ERROR" "$out"
|
|
rm -rf "$t"
|
|
|
|
# 8. Reserved marketplace name exits 1
|
|
t=$(tmpdir)
|
|
mkdir -p "$t/.claude-plugin"
|
|
printf '{"name":"claude-plugins-official","plugins":[]}\n' > "$t/.claude-plugin/marketplace.json"
|
|
set +e
|
|
out=$("$SCRIPTS_DIR/validate.sh" "$t" 2>&1)
|
|
code=$?
|
|
set -e
|
|
assert_exit "reserved marketplace name: exit 1" 1 "$code"
|
|
assert_contains "reserved marketplace name: error message" "reserved" "$out"
|
|
rm -rf "$t"
|
|
|
|
# 9. ../ in plugin file exits 1
|
|
t=$(tmpdir)
|
|
mkdir -p "$t/.claude-plugin" "$t/plugins/my-plugin/skills/hello"
|
|
printf '{"name":"my-marketplace","plugins":[{"name":"my-plugin","source":"./plugins/my-plugin"}]}\n' \
|
|
> "$t/.claude-plugin/marketplace.json"
|
|
printf -- '---\ndescription: test\n---\nSee ../shared.md\n' \
|
|
> "$t/plugins/my-plugin/skills/hello/SKILL.md"
|
|
set +e
|
|
out=$("$SCRIPTS_DIR/validate.sh" "$t" 2>&1)
|
|
code=$?
|
|
set -e
|
|
assert_exit "..// in plugin: exit 1" 1 "$code"
|
|
assert_contains "..// in plugin: error message" "../" "$out"
|
|
rm -rf "$t"
|
|
|
|
# 10. No marketplace.json warns but exits 0
|
|
t=$(tmpdir)
|
|
set +e
|
|
out=$("$SCRIPTS_DIR/validate.sh" "$t" 2>&1)
|
|
code=$?
|
|
set -e
|
|
assert_exit "no marketplace.json: exit 0" 0 "$code"
|
|
assert_contains "no marketplace.json: warning emitted" "WARN" "$out"
|
|
rm -rf "$t"
|
|
|
|
# ── summary ──────────────────────────────────────────────────────────────────
|
|
|
|
echo ""
|
|
echo "Results: $PASS passed, $FAIL failed"
|
|
[[ $FAIL -eq 0 ]]
|