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:
2026-06-20 13:09:16 +00:00
parent 25a6a454b9
commit 36ca3744aa
12 changed files with 1683 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
```yaml
version: "1.0"
updated: 2026-06-20
when: >
Invoked when the user wants to create, manage, or maintain a plugin marketplace for
Claude Code and/or GitHub Copilot CLI. Covers four operations: (a) audit and refactor
a repository of skills/agents/hooks into a plugin marketplace layout, (b) adopt external
plugins/skills/agents from outside sources, (c) update and maintain an existing
marketplace.json and plugin manifests, (d) validate existing plugin manifests for naming,
structure, and cross-tool compatibility. Also triggered implicitly when the user asks
about distributing skills to a team, organizing loose skills into installable units, or
setting up cross-tool distribution — even without saying "marketplace".
references:
- https://code.claude.com/docs/en/plugins
- https://code.claude.com/docs/en/plugin-marketplaces
- https://code.claude.com/docs/en/plugins-reference
- https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-creating
- https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-marketplace
```

View File

@@ -0,0 +1,101 @@
---
name: marketplace-architect
description: >
Manages and maintains a plugin marketplace for Claude Code and GitHub Copilot CLI.
Use this whenever the user wants to: create or update a marketplace (marketplace.json,
plugin.json manifests), adopt plugins/skills/agents/hooks from external sources, evaluate
cross-tool compatibility between Claude Code and Copilot CLI, plan plugin groupings and
boundaries, refactor a repository into marketplace format, validate plugin naming, detect
duplicate capabilities, or generate per-plugin install docs — even if they don't use the
word "marketplace". Do NOT use when the user wants to author a new skill from scratch
(use write-skill), debug an existing skill (use diagnose), or run a direct plugin CLI
command (copilot plugin install, claude plugin list).
metadata:
category: marketplace
---
<requirements>
## Required inputs
- **Target operation** — what the user wants to do; inferred from request. If ambiguous, ask: audit/refactor, adopt an external plugin, update/maintain an existing marketplace, or validate manifests.
- **Repository path** — path to the repo to act on; defaults to current working directory if not stated.
- **Marketplace name** — kebab-case identifier (e.g. `my-ai-marketplace`); required only when generating a new `marketplace.json`. Infer from repo name if obvious, ask if not.
- **Plugin source** — URL, GitHub slug, or local path; required only when adopting an external plugin.
## Constraints
- Load `references/cross-compat.md` before any tool-specific decision — Claude Code and Copilot CLI diverge in ways that cause silent breakage at install time.
- Never write files until the user has approved the plan at Gate A and the specific file contents at Gate B — two separate explicit approvals required.
- If credential-shaped content is detected in any manifest field, halt and redirect to environment variable references (e.g. `$MY_TOKEN`) — do not generate the manifest.
- Produce cross-tool deltas and per-plugin READMEs only when explicitly requested — do not generate them automatically.
- Scripts in `scripts/` are loaded on demand by the step that needs them — never preloaded.
- Flag any `../` cross-references in the audited repo before recommending plugin boundaries — plugins cannot reference files outside their own directory after install-time caching.
- Plugin names must be kebab-case; validate against the reserved name list in `references/claude-code.md` before generating any manifest.
- Do not set `version` in both `plugin.json` and the marketplace entry — `plugin.json` wins silently and causes update failures.
</requirements>
<steps>
## Process
1. **Identify the operation.** Determine intent from the user's request — one of: (a) audit/refactor a repo into marketplace format, (b) adopt an external plugin/skill/agent, (c) maintain or update an existing marketplace, (d) validate existing manifests. Ask if the operation cannot be inferred.
2. **Load the compatibility reference.** Read `references/cross-compat.md` before any tool-specific decision. Claude Code and Copilot CLI diverge in manifest paths, agent file naming, and hooks layout — every recommendation depends on this table.
3. **Execute the operation phase.**
**(a) Audit/refactor:** Run `scripts/inventory.sh` against the repo to classify every asset (skill / command / agent / hook / prompt / MCP). Flag any `../` cross-references — these break under install-time caching. Recommend plugin groupings by user outcome (~10–20 plugins); warn if proposed count exceeds 20 or falls below 3. Diff skill descriptions for duplicate capabilities before finalising boundaries. Produce a concrete migration checklist: old path → new path, one row per file.
**(b) Adopt external plugin:** Fetch and inspect the plugin source. Classify included assets. Check for naming conflicts with existing plugins in the marketplace. Evaluate cross-tool compatibility using `references/cross-compat.md`. Summarise what will be added to `marketplace.json`.
**(c) Maintain/update:** Read current `marketplace.json` and all `plugin.json` files. Identify stale versions, reserved name violations, kebab-case violations, and `version` duplication between plugin.json and marketplace entry. Report findings as a prioritised fix list.
**(d) Validate:** Run `scripts/validate.sh` (wraps `claude plugin validate` plus custom JSON and naming checks). Report each violation with a recommended fix. Do not proceed to file writes until all errors are resolved.
4. **Gate A — plan review.** Present the full plan or fix list to the user. Wait for explicit approval before proceeding. Do not interpret silence or "looks good" as approval — require a direct "yes" or equivalent.
5. **Generate outputs.** After Gate A approval: for audit/refactor and adopt operations, run `scripts/gen_manifests.sh` to produce `plugin.json` (at both `.claude-plugin/plugin.json` and plugin root until the Copilot fallback is verified) and `marketplace.json` (at `.claude-plugin/marketplace.json`; optionally mirror to `.github/plugin/marketplace.json`). Read `references/claude-code.md` for Claude-specific path rules and `references/copilot-cli.md` for Copilot-specific requirements.
6. **Gate B — file write approval.** Show the user every file that will be written with its full contents. Wait for explicit approval per file or as a batch. Write nothing until approved.
7. **Validate post-write.** After writes complete, run `scripts/validate.sh` again. Report any remaining issues. Suggest local install test commands: `claude --plugin-dir ./plugins/<name>` and `copilot plugin install ./plugins/<name>`.
8. **Optional deliverables.** Only when the user explicitly asks: emit cross-tool delta notes (what each plugin needs for Copilot vs Claude Code) and per-plugin README with install commands for both tools.
## Output format
Files generated depend on operation:
- **Audit/refactor and adopt:** `plugin.json` (two locations per plugin until verified), `marketplace.json` (`.claude-plugin/`, optionally `.github/plugin/`), migration checklist as a markdown table
- **Maintain/update:** updated `marketplace.json` and affected `plugin.json` files
- **Validate:** report only — no file writes unless explicitly requested after review
- **Optional:** per-plugin `README.md` with both `claude` and `copilot` install commands
</steps>
<checks>
## Failure handling
- `scripts/inventory.sh` not found or fails — perform manual asset classification using Read and Bash find; note the fallback in output.
- `scripts/gen_manifests.sh` not found or fails — generate manifest JSON inline; flag that the output was not script-produced.
- `scripts/validate.sh` not found or `claude plugin validate` unavailable — run manual JSON schema and naming checks using `references/claude-code.md`; flag that automated validation was skipped.
- Plugin source unreachable (bad URL, private repo, missing path) — stop the adopt operation, report the error, ask the user to verify the source before retrying.
- Reserved name detected in proposed plugin or marketplace name — halt, report the name and the reserved list from `references/claude-code.md`, ask for a replacement before proceeding.
- Credential-shaped content detected in any manifest field — halt, do not generate the manifest, redirect to environment variable references.
## Self-check
- [ ] `references/cross-compat.md` loaded before any tool-specific recommendation was made
- [ ] Operation identified before any scanning or file reading began
- [ ] Gate A presented and explicit approval received before any manifest was generated
- [ ] Gate B presented with full file contents and explicit approval received before any file was written
- [ ] No credential-shaped content in any generated manifest field
- [ ] All plugin names validated as kebab-case and checked against reserved name list
- [ ] `version` field not set in both `plugin.json` and marketplace entry for the same plugin
- [ ] Scripts loaded on demand by step — not preloaded at skill invocation
- [ ] Cross-tool deltas and READMEs produced only if explicitly requested
- [ ] Post-write validation run and findings reported
</checks>

View File

@@ -0,0 +1,169 @@
# Claude Code Plugin Reference
Verified against code.claude.com/docs as of June 2026.
---
## Directory structure
```text
plugin-root/
├── .claude-plugin/
│ └── plugin.json # ONLY plugin.json goes here; all other dirs at plugin root
├── skills/ # skill directories: <name>/SKILL.md
├── commands/ # legacy flat .md files; promote to skills/ for new plugins
├── agents/ # agent definitions: <name>.md
├── hooks/
│ └── hooks.json
├── .mcp.json
├── .lsp.json
├── monitors/
│ └── monitors.json
├── bin/ # executables added to PATH while plugin is enabled
└── settings.json # default settings applied when plugin is enabled
```
A plugin that ships exactly one skill may place `SKILL.md` directly at the plugin root.
Use `skills/` for plugins that may grow beyond one skill.
---
## plugin.json schema
```json
{
"name": "my-plugin", // kebab-case, no spaces — also the skill namespace prefix
"displayName": "My Plugin", // human-readable; shown in UI (v2.1.143+)
"description": "What it does",
"version": "1.0.0", // OPTIONAL — omit to use git SHA per commit
"author": { "name": "Name", "url": "https://..." },
"homepage": "https://...",
"repository": "https://github.com/...",
"license": "MIT",
"keywords": [],
"defaultEnabled": true, // set false to install disabled (v2.1.154+)
"dependencies": [
{ "name": "other-plugin", "version": "~2.1.0" }
]
}
```
Only `name` is required. Add fields only when needed.
---
## marketplace.json schema
```json
{
"name": "my-ai-marketplace",
"owner": { "name": "Your Name", "email": "you@example.com" },
"description": "Description",
"version": "1.0.0",
"plugins": [
{
"name": "startup-cto",
"source": "./plugins/startup-cto",
"description": "...",
"strict": true
}
]
}
```
`description` and `version` are also accepted under a `metadata` key for backward compatibility.
---
## Plugin source types
| Type | Format | Notes |
|---|---|---|
| Relative path | `"./plugins/my-plugin"` | Must start with `./`. Resolved from marketplace root. Only works with git-hosted marketplaces, not URL-based. |
| GitHub | `{ "source": "github", "repo": "owner/repo", "ref": "main", "sha": "abc123" }` | sha pins exact commit; ref is branch/tag |
| URL / git | `{ "source": "url", "url": "https://...", "ref": "main" }` | also accepts `owner/repo` shorthand and SSH URLs |
| git-subdir | `{ "source": "git-subdir", "url": "...", "path": "packages/my-plugin" }` | sparse clone of a monorepo path |
| npm | `{ "source": "npm", "package": "@scope/pkg", "version": "^2.0.0", "registry": "https://..." }` | installed via npm install |
When both `ref` and `sha` are set, `sha` is the effective pin.
---
## Strict mode
Controls whether `plugin.json` is the authority for component definitions.
- **`strict: true`** (default) — plugin has its own `plugin.json`; marketplace entry can add extra skills/hooks on top.
- **`strict: false`** — marketplace entry is the entire definition; plugin needs no `plugin.json`. The entry declares `skills`, `agents`, `hooks`, `mcpServers` path arrays.
Do not use `strict: false` plus a component-declaring `plugin.json` — this is a conflict and fails to load.
---
## Reserved marketplace names
These names are blocked for third-party use:
`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`
Names that impersonate official marketplaces are also blocked (e.g. `official-claude-plugins`,
`anthropic-tools-v2`).
Plugin names must be kebab-case (lowercase, digits, hyphens). Claude.ai marketplace sync
rejects anything else even if the local CLI tolerates it.
---
## Version management
- If `version` is set in `plugin.json`, users receive updates only when you bump it.
- If `version` is omitted, git commit SHA is used — every commit is a new version.
- If `version` is set in both `plugin.json` and the marketplace entry, `plugin.json` wins silently.
- **Recommendation:** omit `version` unless you need explicit release gates.
---
## Environment variables
- **`${CLAUDE_PLUGIN_ROOT}`** — absolute path to the plugin's installation directory. Use in hook commands and MCP/LSP configs for all in-plugin file references. This path changes on update.
- **`${CLAUDE_PLUGIN_DATA}`** — persistent directory for plugin state that survives updates. Use for `node_modules`, generated code, caches.
---
## Validation and CLI commands
```bash
# Validate plugin structure and manifest
claude plugin validate ./my-plugin
claude plugin validate ./my-plugin --strict # treat warnings as errors
# Install/manage
claude plugin install <name>@<marketplace>
claude plugin update <name>@<marketplace>
claude plugin uninstall <name>
claude plugin list
claude plugin enable <name>
claude plugin disable <name>
# Marketplace
claude plugin marketplace add owner/repo
claude plugin marketplace update
# Development
claude --plugin-dir ./my-plugin # load without installing
claude --plugin-dir ./my-plugin.zip # load from zip (v2.1.128+)
claude plugin init my-tool # scaffold a skills-dir plugin
```
---
## Key gotchas
1. **Plugins are copied to cache on install.** Cannot reference `../shared-utils` — those files are not copied. Duplicate shared files into each plugin or use symlinks.
2. **`commands/` ≠ `skills/`.** Flat `foo.md` is a legacy command; `foo/SKILL.md` is a skill. Promote flat commands to skill directories during migration.
3. **Only `plugin.json` in `.claude-plugin/`.** Skills, agents, hooks, and other directories must be at the plugin root, not inside `.claude-plugin/`.
4. **Plugin names are skill namespace prefixes.** `name: my-plugin` means skills invoke as `/my-plugin:skill-name`.
5. **`defaultEnabled: false` requires v2.1.154+.** Earlier versions ignore it and enable on install.

View File

@@ -0,0 +1,143 @@
# GitHub Copilot CLI Plugin Reference
Verified against docs.github.com as of June 2026.
---
## Directory structure
```text
plugin-root/
├── plugin.json # at plugin root (NOT in .claude-plugin/)
├── skills/ # skill directories: <name>/SKILL.md (same as Claude Code)
├── agents/ # agent files: <name>.agent.md (differs from Claude Code)
├── hooks.json # at plugin root (differs from Claude Code: hooks/hooks.json)
└── .mcp.json # at plugin root (same as Claude Code)
```
---
## plugin.json schema (Copilot)
```json
{
"name": "my-plugin",
"description": "What it does",
"version": "1.0.0",
"author": { "name": "Name", "email": "you@example.com" },
"license": "MIT",
"keywords": [],
"agents": "agents/",
"skills": ["skills/"],
"hooks": "hooks.json",
"mcpServers": ".mcp.json"
}
```
Key difference from Claude Code: Copilot expects component path declarations inside
`plugin.json` (`"skills": "skills/"`, `"agents": "agents/"`, etc.). Claude Code instead
defaults to standard dirs and takes path overrides only via the marketplace entry.
This means the same `plugin.json` may need these fields for Copilot but not for Claude.
---
## marketplace.json schema (Copilot)
```json
{
"name": "my-ai-marketplace",
"owner": { "name": "Your Name", "email": "you@example.com" },
"metadata": { "description": "Agents, skills and workflows", "version": "1.0.0" },
"plugins": [
{
"name": "startup-cto",
"source": "./plugins/startup-cto",
"description": "...",
"version": "1.0.0"
}
]
}
```
Copilot's primary marketplace manifest path is `.github/plugin/marketplace.json`.
It also reads `.claude-plugin/marketplace.json` as a fallback.
Relative `source` paths: `./x` and `x` are both valid (Claude requires `./`).
---
## Agent file format
Copilot agents use `.agent.md` extension with frontmatter:
```markdown
---
name: my-agent
description: What this agent does
tools:
- read_file
- run_command
---
Agent instructions here.
```
Claude Code agents use `.md` extension without the `.agent.md` suffix.
If shipping agents for both tools, create both files:
- `agents/my-agent.md` — Claude Code
- `agents/my-agent.agent.md` — Copilot CLI
---
## CLI commands
```bash
# Install plugin locally (development)
copilot plugin install ./my-plugin
# List installed plugins
copilot plugin list
# In interactive mode
/plugin list
/skills list
/agent
# Reload after changes
/reload-plugins
# Uninstall (uses bare plugin name, not @marketplace form)
copilot plugin uninstall <name>
# Marketplace
copilot plugin marketplace add owner/repo
```
> ⚠️ **UNVERIFIED: Copilot marketplace install command.**
> The `update`/`uninstall` commands take a bare `<name>`. Whether install from a marketplace
> uses `<name>@<marketplace>` (Claude Code's form) or a bare `<name>` is not confirmed in docs.
> Run `copilot plugin install --help` before documenting the install command anywhere.
---
## Validation
Copilot has no documented `plugin validate` command. For Copilot-side validation, run manual checks:
- Valid JSON in `plugin.json` and `marketplace.json`
- Required fields: `name`, `description`
- Unique plugin names across marketplace
- Kebab-case plugin names
- All `source` paths resolve to existing directories
- `.agent.md` files have valid YAML frontmatter with `name`, `description`, `tools`
---
## Key differences from Claude Code (summary)
| What | Claude Code | Copilot CLI |
|---|---|---|
| Plugin manifest location | `.claude-plugin/plugin.json` | `plugin.json` at plugin root |
| Agent files | `agents/<name>.md` | `agents/<name>.agent.md` |
| Hooks file | `hooks/hooks.json` | `hooks.json` at plugin root |
| Component paths | Declared in marketplace entry | Declared in `plugin.json` |
| Validate command | `claude plugin validate` | None — manual checks only |
| Relative source `./` | Required | Optional (`x` also valid) |

View File

@@ -0,0 +1,79 @@
# Cross-Tool Compatibility Reference
Claude Code and GitHub Copilot CLI share the plugin concept but diverge in specific, breaking
ways. **Skills are the portable core. Manifests and agents are where they split.**
Make Claude Code the source of truth — it is the stricter, more fully specified format.
Treat "loads in Copilot CLI" as a tested checklist item per plugin, not an assumption.
---
## Divergence table
| Concern | Claude Code | GitHub Copilot CLI | Portable choice |
|---|---|---|---|
| Marketplace manifest path | `.claude-plugin/marketplace.json` (required) | `.github/plugin/marketplace.json` (primary); also reads `.claude-plugin/` | Put it in `.claude-plugin/` — both read it. Optionally mirror to `.github/plugin/`. |
| Plugin manifest path | `.claude-plugin/plugin.json` (required; only `plugin.json` goes in this dir) | `plugin.json` at **plugin root** | Ship in both locations until verified — see ⚠️ below |
| Skills | `skills/<name>/SKILL.md` | `skills/<name>/SKILL.md` | ✅ Identical |
| Agents | `agents/<name>.md` | `agents/<name>.agent.md` (frontmatter incl. `tools:`) | Diverges — keep portable logic in skills; ship per-tool agent files only when needed |
| Hooks | `hooks/hooks.json` | `hooks.json` at plugin root | Diverges; declare paths in manifest to be safe |
| MCP servers | `.mcp.json` at plugin root | `.mcp.json` at plugin root | ✅ Same |
| Relative `source` | must start with `./` | `./x` and `x` both valid | Always use `./` — valid for both |
| Validate command | `claude plugin validate .` (or `/plugin validate .`) | none documented | Run Claude validator + manual JSON checks for Copilot |
| Install marketplace | `claude plugin marketplace add owner/repo` | `copilot plugin marketplace add owner/repo` | Same shape |
| Install plugin | `claude plugin install <name>@<marketplace-name>` | install by plugin name; `@marketplace` suffix unconfirmed | ⚠️ Verify Copilot install string before documenting |
| Local install (dev) | `claude --plugin-dir ./plugin` | `copilot plugin install ./plugin` | Tool-specific |
| Component paths in plugin.json | Claude defaults to standard dirs; path overrides via marketplace entry only | `"skills": "skills/"`, `"agents": "agents/"`, etc. in plugin.json | Generate per-tool manifests rather than one shared file |
> ⚠️ **UNVERIFIED — test before committing to a layout.**
> Copilot docs confirm it reads the **marketplace** manifest from `.claude-plugin/`. They do NOT
> confirm the same fallback for a plugin's `plugin.json`. Copilot docs show `plugin.json` at plugin
> root; Claude requires it in `.claude-plugin/`. Until verified: ship `plugin.json` in BOTH
> `plugin-name/plugin.json` and `plugin-name/.claude-plugin/plugin.json` (identical content), then
> drop whichever proves redundant.
---
## `@<marketplace-name>` resolution
`claude plugin install startup-cto@my-ai-marketplace` requires the marketplace manifest's
top-level `name` field to be exactly `my-ai-marketplace`. It is **not** the GitHub repo name.
Keep them aligned to avoid confusion, but they are separate fields.
---
## Canonical cross-compatible repo layout
```text
repo-root/
├── .claude-plugin/
│ └── marketplace.json # both tools read here
├── .github/plugin/
│ └── marketplace.json # OPTIONAL: Copilot canonical path (mirror)
├── plugins/
│ └── startup-cto/
│ ├── plugin.json # Copilot root manifest ┐ ship both until
│ ├── .claude-plugin/ # │ the note above is
│ │ └── plugin.json # Claude manifest ┘ verified
│ ├── skills/
│ │ ├── fundraising/SKILL.md
│ │ └── hiring/SKILL.md
│ ├── agents/
│ │ ├── startup-cto.md # Claude
│ │ └── startup-cto.agent.md # Copilot (only if shipping native agents)
│ ├── hooks/hooks.json # Claude
│ ├── hooks.json # Copilot (if hooks used)
│ └── README.md
└── README.md
```
---
## Open questions to resolve before generating layouts
1. **Does Copilot CLI load a plugin whose `plugin.json` lives only in `.claude-plugin/`?**
Install a test plugin both ways. The answer decides whether to ship one manifest or two.
2. **What is Copilot's exact install-from-marketplace command?**
Run `copilot plugin install --help`. The `update`/`uninstall` commands take a bare plugin
name — the `@marketplace` form may not apply.

View 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

View File

@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# Scan a repository and classify every asset as skill/command/agent/hook/prompt/MCP.
# Outputs a markdown table of findings plus a list of cross-reference warnings.
#
# Usage: inventory.sh <repo-path>
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "Usage: inventory.sh <repo-path>" >&2
exit 1
fi
ROOT="$(cd "$1" && pwd)"
if [[ ! -d "$ROOT" ]]; then
echo "Error: $ROOT is not a directory" >&2
exit 1
fi
# ── classify assets ──────────────────────────────────────────────────────────
declare -a ROWS=()
declare -a CROSS_REFS=()
while IFS= read -r -d '' path; do
rel="${path#"$ROOT/"}"
name="$(basename "$path")"
dir="$(dirname "$rel")"
parent="$(basename "$dir")"
# Skip hidden dirs except .claude-plugin and .github
skip=false
IFS='/' read -ra parts <<< "$dir"
for part in "${parts[@]}"; do
if [[ "$part" == .* && "$part" != ".claude-plugin" && "$part" != ".github" ]]; then
skip=true; break
fi
done
$skip && continue
asset_type=""
case "$name" in
SKILL.md) asset_type="skill" ;;
hooks.json) asset_type="hook" ;;
.mcp.json) asset_type="mcp" ;;
.lsp.json) asset_type="lsp" ;;
plugin.json) asset_type="manifest-plugin" ;;
marketplace.json) asset_type="manifest-marketplace" ;;
*.agent.md) asset_type="agent-copilot" ;;
*.md)
if [[ "$parent" == "agents" ]]; then
asset_type="agent-claude"
elif [[ "$parent" == "commands" ]]; then
asset_type="command"
elif [[ "$rel" != *"/skills/"* && "$rel" != *"/commands/"* && "$rel" != *"/agents/"* ]]; then
asset_type="prompt"
fi
;;
esac
[[ -n "$asset_type" ]] && ROWS+=("$asset_type|$rel")
# Check for cross-references in text files
case "$name" in *.md|*.json|*.sh)
if grep -q '\.\.\/' "$path" 2>/dev/null; then
while IFS= read -r line; do
lineno="${line%%:*}"
content="${line#*:}"
CROSS_REFS+=("$rel:$lineno: $content")
done < <(grep -n '\.\.\/' "$path" 2>/dev/null | head -20)
fi
;;
esac
done < <(find "$ROOT" -type f -print0 | sort -z)
# ── report ───────────────────────────────────────────────────────────────────
echo "# Asset Inventory: $ROOT"
echo ""
echo "## Assets"
echo ""
echo "| Type | Path |"
echo "|---|---|"
for row in "${ROWS[@]+"${ROWS[@]}"}"; do
type="${row%%|*}"
path="${row#*|}"
echo "| \`$type\` | \`$path\` |"
done | sort
total="${#ROWS[@]}"
echo ""
echo "**Total: $total assets**"
echo ""
# Summary by type
echo "## Summary by type"
echo ""
for row in "${ROWS[@]+"${ROWS[@]}"}"; do
echo "${row%%|*}"
done | sort | uniq -c | while read -r count type; do
echo "- \`$type\`: $count"
done
# Cross-reference warnings
echo ""
if [[ ${#CROSS_REFS[@]} -gt 0 ]]; then
echo "## ⚠️ Cross-reference warnings (${#CROSS_REFS[@]} found)"
echo ""
echo "These \`../\` references will break after install-time caching:"
echo ""
for ref in "${CROSS_REFS[@]}"; do
echo "- \`$ref\`"
done
else
echo "## Cross-references"
echo ""
echo "No \`../\` cross-references found. Safe to proceed with plugin boundaries."
fi

View File

@@ -0,0 +1,264 @@
#!/usr/bin/env bash
# Validate plugin marketplace manifests for Claude Code and GitHub Copilot CLI.
# Wraps `claude plugin validate` (Claude-side) and runs manual checks (Copilot-side).
#
# Usage:
# validate.sh <repo-root>
# validate.sh <repo-root> --plugin plugins/my-plugin
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"
ERRORS=0
WARNINGS=0
error() { echo "ERROR: $1"; ((ERRORS++)) || true; }
warn() { echo "WARN: $1"; ((WARNINGS++)) || true; }
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"
[[ -z "$name" ]] && { error "$context: name is missing or empty"; return 0; }
is_kebab_case "$name" || error "$context: name '$name' is not kebab-case"
if is_reserved "$name"; then
error "$context: name '$name' is reserved for official Anthropic use"
fi
return 0
}
valid_json() {
local path="$1"
if ! jq -e . "$path" >/dev/null 2>&1; then
error "Invalid JSON in $path"
return 1
fi
return 0
}
validate_marketplace_json() {
local path="$1"
[[ -f "$path" ]] || return 0
valid_json "$path" || return 0
local name
name=$(jq -r '.name // empty' "$path")
[[ -z "$name" ]] && error "$path: 'name' field is required" || validate_name "$name" "$path"
local plugins_type
plugins_type=$(jq -r 'if .plugins | type == "array" then "ok" else "bad" end' "$path")
if [[ "$plugins_type" != "ok" ]]; then
error "$path: 'plugins' must be an array"
return 0
fi
# Check each plugin entry
local seen_names=()
while IFS= read -r pname; do
# Duplicate check
for seen in "${seen_names[@]+"${seen_names[@]}"}"; do
if [[ "$seen" == "$pname" ]]; then error "$path: duplicate plugin name '$pname'"; fi
done
seen_names+=("$pname")
validate_name "$pname" "$path plugin '$pname'"
# Source path check
local src
src=$(jq -r --arg n "$pname" '.plugins[] | select(.name==$n) | .source // empty' "$path")
if [[ -n "$src" && "$src" != ./* && "$src" != "github" && "$src" != "npm" && "$src" != "url" && "$src" != "git-subdir" ]]; then
warn "$path plugin '$pname': relative source '$src' should start with './' for Claude Code compatibility"
fi
# Version duplication warning
local has_ver
has_ver=$(jq -r --arg n "$pname" '.plugins[] | select(.name==$n) | .version // empty' "$path")
if [[ -n "$has_ver" ]]; then
warn "$path plugin '$pname': version set in marketplace entry. If also set in plugin.json, plugin.json wins silently."
fi
done < <(jq -r '.plugins[].name // empty' "$path")
return 0
}
validate_plugin_json() {
local path="$1" marketplace_json="${2:-}"
[[ -f "$path" ]] || return 0
valid_json "$path" || return 0
local name
name=$(jq -r '.name // empty' "$path")
if [[ -z "$name" ]]; then
warn "$path: 'name' field missing (plugin dir name will be used)"
else
validate_name "$name" "$path"
# Version duplication check
if [[ -n "$marketplace_json" && -f "$marketplace_json" ]]; then
local pver mver
pver=$(jq -r '.version // empty' "$path")
mver=$(jq -r --arg n "$name" '.plugins[]? | select(.name==$n) | .version // empty' "$marketplace_json")
if [[ -n "$pver" && -n "$mver" ]]; then
error "$path: version '$pver' set in both plugin.json and marketplace entry — plugin.json wins silently. Remove one."
fi
fi
fi
return 0
}
validate_skill_md() {
local path="$1"
local content
content=$(cat "$path")
if [[ "$content" != ---* ]]; then
warn "$path: SKILL.md has no YAML frontmatter"
return
fi
if ! echo "$content" | awk 'NR>1 && /^---/' | grep -q '^---'; then
error "$path: SKILL.md frontmatter not closed"
return
fi
if ! echo "$content" | awk '/^---/{n++; if(n==2) exit} n==1' | grep -q 'description:'; then
warn "$path: SKILL.md frontmatter missing 'description' field"
fi
}
validate_plugin_dir() {
local pd="$1" marketplace_json="${2:-}"
local claude_manifest="$pd/.claude-plugin/plugin.json"
local root_manifest="$pd/plugin.json"
if [[ ! -f "$claude_manifest" && ! -f "$root_manifest" ]]; then
warn "$pd: no plugin.json found (will auto-discover components)"
else
validate_plugin_json "$claude_manifest" "$marketplace_json"
validate_plugin_json "$root_manifest" "$marketplace_json"
# Sync check
if [[ -f "$claude_manifest" && -f "$root_manifest" ]]; then
if ! diff <(jq -S . "$claude_manifest") <(jq -S . "$root_manifest") >/dev/null 2>&1; then
warn "$pd: .claude-plugin/plugin.json and plugin.json differ — keep them in sync"
fi
fi
fi
# Components must not be inside .claude-plugin/
for bad_dir in skills agents hooks commands; do
if [[ -d "$pd/.claude-plugin/$bad_dir" ]]; then
error "$pd/.claude-plugin/$bad_dir: only plugin.json belongs in .claude-plugin/; move $bad_dir/ to plugin root"
fi
done
# Validate SKILL.md files
while IFS= read -r -d '' skill_md; do
validate_skill_md "$skill_md"
done < <(find "$pd" -name "SKILL.md" -print0 2>/dev/null)
# Cross-reference check
while IFS= read -r -d '' f; do
if grep -q '\.\.\/' "$f" 2>/dev/null; then
local rel="${f#"$pd/"}"
error "$rel: contains '../' reference — plugins cannot access files outside their directory after caching"
fi
done < <(find "$pd" \( -name "*.md" -o -name "*.json" \) -print0 2>/dev/null)
}
run_claude_validate() {
local path="$1"
if command -v claude >/dev/null 2>&1; then
if ! claude plugin validate "$path" 2>&1; then
error "claude plugin validate failed for $path"
fi
else
warn "'claude' CLI not found — skipping claude plugin validate"
fi
}
# ── parse args ────────────────────────────────────────────────────────────────
ROOT=""
PLUGIN_ONLY=""
while [[ $# -gt 0 ]]; do
case "$1" in
--plugin) PLUGIN_ONLY="$2"; shift 2 ;;
-*) echo "Unknown option: $1" >&2; exit 1 ;;
*) ROOT="$1"; shift ;;
esac
done
if [[ -z "$ROOT" ]]; then
echo "Usage: validate.sh <repo-root> [--plugin <path>]" >&2
exit 1
fi
ROOT="$(cd "$ROOT" && pwd)"
# ── validate marketplace.json ─────────────────────────────────────────────────
MARKETPLACE_JSON=""
for mp in "$ROOT/.claude-plugin/marketplace.json" "$ROOT/.github/plugin/marketplace.json"; do
if [[ -f "$mp" ]]; then
MARKETPLACE_JSON="$mp"
validate_marketplace_json "$mp"
break
fi
done
if [[ -z "$MARKETPLACE_JSON" ]]; then
warn "No marketplace.json found. Expected at .claude-plugin/marketplace.json"
fi
# ── validate plugins ──────────────────────────────────────────────────────────
if [[ -n "$PLUGIN_ONLY" ]]; then
validate_plugin_dir "$(cd "$PLUGIN_ONLY" && pwd)" "$MARKETPLACE_JSON"
run_claude_validate "$(cd "$PLUGIN_ONLY" && pwd)"
else
plugins_path="$ROOT/plugins"
if [[ -d "$plugins_path" ]]; then
while IFS= read -r -d '' pd; do
validate_plugin_dir "$pd" "$MARKETPLACE_JSON"
run_claude_validate "$pd"
done < <(find "$plugins_path" -mindepth 1 -maxdepth 1 -type d ! -name '.*' -print0 | sort -z)
else
warn "No plugins/ directory found at $ROOT"
fi
fi
# ── check source paths resolve ────────────────────────────────────────────────
if [[ -n "$MARKETPLACE_JSON" ]]; then
while IFS= read -r src; do
[[ "$src" != ./* ]] && continue
src_path="$ROOT/${src#./}"
if [[ ! -d "$src_path" ]]; then error "Marketplace source path '$src' does not exist at $src_path"; fi
done < <(jq -r '.plugins[]?.source | strings' "$MARKETPLACE_JSON" 2>/dev/null)
fi
# ── report ────────────────────────────────────────────────────────────────────
echo ""
if [[ $ERRORS -eq 0 && $WARNINGS -eq 0 ]]; then
echo "✓ All checks passed."
exit 0
elif [[ $ERRORS -eq 0 ]]; then
echo "Passed with $WARNINGS warning(s)."
exit 0
else
echo "Failed. Fix $ERRORS error(s) before proceeding."
exit 1
fi

View File

@@ -0,0 +1,194 @@
#!/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 ]]

View File

@@ -12,4 +12,5 @@
| `iac` | write-ansible-role, write-terraform-module, write-k8s-manifest, write-docker-compose, proxmox-vm-spec, iac-security-review, write-molecule-test |
| `cross-cutting` | zoom-out, caveman, session-handoff, governance-check, git-guardrails, git-commit-message |
| `factory` | write-skill, write-adr, write-workflow, write-eval, validate-skill, upgrade-skill, write-issue-spec |
| `marketplace` | marketplace-architect — plugin and skill distribution tooling for Claude Code / GitHub Copilot CLI |
| `roles` | architect, developer, reviewer, security, qa, ops — Chunk 5 |